Skip to content

Commit 7e5b993

Browse files
castillaxgithub-actions[bot]atheixermicus
authored
Add new host APIs set_storage_or_clear and get_storage_or_zero (#7857)
# Description *This PR introduces two new storage API functions—set_storage_or_clear and get_storage_or_zero—which provide fixed‑size (32‑byte) storage operations. These APIs are an attempt to match Ethereum’s SSTORE semantics. These APIs provide additional functionality for setting and retrieving storage values and clearing storage when a zero value is provided and returning zero bytes when a key does not exist.* Fixes #6944 ## Review Notes * Changes in `runtime.rs` Added the set_storage_or_clear function to set storage at a fixed 256-bit key with a fixed 256-bit value. If the provided value is all zeros, the key is cleared. Added the get_storage_or_zero function to read storage at a fixed 256-bit key and write back a fixed 256-bit value. If the key does not exist, 32 bytes of zero are written back. * Changes in `storage.rs` Added test cases to cover the new set_storage_or_clear and get_storage_or_zero APIs. . ``` // Example usage of the new set_storage_or_clear function let existing = api::set_storage_or_clear(StorageFlags::empty(), &KEY, &VALUE_A); assert_eq!(existing, None); // Example usage of the new get_storage_or_zero function let mut stored: [u8; 32] = [0u8; 32]; let _ = api::get_storage_or_zero(StorageFlags::empty(), &KEY, &mut stored); assert_eq!(stored, VALUE_A); ``` *All existing tests pass* # Checklist * [x] My PR includes a detailed description as outlined in the "Description" and its two subsections above. * [x] My PR follows the [labeling requirements]( https://github.com/paritytech/polkadot-sdk/blob/master/docs/contributor/CONTRIBUTING.md#Process ) of this project (at minimum one label for `T` required) * External contributors: ask maintainers to put the right label on your PR. * [x] I have made corresponding changes to the documentation (if applicable) * [x] I have added tests that prove my fix is effective or that my feature works (if applicable) --------- Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Alexander Theißen <[email protected]> Co-authored-by: xermicus <[email protected]>
1 parent 0171ff8 commit 7e5b993

6 files changed

Lines changed: 314 additions & 16 deletions

File tree

prdoc/pr_7857.prdoc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
title: Add new host APIs set_storage_or_clear and get_storage_or_zero
2+
doc:
3+
- audience: Runtime Dev
4+
description: "# Description\n\n*This PR introduces two new storage API functions\u2014\
5+
set_storage_or_clear and get_storage_or_zero\u2014which provide fixed\u2011size\
6+
\ (32\u2011byte) storage operations. These APIs are an attempt to match Ethereum\u2019\
7+
s SSTORE semantics. These APIs provide additional functionality for setting and\
8+
\ retrieving storage values and clearing storage when a zero value is provided\
9+
\ and returning zero bytes when a key does not exist.*\n\nFixes #6944\n## Review\
10+
\ Notes\n\n* Changes in `runtime.rs`\nAdded the set_storage_or_clear function\
11+
\ to set storage at a fixed 256-bit key with a fixed 256-bit value. If the provided\
12+
\ value is all zeros, the key is cleared.\nAdded the get_storage_or_zero function\
13+
\ to read storage at a fixed 256-bit key and write back a fixed 256-bit value.\
14+
\ If the key does not exist, 32 bytes of zero are written back.\n* Changes in\
15+
\ `storage.rs`\nAdded test cases to cover the new set_storage_or_clear and get_storage_or_zero\
16+
\ APIs.\n.\n\n```\n// Example usage of the new set_storage_or_clear function\n\
17+
let existing = api::set_storage_or_clear(StorageFlags::empty(), &KEY, &VALUE_A);\n\
18+
assert_eq!(existing, None);\n\n// Example usage of the new get_storage_or_zero\
19+
\ function\nlet mut stored: [u8; 32] = [0u8; 32];\nlet _ = api::get_storage_or_zero(StorageFlags::empty(),\
20+
\ &KEY, &mut stored);\nassert_eq!(stored, VALUE_A);\n```\n\n*All existing tests\
21+
\ pass*\n\n# Checklist\n\n* [x] My PR includes a detailed description as outlined\
22+
\ in the \"Description\" and its two subsections above.\n* [ ] My PR follows the\
23+
\ [labeling requirements](\nhttps://github.com/paritytech/polkadot-sdk/blob/master/docs/contributor/CONTRIBUTING.md#Process\n\
24+
) of this project (at minimum one label for `T` required)\n * External contributors:\
25+
\ ask maintainers to put the right label on your PR.\n* [x] I have made corresponding\
26+
\ changes to the documentation (if applicable)\n* [x] I have added tests that\
27+
\ prove my fix is effective or that my feature works (if applicable)"
28+
crates:
29+
- name: pallet-revive-fixtures
30+
bump: patch
31+
- name: pallet-revive
32+
bump: patch
33+
- name: pallet-revive-uapi
34+
bump: minor
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// This file is part of Substrate.
2+
3+
// Copyright (C) Parity Technologies (UK) Ltd.
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
18+
//! This contract tests the storage APIs. It sets and clears storage values using the different
19+
//! versions of the storage APIs.
20+
#![no_std]
21+
#![no_main]
22+
23+
include!("../panic_handler.rs");
24+
use uapi::{HostFn, HostFnImpl as api, StorageFlags};
25+
26+
#[no_mangle]
27+
#[polkavm_derive::polkavm_export]
28+
pub extern "C" fn deploy() {}
29+
30+
fn test_storage_operations(flags: StorageFlags) {
31+
const KEY: [u8; 32] = [1u8; 32];
32+
const VALUE_A: [u8; 32] = [4u8; 32];
33+
const ZERO: [u8; 32] = [0u8; 32];
34+
let mut small_value_padded = [0u8; 32];
35+
small_value_padded[0] = 5;
36+
small_value_padded[1] = 6;
37+
small_value_padded[2] = 7;
38+
39+
api::clear_storage(flags, &KEY);
40+
41+
assert_eq!(api::contains_storage(flags, &KEY), None);
42+
43+
let existing = api::set_storage_or_clear(flags, &KEY, &VALUE_A);
44+
assert_eq!(existing, None);
45+
46+
let mut stored: [u8; 32] = [0u8; 32];
47+
api::get_storage_or_zero(flags, &KEY, &mut stored);
48+
assert_eq!(stored, VALUE_A);
49+
50+
let existing = api::set_storage_or_clear(flags, &KEY, &ZERO);
51+
assert_eq!(existing, Some(32));
52+
53+
let mut cleared: [u8; 32] = [1u8; 32];
54+
api::get_storage_or_zero(flags, &KEY, &mut cleared);
55+
assert_eq!(cleared, ZERO);
56+
57+
assert_eq!(api::contains_storage(flags, &KEY), None);
58+
59+
// Test retrieving a value smaller than 32 bytes
60+
api::set_storage_or_clear(flags, &KEY, &small_value_padded);
61+
let mut retrieved = [255u8; 32];
62+
api::get_storage_or_zero(flags, &KEY, &mut retrieved);
63+
64+
assert_eq!(retrieved[0], 5);
65+
assert_eq!(retrieved[1], 6);
66+
assert_eq!(retrieved[2], 7);
67+
for i in 3..32 {
68+
assert_eq!(retrieved[i], 0, "Byte at position {} should be zero", i);
69+
}
70+
71+
// Clean up
72+
api::clear_storage(flags, &KEY);
73+
}
74+
75+
#[no_mangle]
76+
#[polkavm_derive::polkavm_export]
77+
pub extern "C" fn call() {
78+
// Test with regular storage
79+
test_storage_operations(StorageFlags::empty());
80+
81+
// Test with transient storage
82+
test_storage_operations(StorageFlags::TRANSIENT);
83+
}

substrate/frame/revive/src/tests.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,21 @@ fn storage_max_value_limit() {
946946
});
947947
}
948948

