Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions prdoc/pr_7857.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
title: Add new host APIs set_storage_or_clear and get_storage_or_zero
doc:
- audience: Runtime Dev
description: "# Description\n\n*This PR introduces two new storage API functions\u2014\
set_storage_or_clear and get_storage_or_zero\u2014which provide fixed\u2011size\
\ (32\u2011byte) storage operations. These APIs are an attempt to match Ethereum\u2019\
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.*\n\nFixes #6944\n## Review\
\ Notes\n\n* Changes in `runtime.rs`\nAdded 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.\nAdded 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.\n* Changes in\
\ `storage.rs`\nAdded test cases to cover the new set_storage_or_clear and get_storage_or_zero\
\ APIs.\n.\n\n```\n// Example usage of the new set_storage_or_clear function\n\
let existing = api::set_storage_or_clear(StorageFlags::empty(), &KEY, &VALUE_A);\n\
assert_eq!(existing, None);\n\n// Example usage of the new get_storage_or_zero\
\ function\nlet mut stored: [u8; 32] = [0u8; 32];\nlet _ = api::get_storage_or_zero(StorageFlags::empty(),\
\ &KEY, &mut stored);\nassert_eq!(stored, VALUE_A);\n```\n\n*All existing tests\
\ pass*\n\n# Checklist\n\n* [x] My PR includes a detailed description as outlined\
\ in the \"Description\" and its two subsections above.\n* [ ] My PR follows the\
\ [labeling requirements](\nhttps://github.com/paritytech/polkadot-sdk/blob/master/docs/contributor/CONTRIBUTING.md#Process\n\
) of this project (at minimum one label for `T` required)\n * External contributors:\
\ ask maintainers to put the right label on your PR.\n* [x] I have made corresponding\
\ changes to the documentation (if applicable)\n* [x] I have added tests that\
\ prove my fix is effective or that my feature works (if applicable)"
crates:
- name: pallet-revive-fixtures
bump: patch
- name: pallet-revive
bump: patch
- name: pallet-revive-uapi
bump: minor
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! This contract tests the storage APIs. It sets and clears storage values using the different
//! versions of the storage APIs.
#![no_std]
#![no_main]

include!("../panic_handler.rs");
use uapi::{HostFn, HostFnImpl as api, StorageFlags};

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn deploy() {}

fn test_storage_operations(flags: StorageFlags) {
const KEY: [u8; 32] = [1u8; 32];
const VALUE_A: [u8; 32] = [4u8; 32];
const ZERO: [u8; 32] = [0u8; 32];
let mut small_value_padded = [0u8; 32];
small_value_padded[0] = 5;
small_value_padded[1] = 6;
small_value_padded[2] = 7;

api::clear_storage(flags, &KEY);

assert_eq!(api::contains_storage(flags, &KEY), None);

let existing = api::set_storage_or_clear(flags, &KEY, &VALUE_A);
assert_eq!(existing, None);

let mut stored: [u8; 32] = [0u8; 32];
api::get_storage_or_zero(flags, &KEY, &mut stored);
assert_eq!(stored, VALUE_A);

let existing = api::set_storage_or_clear(flags, &KEY, &ZERO);
assert_eq!(existing, Some(32));

let mut cleared: [u8; 32] = [1u8; 32];
api::get_storage_or_zero(flags, &KEY, &mut cleared);
assert_eq!(cleared, ZERO);

assert_eq!(api::contains_storage(flags, &KEY), None);

// Test retrieving a value smaller than 32 bytes
api::set_storage_or_clear(flags, &KEY, &small_value_padded);
let mut retrieved = [255u8; 32];
api::get_storage_or_zero(flags, &KEY, &mut retrieved);

assert_eq!(retrieved[0], 5);
assert_eq!(retrieved[1], 6);
assert_eq!(retrieved[2], 7);
for i in 3..32 {
assert_eq!(retrieved[i], 0, "Byte at position {} should be zero", i);
}

// Clean up
api::clear_storage(flags, &KEY);
}

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn call() {
// Test with regular storage
test_storage_operations(StorageFlags::empty());

// Test with transient storage
test_storage_operations(StorageFlags::TRANSIENT);
}
15 changes: 15 additions & 0 deletions substrate/frame/revive/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,21 @@ fn storage_max_value_limit() {
});
}

