Skip to content

Commit 031785b

Browse files
authored
feat(u): add u slot subcommand for Solidity storage slot computation (#5475)
Closes #3417. ## What this does Adds a `u slot` subcommand (alias `u s`) to the `u` CLI. Given a Solidity storage layout type and an ordered list of keys, it prints the storage slot as a `0x`-prefixed 32-byte hex value to stdout. ``` u slot "mapping(uint256 => mapping(uint256 => uint256)[])" 100 1 123 # 0x00a9b48fe93e5d10ebc2d9021d1477088c6292bf047876944343f57fdf3f0467 ``` Keys go outermost to innermost. The output is bare (no label), so it pipes into other tools without any stripping. ## Implementation - New module `tools/u/src/slot.rs` with three private functions: `parse_layout` (wraps the bare type in a dummy state variable so `syn-solidity` can parse it), `parse_mapping_key` (maps a Solidity key type and string value to `MappingKey`), and `build_slot` (walks the type tree consuming keys front-to-back, allocating each `Slot` node in a `typed_arena::Arena` to satisfy the borrow-based lifetime constraints). - Reuses the existing `solidity-slot` library unchanged. - Wired into `main.rs` as `Cmd::Slot` with visible alias `s`. - Added `syn-solidity` and `typed-arena` to workspace dependencies. ## Test coverage 14 unit tests covering: nested mapping + array layout parsing, all supported key types (`uint256`, `uint64`, `bytes32`, `string`), rejection of unsupported types and malformed inputs, the known-answer vector from the issue, too-few and too-many keys, a single mapping, a single dynamic array, and a `bytes32` key. ## Checklist - [x] `cargo clippy -p u --all-targets` passes - [x] `cargo fmt -p u` produces no diff - [x] `cargo test -p u` passes (14/14) - [x] Known-answer vector confirmed end-to-end via CLI - [x] Positional args (not flags), per maintainer feedback - [x] Lives in existing `tools/u` crate, no new crate
2 parents 44061d0 + 87d1b09 commit 031785b

5 files changed

Lines changed: 249 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ static_assertions = { git = "https://github.com/nvzqz/static-assertions" }
553553
strum = { version = "0.27", default-features = false }
554554
subtle-encoding = { version = "0.5.1", default-features = false }
555555
syn = { version = "2", default-features = false }
556+
syn-solidity = { version = "1", default-features = false }
556557
thiserror = { version = "2.0.12", default-features = false }
557558
time = { version = "0.3.41", default-features = false }
558559
tokio = { version = "1.45.1", default-features = false }
@@ -563,6 +564,7 @@ tower = { version = "0.5", default-features = false }
563564
tower-http = { version = "0.6.4", default-features = false }
564565
tracing = { version = "0.1.41", default-features = false }
565566
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi"] }
567+
typed-arena = { version = "2", default-features = false }
566568
typenum = { version = "1.18.0", default-features = false }
567569

568570
# tracing-opentelemetry = { version = "0.30.0", default-features = false }

tools/u/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@ keccak-asm = "0.1.4"
3838
num_cpus = "1.16"
3939
parlia-verifier = { workspace = true }
4040
protos = { workspace = true, features = ["proto_full", "serde"] }
41+
syn = { workspace = true, features = ["parsing"] }
4142
rand = { workspace = true }
4243
serde = { workspace = true, features = ["derive"] }
4344
serde_json = { workspace = true }
4445
sha2 = { workspace = true, features = ["asm", "asm-aarch64"] }
46+
solidity-slot = { workspace = true }
4547
subtle-encoding = { workspace = true, features = ["bech32-preview"] }
48+
syn-solidity = { workspace = true }
4649
tokio = { workspace = true, features = ["macros"] }
50+
typed-arena = { workspace = true }
4751
tracing = { workspace = true }
4852
tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }
4953
ucs03-zkgm = { workspace = true }
@@ -67,3 +71,6 @@ state-lens-ics23-mpt-light-client-types = { workspace = true, features = ["ser
6771
state-lens-ics23-smt-light-client-types = { workspace = true, features = ["serde", "bincode", "ethabi"] }
6872
tendermint-light-client-types = { workspace = true, features = ["serde", "bincode", "ethabi"] }
6973
trusted-mpt-light-client-types = { workspace = true, features = ["serde", "bincode", "ethabi"] }
74+
75+
[dev-dependencies]
76+
hex-literal = { workspace = true }

tools/u/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod gno;
2525
pub mod packet;
2626
pub mod parlia;
2727
pub mod path;
28+
pub mod slot;
2829
pub mod vanity;
2930
pub mod zkgm;
3031

@@ -83,6 +84,8 @@ pub enum Cmd {
8384
Path(path::Cmd),
8485
#[command(subcommand)]
8586
Packet(packet::Cmd),
87+
#[command(visible_alias = "s")]
88+
Slot(slot::Cmd),
8689
#[command(visible_alias = "v", subcommand)]
8790
Vanity(vanity::Cmd),
8891
#[command(visible_alias = "h")]
@@ -136,6 +139,7 @@ async fn main() -> Result<()> {
136139
Cmd::Deployments(cmd) => cmd.run(),
137140
Cmd::Path(cmd) => cmd.run(),
138141
Cmd::Packet(cmd) => cmd.run(),
142+
Cmd::Slot(cmd) => cmd.run(),
139143
Cmd::Vanity(cmd) => cmd.run().await,
140144
Cmd::Hex {
141145
decode,

tools/u/src/slot.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
use core::str::FromStr;
2+
use std::collections::VecDeque;
3+
4+
use anyhow::{Context, bail};
5+
use solidity_slot::{H256, MappingKey, Slot, U256};
6+
use syn_solidity::Type;
7+
use typed_arena::Arena;
8+
9+
fn parse_layout(layout: &str) -> anyhow::Result<Type> {
10+
match syn::parse_str::<Type>(layout).context("failed to parse layout as Solidity type")? {
11+
ty @ (Type::Mapping(_) | Type::Array(_)) => Ok(ty),
12+
other => bail!("unsupported top-level layout type: {other:?}"),
13+
}
14+
}
15+
16+
fn parse_mapping_key<'a>(key_type: &Type, key: &'a str) -> anyhow::Result<MappingKey<'a>> {
17+
match key_type {
18+
Type::Uint(_, size) => {
19+
let bits = size.map_or(256, |s| s.get());
20+
match bits {
21+
256 => Ok(MappingKey::Uint256(
22+
U256::from_str(key).context("invalid uint256 key")?,
23+
)),
24+
64 => Ok(MappingKey::Uint64(
25+
key.parse::<u64>().context("invalid uint64 key")?,
26+
)),
27+
other => bail!("unsupported uint size for mapping key: uint{other}"),
28+
}
29+
}
30+
Type::FixedBytes(_, size) => match size.get() {
31+
32 => Ok(MappingKey::Bytes32(
32+
H256::from_str(key).context("invalid bytes32 key")?,
33+
)),
34+
other => bail!("unsupported fixed-bytes size for mapping key: bytes{other}"),
35+
},
36+
Type::String(_) => Ok(MappingKey::String(key)),
37+
other => bail!("unsupported mapping key type: {other:?}"),
38+
}
39+
}
40+
41+
#[derive(Debug, clap::Args)]
42+
pub struct Cmd {
43+
/// The Solidity storage layout type, e.g.
44+
/// "mapping(uint256 => mapping(uint256 => uint256)[])"
45+
pub layout: String,
46+
/// Keys from outermost to innermost, e.g. 100 1 123
47+
pub keys: Vec<String>,
48+
}
49+
50+
// Consumes one key per container, outermost first. Each Slot node lives in the arena so the borrowed tree stays valid.
51+
fn build_slot<'a>(
52+
ty: &Type,
53+
keys: &mut VecDeque<&'a str>,
54+
arena: &'a Arena<Slot<'a>>,
55+
) -> anyhow::Result<&'a Slot<'a>> {
56+
match ty {
57+
Type::Mapping(mapping) => {
58+
let key = keys
59+
.pop_front()
60+
.context("not enough keys: missing a mapping key")?;
61+
let mapping_key = parse_mapping_key(&mapping.key, key)?;
62+
let base = build_slot(&mapping.value, keys, arena)?;
63+
Ok(arena.alloc(Slot::Mapping(base, mapping_key)))
64+
}
65+
Type::Array(arr) => {
66+
let key = keys
67+
.pop_front()
68+
.context("not enough keys: missing an array index")?;
69+
let index = U256::from_str(key).context("invalid array index")?;
70+
let base = build_slot(&arr.ty, keys, arena)?;
71+
Ok(arena.alloc(Slot::Array(base, index)))
72+
}
73+
_ => Ok(arena.alloc(Slot::Offset(U256::from(0u32)))),
74+
}
75+
}
76+
77+
fn calculate_slot(layout: &str, keys: &[String]) -> anyhow::Result<U256> {
78+
let ty = parse_layout(layout)?;
79+
let mut queue: VecDeque<&str> = keys.iter().map(String::as_str).collect();
80+
let arena = Arena::new();
81+
let slot = build_slot(&ty, &mut queue, &arena)?;
82+
if !queue.is_empty() {
83+
bail!(
84+
"too many keys: {} leftover after walking the layout",
85+
queue.len()
86+
);
87+
}
88+
Ok(slot.slot())
89+
}
90+
91+
impl Cmd {
92+
pub fn run(&self) -> anyhow::Result<()> {
93+
let slot = calculate_slot(&self.layout, &self.keys)?;
94+
println!("{}", <H256>::new(slot.to_be_bytes()));
95+
Ok(())
96+
}
97+
}
98+
99+
#[cfg(test)]
100+
mod tests {
101+
use super::*;
102+
103+
#[test]
104+
fn parses_nested_mapping_layout() {
105+
let ty = parse_layout("mapping(uint256 => mapping(uint256 => uint256)[])")
106+
.expect("should parse");
107+
assert!(matches!(ty, syn_solidity::Type::Mapping(_)));
108+
}
109+
110+
#[test]
111+
fn rejects_garbage_layout() {
112+
assert!(parse_layout("not a type !!").is_err());
113+
}
114+
115+
#[test]
116+
fn parses_uint256_key() {
117+
let ty = key_type("uint256");
118+
let k = parse_mapping_key(&ty, "123").unwrap();
119+
assert!(matches!(k, MappingKey::Uint256(_)));
120+
}
121+
122+
#[test]
123+
fn parses_uint64_key() {
124+
let ty = key_type("uint64");
125+
assert!(matches!(
126+
parse_mapping_key(&ty, "7").unwrap(),
127+
MappingKey::Uint64(_)
128+
));
129+
}
130+
131+
#[test]
132+
fn parses_bytes32_key() {
133+
let ty = key_type("bytes32");
134+
let k = parse_mapping_key(
135+
&ty,
136+
"0x0000000000000000000000000000000000000000000000000000000000000001",
137+
)
138+
.unwrap();
139+
assert!(matches!(k, MappingKey::Bytes32(_)));
140+
}
141+
142+
#[test]
143+
fn parses_string_key() {
144+
let ty = key_type("string");
145+
assert!(matches!(
146+
parse_mapping_key(&ty, "hello").unwrap(),
147+
MappingKey::String(_)
148+
));
149+
}
150+
151+
#[test]
152+
fn rejects_bad_uint_key() {
153+
let ty = key_type("uint256");
154+
assert!(parse_mapping_key(&ty, "not-a-number").is_err());
155+
}
156+
157+
#[test]
158+
fn rejects_unsupported_key_type() {
159+
let ty = key_type("uint128");
160+
assert!(parse_mapping_key(&ty, "1").is_err());
161+
}
162+
163+
// Extract the key Type from a `mapping(KEY => uint256)` layout string.
164+
fn key_type(key: &str) -> Type {
165+
match parse_layout(&format!("mapping({key} => uint256)")).unwrap() {
166+
Type::Mapping(m) => *m.key,
167+
_ => unreachable!("constructed a mapping"),
168+
}
169+
}
170+
171+
#[test]
172+
fn known_answer_vector() {
173+
let keys = ["100".to_owned(), "1".to_owned(), "123".to_owned()];
174+
let slot =
175+
calculate_slot("mapping(uint256 => mapping(uint256 => uint256)[])", &keys).unwrap();
176+
assert_eq!(
177+
<H256>::new(slot.to_be_bytes()),
178+
<H256>::new(hex_literal::hex!(
179+
"00a9b48fe93e5d10ebc2d9021d1477088c6292bf047876944343f57fdf3f0467"
180+
))
181+
);
182+
}
183+
184+
#[test]
185+
fn too_few_keys_is_error() {
186+
let keys = ["100".to_owned()];
187+
assert!(
188+
calculate_slot("mapping(uint256 => mapping(uint256 => uint256)[])", &keys,).is_err()
189+
);
190+
}
191+
192+
#[test]
193+
fn too_many_keys_is_error() {
194+
let keys = [
195+
"100".to_owned(),
196+
"1".to_owned(),
197+
"123".to_owned(),
198+
"9".to_owned(),
199+
];
200+
assert!(
201+
calculate_slot("mapping(uint256 => mapping(uint256 => uint256)[])", &keys,).is_err()
202+
);
203+
}
204+
205+
#[test]
206+
fn single_mapping_uint256() {
207+
let keys = ["1".to_owned()];
208+
let a = calculate_slot("mapping(uint256 => uint256)", &keys).unwrap();
209+
let b = calculate_slot("mapping(uint256 => uint256)", &keys).unwrap();
210+
assert_eq!(a, b);
211+
assert_ne!(a, U256::from(0u32));
212+
}
213+
214+
#[test]
215+
fn single_dynamic_array() {
216+
let keys = ["2".to_owned()];
217+
let slot = calculate_slot("uint256[]", &keys).unwrap();
218+
let expected =
219+
U256::from_be_bytes(*solidity_slot::keccak256(U256::from(0u32).to_be_bytes()).get())
220+
+ U256::from(2u32);
221+
assert_eq!(slot, expected);
222+
}
223+
224+
#[test]
225+
fn bytes32_mapping_key() {
226+
let keys =
227+
["0x0000000000000000000000000000000000000000000000000000000000000001".to_owned()];
228+
let slot = calculate_slot("mapping(bytes32 => uint256)", &keys).unwrap();
229+
assert_ne!(slot, U256::from(0u32));
230+
}
231+
}

0 commit comments

Comments
 (0)