Skip to content

Commit d2000ca

Browse files
authored
std: Add non-terminal read API to StorageBytes (argotorg#1510)
StorageBytes could only read stored bytes via encode_return, which ends the call. Add to_memory, which copies the stored bytes into freshly allocated memory and returns (ptr, len) so callers can post-process them (hash, deploy via create2_raw, etc), plus word_at for random access to individual payload words.
1 parent 5de2480 commit d2000ca

2 files changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use std::evm::{Address, Create, Evm, StorageBytes, ceil_words, keccak256, mem}
2+
3+
type Blobs = StorageBytes<u256, 0, 1>
4+
5+
msg BlobMsg {
6+
#[selector = 0x01]
7+
Set { data: Bytes },
8+
#[selector = 0x02]
9+
LoadHash -> u256,
10+
#[selector = 0x03]
11+
GetMarker -> u256,
12+
#[selector = 0x04]
13+
Deploy { salt: u256 } -> u256,
14+
#[selector = 0x05]
15+
WordAt { index: u256 } -> u256,
16+
}
17+
18+
pub contract Blob uses (create: mut Create) {
19+
mut blobs: Blobs,
20+
mut marker: u256,
21+
22+
recv BlobMsg {
23+
Set { data } uses (mut blobs) {
24+
blobs.store_view(key: 0, view: data.view())
25+
}
26+
27+
// Loads the stored bytes back into memory, then keeps executing:
28+
// writes `marker` and returns a hash over the zero-padded payload.
29+
LoadHash -> u256 uses (blobs, mut marker) {
30+
let (ptr, len) = blobs.to_memory(key: 0)
31+
let hash = keccak256(ptr, ceil_words(len) * 32)
32+
marker = len + 1
33+
hash
34+
}
35+
36+
GetMarker -> u256 uses (marker) {
37+
marker
38+
}
39+
40+
// The motivating use case: deploy the stored bytes as initcode.
41+
Deploy { salt } -> u256 uses (blobs, mut create, mut marker) {
42+
let (ptr, len) = blobs.to_memory(key: 0)
43+
let deployed = create.create2_raw(value: 0, offset: ptr, len: len, salt: salt)
44+
marker = len + 1
45+
deployed.inner
46+
}
47+
48+
WordAt { index } -> u256 uses (blobs) {
49+
blobs.word_at(key: 0, index: index)
50+
}
51+
}
52+
}
53+
54+
fn set_bytes(addr: Address, data: Bytes)
55+
uses (evm: mut Evm)
56+
{
57+
evm.call(addr: addr, gas: 1000000, value: 0, message: BlobMsg::Set { data: data })
58+
}
59+
60+
fn load_hash(addr: Address) -> u256
61+
uses (evm: mut Evm)
62+
{
63+
let hash: u256 = evm.call(addr: addr, gas: 1000000, value: 0, message: BlobMsg::LoadHash {})
64+
hash
65+
}
66+
67+
fn get_marker(addr: Address) -> u256
68+
uses (evm: mut Evm)
69+
{
70+
let marker: u256 = evm.call(addr: addr, gas: 1000000, value: 0, message: BlobMsg::GetMarker {})
71+
marker
72+
}
73+
74+
#[test]
75+
fn test_to_memory_empty()
76+
uses (evm: mut Evm)
77+
{
78+
let blob = evm.create2<Blob>(value: 0, args: (), salt: 0)
79+
assert!(blob.inner != 0)
80+
81+
let data_ptr = mem::alloc(32)
82+
evm.mstore(addr: data_ptr, value: 0)
83+
set_bytes(addr: blob, data: Bytes { data: data_ptr, len: 0, size: 32 })
84+
85+
assert!(load_hash(addr: blob) == keccak256(0, 0))
86+
87+
// Execution continued after the read: the handler wrote `marker`.
88+
assert!(get_marker(addr: blob) == 1)
89+
}
90+
91+
#[test]
92+
fn test_to_memory_exact_word_multiple()
93+
uses (evm: mut Evm)
94+
{
95+
let blob = evm.create2<Blob>(value: 0, args: (), salt: 0)
96+
assert!(blob.inner != 0)
97+
98+
let payload_len: u256 = 64
99+
let data_ptr = mem::alloc(payload_len + 32)
100+
evm.mstore(addr: data_ptr, value: payload_len)
101+
evm.mstore(
102+
addr: data_ptr + 32,
103+
value: 0x1111111111111111111111111111111111111111111111111111111111111111,
104+
)
105+
evm.mstore(
106+
addr: data_ptr + 64,
107+
value: 0x2222222222222222222222222222222222222222222222222222222222222222,
108+
)
109+
set_bytes(addr: blob, data: Bytes { data: data_ptr, len: payload_len, size: payload_len + 32 })
110+
111+
// Hash of the two payload words, computed locally.
112+
let expected = keccak256(data_ptr + 32, 64)
113+
assert!(load_hash(addr: blob) == expected)
114+
assert!(get_marker(addr: blob) == 65)
115+
}
116+
117+
#[test]
118+
fn test_to_memory_partial_trailing_word()
119+
uses (evm: mut Evm)
120+
{
121+
let blob = evm.create2<Blob>(value: 0, args: (), salt: 0)
122+
assert!(blob.inner != 0)
123+
124+
let payload_len: u256 = 65
125+
let data_ptr = mem::alloc(128)
126+
evm.mstore(addr: data_ptr, value: payload_len)
127+
evm.mstore(
128+
addr: data_ptr + 32,
129+
value: 0x1111111111111111111111111111111111111111111111111111111111111111,
130+
)
131+
evm.mstore(
132+
addr: data_ptr + 64,
133+
value: 0x2222222222222222222222222222222222222222222222222222222222222222,
134+
)
135+
evm.mstore(
136+
addr: data_ptr + 96,
137+
value: 0x3300000000000000000000000000000000000000000000000000000000000000,
138+
)
139+
set_bytes(addr: blob, data: Bytes { data: data_ptr, len: payload_len, size: 128 })
140+
141+
// The contract hashes ceil_words(65) * 32 = 96 bytes, so a matching hash
142+
// proves the 31 padding bytes of the trailing word come back zeroed.
143+
let expected = keccak256(data_ptr + 32, 96)
144+
assert!(load_hash(addr: blob) == expected)
145+
assert!(get_marker(addr: blob) == 66)
146+
147+
let word0: u256 = evm.call(addr: blob, gas: 1000000, value: 0, message: BlobMsg::WordAt { index: 0 })
148+
assert!(word0 == 0x1111111111111111111111111111111111111111111111111111111111111111)
149+
let word2: u256 = evm.call(addr: blob, gas: 1000000, value: 0, message: BlobMsg::WordAt { index: 2 })
150+
assert!(word2 == 0x3300000000000000000000000000000000000000000000000000000000000000)
151+
let word3: u256 = evm.call(addr: blob, gas: 1000000, value: 0, message: BlobMsg::WordAt { index: 3 })
152+
assert!(word3 == 0)
153+
}
154+
155+
#[test]
156+
fn test_to_memory_deploys_stored_initcode()
157+
uses (evm: mut Evm)
158+
{
159+
let blob = evm.create2<Blob>(value: 0, args: (), salt: 0)
160+
assert!(blob.inner != 0)
161+
162+
// 22-byte initcode that deploys a 10-byte runtime returning 0x2a:
163+
// 600a600c600039600a6000f3 copy runtime to memory and return it
164+
// 602a60005260206000f3 runtime: mstore(0, 42); return(0, 32)
165+
let payload_len: u256 = 22
166+
let data_ptr = mem::alloc(64)
167+
evm.mstore(addr: data_ptr, value: payload_len)
168+
evm.mstore(
169+
addr: data_ptr + 32,
170+
value: 0x600a600c600039600a6000f3602a60005260206000f300000000000000000000,
171+
)
172+
set_bytes(addr: blob, data: Bytes { data: data_ptr, len: payload_len, size: 64 })
173+
174+
let deployed: u256 = evm.call(addr: blob, gas: 1000000, value: 0, message: BlobMsg::Deploy { salt: 7 })
175+
assert!(deployed != 0)
176+
177+
// Execution continued after the read and the CREATE2.
178+
assert!(get_marker(addr: blob) == 23)
179+
}

ingots/std/src/evm/storage_bytes.fe

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,38 @@ impl<K: StorageKey + Copy, const LEN_SALT: u256, const WORD_SALT: u256>
197197
}
198198
}
199199