#[test]
fn clear_storage_on_zero_value() {
let (code, _code_hash) = compile_module("clear_storage_on_zero_value").unwrap();

ExtBuilder::default().build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);
let min_balance = Contracts::min_balance();
let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code))
.value(min_balance * 100)
.build_and_unwrap_contract();

builder::bare_call(addr).build_and_unwrap_result();
});
}

#[test]
fn transient_storage_work() {
let (code, _code_hash) = compile_module("transient_storage").unwrap();
Expand Down
155 changes: 139 additions & 16 deletions substrate/frame/revive/src/wasm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,29 @@ fn extract_hi_lo(reg: u64) -> (u32, u32) {
((reg >> 32) as u32, reg as u32)
}

/// Provides storage variants to support standard and Etheruem compatible semantics.
enum StorageValue {
/// Indicates that the storage value should be read from a memory buffer.
/// - `ptr`: A pointer to the start of the data in sandbox memory.
/// - `len`: The length (in bytes) of the data.
Memory { ptr: u32, len: u32 },

/// Indicates that the storage value is provided inline as a fixed-size (256-bit) value.
/// This is used by set_storage_or_clear() to avoid double reads.
/// This variant is used to implement Ethereum SSTORE-like semantics.
Value(Vec<u8>),
}

/// Controls the output behavior for storage reads, both when a key is found and when it is not.
enum StorageReadMode {
/// VariableOutput mode: if the key exists, the full stored value is returned
/// using the caller‑provided output length.
VariableOutput { output_len_ptr: u32 },
/// Ethereum commpatible(FixedOutput32) mode: always write a 32-byte value into the output
/// buffer. If the key is missing, write 32 bytes of zeros.
FixedOutput32,
}

/// Can only be used for one call.
pub struct Runtime<'a, E: Ext, M: ?Sized> {
ext: &'a mut E,
Expand Down Expand Up @@ -872,8 +895,7 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
flags: u32,
key_ptr: u32,
key_len: u32,
value_ptr: u32,
value_len: u32,
value: StorageValue,
) -> Result<u32, TrapReason> {
let transient = Self::is_transient(flags)?;
let costs = |new_bytes: u32, old_bytes: u32| {
Expand All @@ -883,18 +905,31 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
RuntimeCosts::SetStorage { new_bytes, old_bytes }
}
};

let value_len = match &value {
StorageValue::Memory { ptr: _, len } => *len,
StorageValue::Value(data) => data.len() as u32,
};

let max_size = self.ext.max_value_size();
let charged = self.charge_gas(costs(value_len, self.ext.max_value_size()))?;
if value_len > max_size {
return Err(Error::<E::T>::ValueTooLarge.into());
}

let key = self.decode_key(memory, key_ptr, key_len)?;
let value = Some(memory.read(value_ptr, value_len)?);

let value = match value {
StorageValue::Memory { ptr, len } => Some(memory.read(ptr, len)?),
StorageValue::Value(data) => Some(data),
};

let write_outcome = if transient {
self.ext.set_transient_storage(&key, value, false)?
} else {
self.ext.set_storage(&key, value, false)?
};

self.adjust_gas(charged, costs(value_len, write_outcome.old_len()));
Ok(write_outcome.old_len_with_sentinel())
}
Expand Down Expand Up @@ -932,7 +967,7 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
key_ptr: u32,
key_len: u32,
out_ptr: u32,
out_len_ptr: u32,
read_mode: StorageReadMode,
) -> Result<ReturnErrorCode, TrapReason> {
let transient = Self::is_transient(flags)?;
let costs = |len| {
Expand All @@ -949,20 +984,53 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
} else {
self.ext.get_storage(&key)
};

if let Some(value) = outcome {
self.adjust_gas(charged, costs(value.len() as u32));
self.write_sandbox_output(
memory,
out_ptr,
out_len_ptr,
&value,
false,
already_charged,
)?;
Ok(ReturnErrorCode::Success)

match read_mode {
StorageReadMode::FixedOutput32 => {
let mut fixed_output = [0u8; 32];
let len = value.len().min(fixed_output.len());
fixed_output[..len].copy_from_slice(&value[..len]);

self.write_fixed_sandbox_output(
memory,
out_ptr,
&fixed_output,
false,
already_charged,
)?;
Ok(ReturnErrorCode::Success)
},
StorageReadMode::VariableOutput { output_len_ptr: out_len_ptr } => {
self.write_sandbox_output(
memory,
out_ptr,
out_len_ptr,
&value,
false,
already_charged,
)?;
Ok(ReturnErrorCode::Success)
},
}
} else {
self.adjust_gas(charged, costs(0));
Ok(ReturnErrorCode::KeyNotFound)

match read_mode {
StorageReadMode::FixedOutput32 => {
self.write_fixed_sandbox_output(
memory,
out_ptr,
&[0u8; 32],
false,
already_charged,
)?;
Ok(ReturnErrorCode::Success)
},
StorageReadMode::VariableOutput { .. } => Ok(ReturnErrorCode::KeyNotFound),
}
}
}

