Skip to content

Commit e58c656

Browse files
prestwichclaude
andauthored
feat(host-rpc): add RpcAliasOracle with shared positive-result cache (#114)
* chore(host-rpc): add signet-block-processor and eyre dependencies * feat(host-rpc): add RpcAliasOracle and RpcAliasOracleFactory * refactor(host-rpc): collapse oracle types and add positive-result cache Merge RpcAliasOracle and RpcAliasOracleFactory into a single RpcAliasOracle type that implements both traits. Add a shared Arc<RwLock<HashSet>> cache for positive results — once an address is confirmed as a non-delegation contract, subsequent lookups return immediately without an RPC call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(host-rpc): address review feedback on RpcAliasOracle - Validate exact 23-byte length for EIP-7702 delegation detection - Add #[instrument] span on should_alias with debug events - Document std::sync::RwLock safety invariant (guards dropped before .await) - Extract should_alias_bytecode for testability, add 6 unit tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 375fd56 commit e58c656

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

crates/host-rpc/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ homepage.workspace = true
1010
repository.workspace = true
1111

1212
[dependencies]
13+
signet-block-processor.workspace = true
1314
signet-node-types.workspace = true
1415
signet-extract.workspace = true
1516
signet-types.workspace = true
1617

1718
alloy.workspace = true
19+
eyre.workspace = true
1820
init4-bin-base.workspace = true
1921
futures-util.workspace = true
2022
metrics.workspace = true

crates/host-rpc/src/alias.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use alloy::{
2+
eips::eip7702::constants::EIP7702_DELEGATION_DESIGNATOR,
3+
primitives::{Address, map::HashSet},
4+
providers::Provider,
5+
};
6+
use signet_block_processor::{AliasOracle, AliasOracleFactory};
7+
use std::sync::{Arc, RwLock};
8+
use tracing::{debug, instrument};
9+
10+
/// EIP-7702 delegation bytecode is exactly 23 bytes: 3-byte designator +
11+
/// 20-byte address.
12+
const EIP7702_DELEGATION_LEN: usize = 23;
13+
14+
/// An RPC-backed [`AliasOracle`] and [`AliasOracleFactory`].
15+
///
16+
/// Checks whether an address has non-delegation bytecode by fetching the
17+
/// code at the address via `eth_getCode`. Addresses with no code or with
18+
/// EIP-7702 delegation code are not aliased; addresses with any other
19+
/// bytecode are.
20+
///
21+
/// Positive results (address is a contract) are cached in a shared set.
22+
/// Contract status is permanent — an address cannot stop being a
23+
/// contract — so cached positives never go stale. All clones (including
24+
/// those produced by [`AliasOracleFactory::create`]) share the same
25+
/// cache.
26+
///
27+
/// Querying at `latest` is safe because alias status is stable across
28+
/// blocks: an EOA cannot become a non-delegation contract without a
29+
/// birthday attack (c.f. EIP-3607), and EIP-7702 delegations are
30+
/// excluded by the delegation designator check. Even in the
31+
/// (computationally infeasible ~2^80) birthday attack scenario, the
32+
/// result is a benign false-positive (over-aliasing), never a dangerous
33+
/// false-negative.
34+
#[derive(Debug, Clone)]
35+
pub struct RpcAliasOracle<P> {
36+
provider: P,
37+
/// Shared cache of addresses known to be non-delegation contracts.
38+
cache: Arc<RwLock<HashSet<Address>>>,
39+
}
40+
41+
impl<P> RpcAliasOracle<P> {
42+
/// Create a new [`RpcAliasOracle`] from an alloy provider.
43+
pub fn new(provider: P) -> Self {
44+
Self { provider, cache: Arc::new(RwLock::new(HashSet::default())) }
45+
}
46+
}
47+
48+
/// Classify bytecode: returns `true` if the code belongs to a
49+
/// non-delegation contract that should be aliased.
50+
fn should_alias_bytecode(code: &[u8]) -> bool {
51+
if code.is_empty() {
52+
return false;
53+
}
54+
// EIP-7702 delegation: exactly 23 bytes starting with the designator.
55+
if code.len() == EIP7702_DELEGATION_LEN && code.starts_with(&EIP7702_DELEGATION_DESIGNATOR) {
56+
return false;
57+
}
58+
true
59+
}
60+
61+
impl<P: Provider + Clone + 'static> AliasOracle for RpcAliasOracle<P> {
62+
#[instrument(skip(self), fields(%address))]
63+
async fn should_alias(&self, address: Address) -> eyre::Result<bool> {
64+
// NOTE: `std::sync::RwLock` is safe here because guards are always
65+
// dropped before the `.await` point. Do not hold a guard across the
66+
// `get_code_at` call — it will deadlock on single-threaded runtimes.
67+
68+
// Check cache first — if we've seen this address as a contract, skip RPC.
69+
if self.cache.read().expect("cache poisoned").contains(&address) {
70+
debug!("cache hit");
71+
return Ok(true);
72+
}
73+
74+
let code = self.provider.get_code_at(address).await?;
75+
let alias = should_alias_bytecode(&code);
76+
debug!(code_len = code.len(), alias, "resolved");
77+
78+
if alias {
79+
self.cache.write().expect("cache poisoned").insert(address);
80+
}
81+
82+
Ok(alias)
83+
}
84+
}
85+
86+
impl<P: Provider + Clone + 'static> AliasOracleFactory for RpcAliasOracle<P> {
87+
type Oracle = Self;
88+
89+
fn create(&self) -> eyre::Result<Self::Oracle> {
90+
Ok(self.clone())
91+
}
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use super::*;
97+
use alloy::eips::eip7702::constants::EIP7702_CLEARED_DELEGATION;
98+
99+
#[test]
100+
fn empty_code_is_not_aliased() {
101+
assert!(!should_alias_bytecode(&[]));
102+
}
103+
104+
#[test]
105+
fn valid_delegation_is_not_aliased() {
106+
// 3-byte designator + 20-byte address = 23 bytes
107+
let mut delegation = [0u8; 23];
108+
delegation[..3].copy_from_slice(&EIP7702_DELEGATION_DESIGNATOR);
109+
delegation[3..].copy_from_slice(&[0xAB; 20]);
110+
assert!(!should_alias_bytecode(&delegation));
111+
}
112+
113+
#[test]
114+
fn cleared_delegation_is_not_aliased() {
115+
assert!(!should_alias_bytecode(&EIP7702_CLEARED_DELEGATION));
116+
}
117+
118+
#[test]
119+
fn contract_bytecode_is_aliased() {
120+
// Typical contract: starts with PUSH, not 0xef
121+
assert!(should_alias_bytecode(&[0x60, 0x80, 0x60, 0x40, 0x52]));
122+
}
123+
124+
#[test]
125+
fn short_ef_prefix_is_aliased() {
126+
// Only 3 bytes starting with the designator — not a valid 23-byte
127+
// delegation, so it should be treated as a contract.
128+
assert!(should_alias_bytecode(&EIP7702_DELEGATION_DESIGNATOR));
129+
}
130+
131+
#[test]
132+
fn long_ef_prefix_is_aliased() {
133+
// 24 bytes starting with the designator — too long to be a
134+
// delegation, so it should be treated as a contract.
135+
let mut long = [0u8; 24];
136+
long[..3].copy_from_slice(&EIP7702_DELEGATION_DESIGNATOR);
137+
assert!(should_alias_bytecode(&long));
138+
}
139+
}

crates/host-rpc/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ pub use notifier::RpcHostNotifier;
3131
mod segment;
3232
pub use segment::{RpcBlock, RpcChainSegment};
3333
mod metrics;
34+
35+
mod alias;
36+
pub use alias::RpcAliasOracle;

0 commit comments

Comments
 (0)