949+
#[test]
950+
fn clear_storage_on_zero_value() {
951+
let (code, _code_hash) = compile_module("clear_storage_on_zero_value").unwrap();
952+
953+
ExtBuilder::default().build().execute_with(|| {
954+
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
955+
let min_balance = Contracts::min_balance();
956+
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code))
957+
.value(min_balance * 100)
958+
.build_and_unwrap_contract();
959+
960+
builder::bare_call(addr).build_and_unwrap_result();
961+
});
962+
}
963+
949964
#[test]
950965
fn transient_storage_work() {
951966
let (code, _code_hash) = compile_module("transient_storage").unwrap();

substrate/frame/revive/src/wasm/runtime.rs

Lines changed: 139 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,29 @@ fn extract_hi_lo(reg: u64) -> (u32, u32) {
606606
((reg >> 32) as u32, reg as u32)
607607
}
608608

609+
/// Provides storage variants to support standard and Etheruem compatible semantics.
610+
enum StorageValue {
611+
/// Indicates that the storage value should be read from a memory buffer.
612+
/// - `ptr`: A pointer to the start of the data in sandbox memory.
613+
/// - `len`: The length (in bytes) of the data.
614+
Memory { ptr: u32, len: u32 },
615+
616+
/// Indicates that the storage value is provided inline as a fixed-size (256-bit) value.
617+
/// This is used by set_storage_or_clear() to avoid double reads.
618+
/// This variant is used to implement Ethereum SSTORE-like semantics.
619+
Value(Vec<u8>),
620+
}
621+
622+
/// Controls the output behavior for storage reads, both when a key is found and when it is not.
623+
enum StorageReadMode {
624+
/// VariableOutput mode: if the key exists, the full stored value is returned
625+
/// using the caller‑provided output length.
626+
VariableOutput { output_len_ptr: u32 },
627+
/// Ethereum commpatible(FixedOutput32) mode: always write a 32-byte value into the output
628+
/// buffer. If the key is missing, write 32 bytes of zeros.
629+
FixedOutput32,
630+
}
631+
609632
/// Can only be used for one call.
610633
pub struct Runtime<'a, E: Ext, M: ?Sized> {
611634
ext: &'a mut E,
@@ -872,8 +895,7 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
872895
flags: u32,
873896
key_ptr: u32,
874897
key_len: u32,
875-
value_ptr: u32,
876-
value_len: u32,
898+
value: StorageValue,
877899
) -> Result<u32, TrapReason> {
878900
let transient = Self::is_transient(flags)?;
879901
let costs = |new_bytes: u32, old_bytes: u32| {
@@ -883,18 +905,31 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
883905
RuntimeCosts::SetStorage { new_bytes, old_bytes }
884906
}
885907
};
908+
909+
let value_len = match &value {
910+
StorageValue::Memory { ptr: _, len } => *len,
911+
StorageValue::Value(data) => data.len() as u32,
912+
};
913+
886914
let max_size = self.ext.max_value_size();
887915
let charged = self.charge_gas(costs(value_len, self.ext.max_value_size()))?;
888916
if value_len > max_size {
889917
return Err(Error::<E::T>::ValueTooLarge.into());
890918
}
919+
891920
let key = self.decode_key(memory, key_ptr, key_len)?;
892-
let value = Some(memory.read(value_ptr, value_len)?);
921+
922+
let value = match value {
923+
StorageValue::Memory { ptr, len } => Some(memory.read(ptr, len)?),
924+
StorageValue::Value(data) => Some(data),
925+
};
926+
893927
let write_outcome = if transient {
894928
self.ext.set_transient_storage(&key, value, false)?
895929
} else {
896930
self.ext.set_storage(&key, value, false)?
897931
};
932+
898933
self.adjust_gas(charged, costs(value_len, write_outcome.old_len()));
899934
Ok(write_outcome.old_len_with_sentinel())
900935
}
@@ -932,7 +967,7 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
932967
key_ptr: u32,
933968
key_len: u32,
934969
out_ptr: u32,
935-
out_len_ptr: u32,
970+
read_mode: StorageReadMode,
936971
) -> Result<ReturnErrorCode, TrapReason> {
937972
let transient = Self::is_transient(flags)?;
938973
let costs = |len| {
@@ -949,20 +984,53 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
949984
} else {
950985
self.ext.get_storage(&key)
951986
};
987+
952988
if let Some(value) = outcome {
953989
self.adjust_gas(charged, costs(value.len() as u32));
954-
self.write_sandbox_output(
955-
memory,
956-
out_ptr,
957-
out_len_ptr,
958-
&value,
959-
false,
960-
already_charged,
961-
)?;
962-
Ok(ReturnErrorCode::Success)
990+
991+
match read_mode {
992+
StorageReadMode::FixedOutput32 => {
993+
let mut fixed_output = [0u8; 32];
994+
let len = value.len().min(fixed_output.len());
995+
fixed_output[..len].copy_from_slice(&value[..len]);
996+
997+
self.write_fixed_sandbox_output(
998+
memory,
999+
out_ptr,
1000+
&fixed_output,
1001+
false,
1002+
already_charged,
1003+
)?;
1004+
Ok(ReturnErrorCode::Success)
1005+
},
1006+
StorageReadMode::VariableOutput { output_len_ptr: out_len_ptr } => {
1007+
self.write_sandbox_output(
1008+
memory,
1009+
out_ptr,
1010+
out_len_ptr,
1011+
&value,
1012+
false,
1013+
already_charged,
1014+
)?;
1015+
Ok(ReturnErrorCode::Success)
1016+
},
1017+
}
9631018
} else {
9641019
self.adjust_gas(charged, costs(0));
965-
Ok(ReturnErrorCode::KeyNotFound)
1020+
1021+
match read_mode {
1022+
StorageReadMode::FixedOutput32 => {
1023+
self.write_fixed_sandbox_output(
1024+
memory,
1025+
out_ptr,
1026+
&[0u8; 32],
1027+
false,
1028+
already_charged,
1029+
)?;
1030+
Ok(ReturnErrorCode::Success)
1031+
},
1032+
StorageReadMode::VariableOutput { .. } => Ok(ReturnErrorCode::KeyNotFound),
1033+
}
9661034
}
9671035
}
9681036

