use alloy::primitives::{Address, B256};
#[derive(Clone, Debug)]
pub struct ScriptedLog {
pub topics: Vec<B256>,
pub data: Vec<u8>,
}
impl ScriptedLog {
#[must_use]
pub fn new(topics: Vec<B256>, data: Vec<u8>) -> Self {
Self { topics, data }
}
}
#[derive(Clone, Debug)]
pub struct ActorBranch {
pub calldata_len: u8,
pub logs: Vec<ScriptedLog>,
pub return_word: [u8; 32],
}
impl ActorBranch {
#[must_use]
pub fn new(calldata_len: u8, logs: Vec<ScriptedLog>, return_word: [u8; 32]) -> Self {
Self {
calldata_len,
logs,
return_word,
}
}
}
const PUSH1: u8 = 0x60;
const PUSH2: u8 = 0x61;
const PUSH4: u8 = 0x63;
const PUSH20: u8 = 0x73;
const PUSH32: u8 = 0x7f;
const DUP1: u8 = 0x80;
const EQ: u8 = 0x14;
const JUMPI: u8 = 0x57;
const JUMPDEST: u8 = 0x5b;
const POP: u8 = 0x50;
const CALLDATASIZE: u8 = 0x36;
const CODECOPY: u8 = 0x39;
const LOG0: u8 = 0xa0;
const MSTORE: u8 = 0x52;
const CALL: u8 = 0xf1;
const RETURN: u8 = 0xf3;
const LOG_MEMORY_PAGE: usize = 256;
const SUBCALL_GAS: u32 = 1_000_000;
const CREATION_HEADER_LEN: usize = 3 + 3 + 2 + 1 + 3 + 3 + 1;
fn push1(buf: &mut Vec<u8>, value: u8) {
buf.push(PUSH1);
buf.push(value);
}
fn push2(buf: &mut Vec<u8>, value: u16) {
buf.push(PUSH2);
buf.extend_from_slice(&value.to_be_bytes());
}
fn push4(buf: &mut Vec<u8>, value: u32) {
buf.push(PUSH4);
buf.extend_from_slice(&value.to_be_bytes());
}
fn push20(buf: &mut Vec<u8>, value: &Address) {
buf.push(PUSH20);
buf.extend_from_slice(value.as_slice());
}
fn push32(buf: &mut Vec<u8>, value: &[u8; 32]) {
buf.push(PUSH32);
buf.extend_from_slice(value);
}
fn emit_return_word(buf: &mut Vec<u8>, word: &[u8; 32]) {
push32(buf, word);
push1(buf, 0x00);
buf.push(MSTORE);
push1(buf, 0x20);
push1(buf, 0x00);
buf.push(RETURN);
}
const RETURN_WORD_EPILOGUE_LEN: usize = 2 + 33 + 1 + 2 + 2 + 1;
fn branch_code_len(logs: &[ScriptedLog]) -> usize {
let mut len = 2; for log in logs {
len += log.topics.len() * 33; len += 2 + 3 + 3 + 1; len += 2 + 3 + 1; }
len + RETURN_WORD_EPILOGUE_LEN
}
fn validate_log(log: &ScriptedLog, context: &str) -> Result<(), String> {
if log.topics.is_empty() || log.topics.len() > 4 {
return Err(format!(
"{context}: a scripted log needs 1..=4 topics, got {}",
log.topics.len()
));
}
if log.data.len() > 255 {
return Err(format!(
"{context}: scripted log data {} bytes exceeds the 255 PUSH1 bound",
log.data.len()
));
}
Ok(())
}
fn assemble_actor_runtime(
branches: &[ActorBranch],
fallback_word: &[u8; 32],
) -> Result<Vec<u8>, String> {
let mut seen_lengths = Vec::new();
for branch in branches {
if seen_lengths.contains(&branch.calldata_len) {
return Err(format!(
"scripted actor: duplicate calldata length {}",
branch.calldata_len
));
}
seen_lengths.push(branch.calldata_len);
for log in &branch.logs {
validate_log(log, "scripted actor branch")?;
}
}
let head_len = 1; let length_test_len = 1 + 2 + 1 + 3 + 1; let fallback_len = 1 + RETURN_WORD_EPILOGUE_LEN; let mut body_starts = Vec::with_capacity(branches.len());
let mut cursor = head_len + branches.len() * length_test_len + fallback_len;
for branch in branches {
body_starts.push(cursor);
cursor += branch_code_len(&branch.logs);
}
let blob_base = cursor;
let mut code = Vec::new();
code.push(CALLDATASIZE);
for (index, branch) in branches.iter().enumerate() {
code.push(DUP1);
push1(&mut code, branch.calldata_len);
code.push(EQ);
push2(
&mut code,
u16::try_from(body_starts[index])
.map_err(|_| "branch body offset exceeds the 16-bit layout")?,
);
code.push(JUMPI);
}
code.push(POP);
emit_return_word(&mut code, fallback_word);
for (index, branch) in branches.iter().enumerate() {
debug_assert_eq!(code.len(), body_starts[index]);
code.push(JUMPDEST);
code.push(POP);
for (log_index, log) in branch.logs.iter().enumerate() {
for topic in log.topics.iter().rev() {
push32(&mut code, topic.as_ref());
}
push1(
&mut code,
u8::try_from(log.data.len()).map_err(|_| "log data exceeds 255 bytes")?,
);
let blob_offset = blob_base
+ branches[..index]
.iter()
.map(|b| b.logs.len() * LOG_MEMORY_PAGE)
.sum::<usize>()
+ log_index * LOG_MEMORY_PAGE;
push2(
&mut code,
u16::try_from(blob_offset)
.map_err(|_| "log blob offset exceeds the 16-bit layout")?,
);
push2(
&mut code,
u16::try_from(log_index * LOG_MEMORY_PAGE)
.map_err(|_| "log memory page exceeds the 16-bit layout")?,
);
code.push(CODECOPY);
push1(
&mut code,
u8::try_from(log.data.len()).map_err(|_| "log data exceeds 255 bytes")?,
);
push2(
&mut code,
u16::try_from(log_index * LOG_MEMORY_PAGE)
.map_err(|_| "log memory page exceeds the 16-bit layout")?,
);
code.push(LOG0 + u8::try_from(log.topics.len()).map_err(|_| "topic count")?);
}
emit_return_word(&mut code, &branch.return_word);
}
debug_assert_eq!(code.len(), blob_base);
for branch in branches {
for log in &branch.logs {
let mut blob = log.data.clone();
blob.resize(LOG_MEMORY_PAGE, 0);
code.extend_from_slice(&blob);
}
}
Ok(code)
}
fn creation_code_for(runtime: &[u8]) -> Result<Vec<u8>, String> {
let runtime_len = u16::try_from(runtime.len())
.map_err(|_| "scripted runtime exceeds the 16-bit creation layout".to_string())?;
let header_len = u16::try_from(CREATION_HEADER_LEN)
.map_err(|_| "creation header width exceeds u16".to_string())?;
let mut code = Vec::with_capacity(CREATION_HEADER_LEN + runtime.len());
push2(&mut code, runtime_len);
push2(&mut code, header_len);
push1(&mut code, 0x00);
code.push(CODECOPY);
push2(&mut code, runtime_len);
push2(&mut code, 0);
code.push(RETURN);
debug_assert_eq!(code.len(), CREATION_HEADER_LEN);
code.extend_from_slice(runtime);
Ok(code)
}
pub fn scripted_actor_creation_code(
branches: &[ActorBranch],
fallback_word: [u8; 32],
) -> Result<Vec<u8>, String> {
let runtime = assemble_actor_runtime(branches, &fallback_word)?;
creation_code_for(&runtime)
}
pub fn scripted_coordinator_creation_code(
calls: &[(Address, u8)],
return_word: [u8; 32],
) -> Result<Vec<u8>, String> {
let mut runtime = Vec::new();
for (actor, calldata_len) in calls {
push1(&mut runtime, 0x20); push1(&mut runtime, 0x00); push1(&mut runtime, *calldata_len); push1(&mut runtime, 0x00); push1(&mut runtime, 0x00); push20(&mut runtime, actor);
push4(&mut runtime, SUBCALL_GAS);
runtime.push(CALL);
runtime.push(POP);
}
emit_return_word(&mut runtime, &return_word);
creation_code_for(&runtime)
}
#[cfg(test)]
#[expect(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
fn word(low_byte: u8) -> [u8; 32] {
let mut w = [0u8; 32];
w[31] = low_byte;
w
}
#[test]
fn actor_creation_code_layout_is_exact() {
let topic = B256::from([1u8; 32]);
let branch = ActorBranch::new(
1,
vec![ScriptedLog::new(vec![topic], vec![0xab; 96])],
word(0x11),
);
let code = scripted_actor_creation_code(&[branch], word(0x22)).unwrap();
assert_eq!(code[0], PUSH2);
assert_eq!(
u16::from_be_bytes([code[1], code[2]]),
u16::try_from(code.len() - CREATION_HEADER_LEN).unwrap()
);
assert_eq!(code[3], PUSH2);
assert_eq!(
u16::from_be_bytes([code[4], code[5]]),
u16::try_from(CREATION_HEADER_LEN).unwrap()
);
assert_eq!(&code[6..9], &[0x60, 0x00, CODECOPY]);
assert_eq!(code[9], PUSH2);
assert_eq!(code[12], PUSH2);
assert_eq!(&code[13..15], &[0x00, 0x00]);
assert_eq!(code[15], RETURN);
let rt = &code[CREATION_HEADER_LEN..];
assert_eq!(&rt[..1], &[CALLDATASIZE]);
assert_eq!(&rt[1..9], &[DUP1, PUSH1, 0x01, EQ, PUSH2, 0x00, 51, JUMPI]);
assert_eq!(rt[9], POP);
assert_eq!(rt[10], PUSH32);
assert_eq!(&rt[11..43], &word(0x22));
assert_eq!(&rt[43..45], &[0x60, 0x00]);
assert_eq!(rt[45], MSTORE);
assert_eq!(&rt[46..51], &[PUSH1, 0x20, PUSH1, 0x00, RETURN]);
let body = &rt[51..];
assert_eq!(body[0], JUMPDEST);
assert_eq!(body[1], POP);
assert_eq!(body[2], PUSH32);
assert_eq!(&body[3..35], topic.as_slice());
assert_eq!(
&body[35..44],
&[PUSH1, 96, PUSH2, 0x00, 142, PUSH2, 0x00, 0x00, CODECOPY]
);
assert_eq!(&body[44..49], &[PUSH1, 96, PUSH2, 0x00, 0x00]);
assert_eq!(body[49], LOG0 + 1);
assert_eq!(body[50], PUSH32);
assert_eq!(&body[51..83], &word(0x11));
assert_eq!(&body[83..85], &[0x60, 0x00]);
assert_eq!(body[85], MSTORE);
assert_eq!(&body[86..91], &[PUSH1, 0x20, PUSH1, 0x00, RETURN]);
assert_eq!(&rt[142..238], &[0xab; 96][..]);
assert_eq!(&rt[238..rt.len()], &[0u8; 160][..]);
}
#[test]
fn actor_two_branch_layout_keeps_blobs_disjoint() {
let topic = B256::from([1u8; 32]);
let log1 = vec![ScriptedLog::new(vec![topic], vec![0xab; 32])];
let log2 = vec![ScriptedLog::new(vec![topic], vec![0xcd; 32])];
let code = scripted_actor_creation_code(
&[
ActorBranch::new(1, log1.clone(), word(1)),
ActorBranch::new(2, log2.clone(), word(2)),
],
word(3),
)
.unwrap();
let rt = &code[CREATION_HEADER_LEN..];
let head_and_tests = 1 + 2 * 8;
let fallback = 42;
let body1 = head_and_tests + fallback;
let body2 = body1 + branch_code_len(&log1);
let blob_base = body2 + branch_code_len(&log2);
assert_eq!(
u16::from_be_bytes([rt[body1 + 38], rt[body1 + 39]]),
u16::try_from(blob_base).unwrap()
);
assert_eq!(
u16::from_be_bytes([rt[body2 + 38], rt[body2 + 39]]),
u16::try_from(blob_base + 256).unwrap()
);
assert_eq!(&rt[blob_base..blob_base + 32], &[0xab; 32][..]);
assert_eq!(&rt[blob_base + 256..blob_base + 288], &[0xcd; 32][..]);
}
#[test]
fn actor_rejects_out_of_grammar_shapes() {
let topic = B256::from([1u8; 32]);
let dup = scripted_actor_creation_code(
&[
ActorBranch::new(1, vec![ScriptedLog::new(vec![topic], vec![0x00])], word(1)),
ActorBranch::new(1, vec![ScriptedLog::new(vec![topic], vec![0x00])], word(2)),
],
word(3),
);
assert!(dup.is_err());
assert!(scripted_actor_creation_code(
&[ActorBranch::new(
1,
vec![ScriptedLog::new(vec![], vec![0x00])],
word(1)
)],
word(2)
)
.is_err());
assert!(scripted_actor_creation_code(
&[ActorBranch::new(
1,
vec![ScriptedLog::new(vec![topic; 5], vec![0x00])],
word(1)
)],
word(2)
)
.is_err());
assert!(scripted_actor_creation_code(
&[ActorBranch::new(
1,
vec![ScriptedLog::new(vec![topic], vec![0u8; 256])],
word(1)
)],
word(2)
)
.is_err());
}
#[test]
fn actor_executes_on_the_fixture_evm() {
use crate::oracle::{self, Output, TxSpec, Verdict};
use alloy::primitives::Bytes;
let mut evm = oracle::new_fixture_evm();
oracle::set_disable_nonce_check(&mut evm, true);
oracle::set_code_size_limits(&mut evm, Some(usize::MAX));
let topic = B256::from([1u8; 32]);
let creation = scripted_actor_creation_code(
&[
ActorBranch::new(
1,
vec![ScriptedLog::new(vec![topic], vec![0xab; 32])],
word(0x11),
),
ActorBranch::new(
2,
vec![ScriptedLog::new(vec![topic], Vec::new())],
word(0x12),
),
],
word(0x22),
)
.unwrap();
let Verdict::Accepted {
output: Output::Create(_, Some(actor)),
logs: deploy_logs,
} = oracle::transact(
&mut evm,
TxSpec::Deploy {
init_code: Bytes::from(creation),
gas: 16_700_000,
},
)
else {
panic!("actor deploy must succeed");
};
assert!(deploy_logs.is_empty(), "the deploy emits nothing");
let verdict = oracle::transact(
&mut evm,
TxSpec::Call {
to: actor,
data: Bytes::from(vec![0u8; 1]),
gas: 16_700_000,
},
);
let Verdict::Accepted { output, logs } = verdict else {
panic!("branch call must succeed, got {verdict:?}");
};
assert_eq!(logs.len(), 1, "the branch emits its log");
assert_eq!(logs[0].address, actor);
assert_eq!(logs[0].topics(), &[topic]);
assert_eq!(logs[0].data.data.as_ref(), &[0xab; 32]);
let Output::Call(ret) = output else {
panic!("branch call return");
};
assert_eq!(ret.as_ref(), &word(0x11));
let Verdict::Accepted { output, logs } = oracle::transact(
&mut evm,
TxSpec::Call {
to: actor,
data: Bytes::from(vec![0u8; 2]),
gas: 16_700_000,
},
) else {
panic!("empty-data branch call must succeed");
};
assert_eq!(logs.len(), 1, "the empty-data branch emits its log");
assert_eq!(logs[0].data.data.as_ref(), &[] as &[u8]);
let Output::Call(ret) = output else {
panic!("empty-data branch return");
};
assert_eq!(ret.as_ref(), &word(0x12));
let Verdict::Accepted { output, logs } = oracle::transact(
&mut evm,
TxSpec::Call {
to: actor,
data: Bytes::from([vec![0x6c, 0x53, 0x27, 0x2b], vec![0u8; 32]].concat()),
gas: 16_700_000,
},
) else {
panic!("fallback call must succeed");
};
assert!(logs.is_empty(), "the fallback emits nothing");
let Output::Call(ret) = output else {
panic!("fallback call return");
};
assert_eq!(ret.as_ref(), &word(0x22));
}
#[test]
fn coordinator_executes_its_sub_calls() {
use crate::oracle::{self, Output, TxSpec, Verdict};
use alloy::primitives::Bytes;
let mut evm = oracle::new_fixture_evm();
oracle::set_disable_nonce_check(&mut evm, true);
oracle::set_code_size_limits(&mut evm, Some(usize::MAX));
let topic_a = B256::from([2u8; 32]);
let topic_b = B256::from([3u8; 32]);
let first_creation = scripted_actor_creation_code(
&[ActorBranch::new(
1,
vec![ScriptedLog::new(vec![topic_a], vec![0xcd; 16])],
word(0x44),
)],
word(0x55),
)
.unwrap();
let second_creation = scripted_actor_creation_code(
&[ActorBranch::new(
1,
vec![ScriptedLog::new(vec![topic_b], vec![0xce; 16])],
word(0x45),
)],
word(0x56),
)
.unwrap();
let mut deploy = |creation: Vec<u8>| {
let Verdict::Accepted {
output: Output::Create(_, Some(address)),
..
} = oracle::transact(
&mut evm,
TxSpec::Deploy {
init_code: Bytes::from(creation),
gas: 16_700_000,
},
)
else {
panic!("actor deploy");
};
address
};
let actor_a = deploy(first_creation);
let actor_b = deploy(second_creation);
let coordinator_creation =
scripted_coordinator_creation_code(&[(actor_a, 1), (actor_b, 1)], word(0x66)).unwrap();
let coordinator = deploy(coordinator_creation);
let verdict = oracle::transact(
&mut evm,
TxSpec::Call {
to: coordinator,
data: Bytes::new(),
gas: 16_700_000,
},
);
let Verdict::Accepted { output, logs } = verdict else {
panic!("coordinator call must succeed, got {verdict:?}");
};
assert_eq!(logs.len(), 2, "both sub-calls' logs join the tx");
assert_eq!(logs[0].address, actor_a);
assert_eq!(logs[1].address, actor_b);
let Output::Call(ret) = output else {
panic!("coordinator return");
};
assert_eq!(ret.as_ref(), &word(0x66));
}
#[test]
fn actor_multi_topic_log_topics_serve_in_generator_order() {
use crate::oracle::{self, Output, TxSpec, Verdict};
use alloy::primitives::Bytes;
let mut evm = oracle::new_fixture_evm();
oracle::set_disable_nonce_check(&mut evm, true);
oracle::set_code_size_limits(&mut evm, Some(usize::MAX));
let topics: Vec<B256> = (1u8..=4).map(|i| B256::from([i; 32])).collect();
let creation = scripted_actor_creation_code(
&[
ActorBranch::new(
1,
vec![ScriptedLog::new(topics.clone(), vec![0xee; 64])],
word(0x11),
),
ActorBranch::new(
2,
vec![
ScriptedLog::new(vec![topics[0]], vec![0x11; 32]),
ScriptedLog::new(vec![topics[1]], vec![0x22; 32]),
],
word(0x12),
),
],
word(0x22),
)
.unwrap();
let Verdict::Accepted {
output: Output::Create(_, Some(actor)),
..
} = oracle::transact(
&mut evm,
TxSpec::Deploy {
init_code: Bytes::from(creation),
gas: 16_700_000,
},
)
else {
panic!("actor deploy must succeed");
};
let Verdict::Accepted { logs, .. } = oracle::transact(
&mut evm,
TxSpec::Call {
to: actor,
data: Bytes::from(vec![0u8; 1]),
gas: 16_700_000,
},
) else {
panic!("four-topic branch call must succeed");
};
assert_eq!(logs.len(), 1);
assert_eq!(logs[0].topics(), topics.as_slice(), "topic order");
assert_eq!(logs[0].data.data.as_ref(), &[0xee; 64]);
let Verdict::Accepted { logs, .. } = oracle::transact(
&mut evm,
TxSpec::Call {
to: actor,
data: Bytes::from(vec![0u8; 2]),
gas: 16_700_000,
},
) else {
panic!("two-log branch call must succeed");
};
assert_eq!(logs.len(), 2);
assert_eq!(logs[0].topics(), &[topics[0]]);
assert_eq!(logs[0].data.data.as_ref(), &[0x11; 32]);
assert_eq!(logs[1].topics(), &[topics[1]]);
assert_eq!(logs[1].data.data.as_ref(), &[0x22; 32]);
}
}