Expand Down Expand Up @@ -1224,7 +1292,33 @@ pub mod env {
value_ptr: u32,
value_len: u32,
) -> Result<u32, TrapReason> {
self.set_storage(memory, flags, key_ptr, key_len, value_ptr, value_len)
self.set_storage(
memory,
flags,
key_ptr,
key_len,
StorageValue::Memory { ptr: value_ptr, len: value_len },
)
}

/// Sets the storage at a fixed 256-bit key with a fixed 256-bit value.
/// See [`pallet_revive_uapi::HostFn::set_storage_or_clear`].
#[stable]
#[mutating]
fn set_storage_or_clear(
&mut self,
memory: &mut M,
flags: u32,
key_ptr: u32,
value_ptr: u32,
) -> Result<u32, TrapReason> {
let value = memory.read(value_ptr, 32)?;

if value.iter().all(|&b| b == 0) {
self.clear_storage(memory, flags, key_ptr, SENTINEL)
} else {
self.set_storage(memory, flags, key_ptr, SENTINEL, StorageValue::Value(value))
}
}

/// Retrieve the value under the given key from storage.
Expand All @@ -1239,7 +1333,36 @@ pub mod env {
out_ptr: u32,
out_len_ptr: u32,
) -> Result<ReturnErrorCode, TrapReason> {
self.get_storage(memory, flags, key_ptr, key_len, out_ptr, out_len_ptr)
self.get_storage(
memory,
flags,
key_ptr,
key_len,
out_ptr,
StorageReadMode::VariableOutput { output_len_ptr: out_len_ptr },
)
}

/// Reads the storage at a fixed 256-bit key and writes back a fixed 256-bit value.
/// See [`pallet_revive_uapi::HostFn::get_storage_or_zero`].
#[stable]
fn get_storage_or_zero(
&mut self,
memory: &mut M,
flags: u32,
key_ptr: u32,
out_ptr: u32,
) -> Result<(), TrapReason> {
let _ = self.get_storage(
memory,
flags,
key_ptr,
SENTINEL,
out_ptr,
StorageReadMode::FixedOutput32,
)?;

Ok(())
}

/// Make a call to another contract.
Expand Down
22 changes: 22 additions & 0 deletions substrate/frame/revive/uapi/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,28 @@ pub trait HostFn: private::Sealed {
/// Returns the size of the pre-existing value at the specified key if any.
fn set_storage(flags: StorageFlags, key: &[u8], value: &[u8]) -> Option<u32>;

/// Sets the storage entry for a fixed 256‑bit key with a fixed 256‑bit value.
///
/// If the provided 32‑byte value is all zeros then the key is cleared (i.e. deleted),
/// mimicking Ethereum’s SSTORE behavior.
///
/// # Parameters
/// - `key`: The fixed 256‑bit storage key (32 bytes).
/// - `value`: The fixed 256‑bit storage value (32 bytes).
///
/// # Return
/// Returns the size (in bytes) of the pre‑existing value at the specified key, if any.
fn set_storage_or_clear(flags: StorageFlags, key: &[u8; 32], value: &[u8; 32]) -> Option<u32>;

/// Retrieves the storage entry for a fixed 256‑bit key.
///
/// If the key does not exist, the output buffer is filled with 32 zero bytes.
///
/// # Parameters
/// - `key`: The fixed 256‑bit storage key (32 bytes).
/// - `output`: A mutable output buffer (32 bytes) where the storage entry is written.
fn get_storage_or_zero(flags: StorageFlags, key: &[u8; 32], output: &mut [u8; 32]);

/// Stores the value transferred along with this call/instantiate into the supplied buffer.
///
/// # Parameters
Expand Down
Loading
Loading