@@ -1224,7 +1292,33 @@ pub mod env {
12241292
value_ptr: u32,
12251293
value_len: u32,
12261294
) -> Result<u32, TrapReason> {
1227-
self.set_storage(memory, flags, key_ptr, key_len, value_ptr, value_len)
1295+
self.set_storage(
1296+
memory,
1297+
flags,
1298+
key_ptr,
1299+
key_len,
1300+
StorageValue::Memory { ptr: value_ptr, len: value_len },
1301+
)
1302+
}
1303+
1304+
/// Sets the storage at a fixed 256-bit key with a fixed 256-bit value.
1305+
/// See [`pallet_revive_uapi::HostFn::set_storage_or_clear`].
1306+
#[stable]
1307+
#[mutating]
1308+
fn set_storage_or_clear(
1309+
&mut self,
1310+
memory: &mut M,
1311+
flags: u32,
1312+
key_ptr: u32,
1313+
value_ptr: u32,
1314+
) -> Result<u32, TrapReason> {
1315+
let value = memory.read(value_ptr, 32)?;
1316+
1317+
if value.iter().all(|&b| b == 0) {
1318+
self.clear_storage(memory, flags, key_ptr, SENTINEL)
1319+
} else {
1320+
self.set_storage(memory, flags, key_ptr, SENTINEL, StorageValue::Value(value))
1321+
}
12281322
}
12291323