200+
/// Copy the stored bytes into freshly allocated memory.
201+
///
202+
/// Returns the memory offset of the first payload byte and the byte
203+
/// length. Unlike `encode_return`, this does not end the call, so the
204+
/// bytes can be post-processed (hashed, deployed via `create2_raw`, etc).
205+
/// The trailing partial word, if any, is zero-padded.
206+
pub fn to_memory(self, key: K) -> (u256, u256) {
207+
let lengths: StorageMap<K, u256, LEN_SALT> = StorageMap::new_unchecked()
208+
let words: StorageMap<(K, u256), u256, WORD_SALT> = StorageMap::new_unchecked()
209+
210+
let len = lengths.get_unchecked(key)
211+
let word_count = ceil_words(len)
212+
let ptr = mem::alloc(word_count * 32)
213+
214+
let mut i: u256 = 0
215+
while i < word_count {
216+
ops::mstore(addr: ptr + i * 32, value: words.get_unchecked((key, i)))
217+
i += 1
218+
}
219+
220+
(ptr, len)
221+
}
222+
223+
/// Load the 32-byte payload word at word `index` for `key`.
224+
///
225+
/// The last word of a non-word-multiple length is zero-padded; indices
226+
/// past `ceil_words(len(key))` read as zero.
227+
pub fn word_at(self, key: K, index: u256) -> u256 {
228+
let words: StorageMap<(K, u256), u256, WORD_SALT> = StorageMap::new_unchecked()
229+
words.get_unchecked((key, index))
230+
}
231+
200232
/// Return ABI-encoded `bytes` from storage.
201233
pub fn encode_return(self, key: K) -> ! {
202234
let lengths: StorageMap<K, u256, LEN_SALT> = StorageMap::new_unchecked()

0 commit comments

Comments
 (0)