12301324
/// Retrieve the value under the given key from storage.
@@ -1239,7 +1333,36 @@ pub mod env {
12391333
out_ptr: u32,
12401334
out_len_ptr: u32,
12411335
) -> Result<ReturnErrorCode, TrapReason> {
1242-
self.get_storage(memory, flags, key_ptr, key_len, out_ptr, out_len_ptr)
1336+
self.get_storage(
1337+
memory,
1338+
flags,
1339+
key_ptr,
1340+
key_len,
1341+
out_ptr,
1342+
StorageReadMode::VariableOutput { output_len_ptr: out_len_ptr },
1343+
)
1344+
}
1345+
1346+
/// Reads the storage at a fixed 256-bit key and writes back a fixed 256-bit value.
1347+
/// See [`pallet_revive_uapi::HostFn::get_storage_or_zero`].
1348+
#[stable]
1349+
fn get_storage_or_zero(
1350+
&mut self,
1351+
memory: &mut M,
1352+
flags: u32,
1353+
key_ptr: u32,
1354+
out_ptr: u32,
1355+
) -> Result<(), TrapReason> {
1356+
let _ = self.get_storage(
1357+
memory,
1358+
flags,
1359+
key_ptr,
1360+
SENTINEL,
1361+
out_ptr,
1362+
StorageReadMode::FixedOutput32,
1363+
)?;
1364+
1365+
Ok(())
12431366
}
12441367

12451368
/// Make a call to another contract.

substrate/frame/revive/uapi/src/host.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,28 @@ pub trait HostFn: private::Sealed {
368368
/// Returns the size of the pre-existing value at the specified key if any.
369369
fn set_storage(flags: StorageFlags, key: &[u8], value: &[u8]) -> Option<u32>;
370370

371+
/// Sets the storage entry for a fixed 256‑bit key with a fixed 256‑bit value.
372+
///
373+
/// If the provided 32‑byte value is all zeros then the key is cleared (i.e. deleted),
374+
/// mimicking Ethereum’s SSTORE behavior.
375+
///
376+
/// # Parameters
377+
/// - `key`: The fixed 256‑bit storage key (32 bytes).
378+
/// - `value`: The fixed 256‑bit storage value (32 bytes).
379+
///
380+
/// # Return
381+
/// Returns the size (in bytes) of the pre‑existing value at the specified key, if any.
382+
fn set_storage_or_clear(flags: StorageFlags, key: &[u8; 32], value: &[u8; 32]) -> Option<u32>;
383+
384+
/// Retrieves the storage entry for a fixed 256‑bit key.
385+
///
386+
/// If the key does not exist, the output buffer is filled with 32 zero bytes.
387+
///
388+
/// # Parameters
389+
/// - `key`: The fixed 256‑bit storage key (32 bytes).
390+
/// - `output`: A mutable output buffer (32 bytes) where the storage entry is written.
391+
fn get_storage_or_zero(flags: StorageFlags, key: &[u8; 32], output: &mut [u8; 32]);
392+
371393
/// Stores the value transferred along with this call/instantiate into the supplied buffer.
372394
///
373395
/// # Parameters

0 commit comments

Comments
 (0)