diff --git a/ACTIVATIONS.md b/ACTIVATIONS.md index 7f1a404873..ca7bf1493b 100644 --- a/ACTIVATIONS.md +++ b/ACTIVATIONS.md @@ -16,8 +16,8 @@ Statuses checked 2026-10-16 against Base `v1.5.0` (`crates/common/chains/src/con | Beryl | Active 2026-06-25 18:00 UTC | Active 2026-06-18 18:00 UTC | Already in the stream. | | Cobalt | Active 2026-09-30 18:00 UTC | Active 2026-09-23 18:00 UTC | B-20 V2 precompiles. Covered by `etc/systems/tests/firehose_cobalt_b20.rs`. | | Denim | Not scheduled | 2026-10-21 18:00 UTC (`1_792_605_600`) | See [Denim](#denim). | -| Everest | Not scheduled | Not scheduled | See [Everest](#everest). **The node stops on the first EIP-8130 transaction.** | -| Zenith | Never (genesis only) | Never (genesis only) | Gates the EIP-8130 keystore (account configuration changes, non-k1 authenticators). Unreachable on real networks. | +| Everest | Not scheduled | Not scheduled | See [Everest](#everest). Traced from `v1.5.0-fh3.1-2` as `TRX_TYPE_ACCOUNT_ABSTRACTION`. **Earlier versions stop on the first EIP-8130 transaction.** | +| Zenith | Never (genesis only) | Never (genesis only) | Gates the EIP-8130 keystore (account configuration changes, non-k1 authenticators). Unreachable on real networks. Traced from `v1.5.0-fh3.1-2`: create and config change entries in `account_changes`, non-k1 sender authentication in `sender_auth`. | ## Denim @@ -44,15 +44,25 @@ Statuses checked 2026-10-16 against Base `v1.5.0` (`crates/common/chains/src/con - Gates EIP-8130 transactions (type `0x79` = 121) in the EVM, the transaction pool, RPC and derivation. Before v1.5.0 this was Zenith, which no real network can activate. -- `reth-firehose` cannot map type 121: `firehose-tracer` 5.5.0 `TxType` and the block model have - no such type. The mapper panics on it with a message naming the transaction and its type - (`crates/firehose/src/mapper.rs` in `streamingfast/reth`). Halting is deliberate: dropping the - transaction would emit a block that differs from the chain, and failing block execution would - make the node report a valid block as invalid. -- Tracing 8130 properly needs a new transaction type in the block model and the tracer, the 8130 - fields (nonce key, payer, phases with value, account changes, phase statuses), and handling of - its execution shape: pre-call writes and `NonceIncremented` logs outside any call frame, one - depth-0 frame per phase, payer prepay and refund, and fee vault credits outside the EVM. +- From `v1.5.0-fh3.1-2` (`firehose-tracer` 5.7.0, `streamingfast/reth` `base-v2.5.2.8-fh3.1-2`), + type 121 is traced as `TRX_TYPE_ACCOUNT_ABSTRACTION`: + - `TransactionTrace.account_abstraction_trx_input` holds the nonce key, the payer, the validity + window, the metadata, the account changes, the sender's non-k1 authentication, and the signed + calls grouped in `call_phases`, each linked by `call_index` to the call that executed it (0 if + it never ran). `TransactionReceipt.account_abstraction_trx_output.committed_phases` counts the + phases that committed. + - The root call goes from the sender to itself and runs no code. It carries the writes the + executor makes outside the calls: the payer's gas purchase and refund, the fee vault and + beneficiary credits, the nonce or nonce manager writes, the account changes and their logs. + Each signed call that ran is a child of the root call. + - A transaction whose phase reverted is `FAILED`, but the phases before it keep their state: only + the reverted phase's calls are `state_reverted`, and the root call never is. + - Covered by `crates/execution/firehose-tests/tests/eip8130.rs`, which runs signed transactions + through the Base executor on an Everest genesis (and a Zenith one for the Keystore cases). No + Docker system test yet. +- Versions before `v1.5.0-fh3.1-2` cannot map type 121 and stop on the first EIP-8130 transaction, + with a message naming it. Run `v1.5.0-fh3.1-2` or later before Everest activates on a network we + serve. - The activation block writes a code stub to the nonce manager in `apply_pre_execution_changes` (`ensure_eip8130_system_accounts`). `BasePreExecutionWrites` lists it, so it is emitted as a block-level code change. diff --git a/CHANGELOG.sf.md b/CHANGELOG.sf.md index 28a7ecf1a4..ac3df6f698 100644 --- a/CHANGELOG.sf.md +++ b/CHANGELOG.sf.md @@ -1,4 +1,4 @@ -## Unreleased +## v1.5.0-fh3.1-2 ### Added @@ -10,7 +10,14 @@ it, while the phases that committed keep their state even though the transaction is `FAILED`. The payer's gas purchase and refund, the fee payouts, the nonce increment, the nonce manager writes and the delegation code are recorded on the root call. - Keystore account changes, which only Zenith enables, still stop the node. + The Keystore account changes Zenith enables (EIP-8130 create and config change entries) are + recorded in `account_changes`, and a sender that signs with a P-256, WebAuthn or delegate + authenticator has its authentication in `sender_auth` instead of `v`, `r` and `s`. + +### Changed + +* Uses `streamingfast/reth` `base-v2.5.2.8-fh3.1-2` and `firehose-tracer` 5.7.0, which add the + account abstraction tracing above. ## v1.5.0-fh3.1-1 diff --git a/Cargo.lock b/Cargo.lock index 56e376737b..941a52320f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4870,22 +4870,26 @@ dependencies = [ "alloy-genesis", "alloy-primitives", "alloy-signer 2.4.1", + "alloy-sol-types", "base-common-consensus", "base-execution-chainspec", "base-execution-eip8130", "base-execution-evm", "base-execution-firehose", "base-test-utils", + "base64 0.22.1", "eyre", "firehose-tracer", "firehose-tracer-test", "k256 0.13.4", + "p256 0.13.2", "reth-firehose", "reth-firehose-tests", "reth-primitives-traits", "reth-revm", "revm", "serde_json", + "sha2 0.10.9", ] [[package]] diff --git a/crates/execution/firehose-tests/Cargo.toml b/crates/execution/firehose-tests/Cargo.toml index cb38219552..cbb46220b8 100644 --- a/crates/execution/firehose-tests/Cargo.toml +++ b/crates/execution/firehose-tests/Cargo.toml @@ -49,6 +49,10 @@ path = "tests/eip8130.rs" [dev-dependencies] k256.workspace = true +sha2.workspace = true +base64.workspace = true alloy-signer.workspace = true +alloy-sol-types.workspace = true base-test-utils.workspace = true base-execution-eip8130.workspace = true +p256 = { workspace = true, features = ["ecdsa"] } diff --git a/crates/execution/firehose-tests/tests/eip8130.rs b/crates/execution/firehose-tests/tests/eip8130.rs index 681015379b..eb5ddb2071 100644 --- a/crates/execution/firehose-tests/tests/eip8130.rs +++ b/crates/execution/firehose-tests/tests/eip8130.rs @@ -3,24 +3,53 @@ use alloy_eips::eip2718::Encodable2718; use alloy_genesis::{Genesis, GenesisAccount}; -use alloy_primitives::{Address, Bytes, U256, address, bytes}; +use alloy_primitives::{Address, B256, Bytes, U256, address, aliases::U48, bytes, keccak256}; use alloy_signer::SignerSync; +use alloy_sol_types::{SolValue, sol}; use base_common_consensus::{ - AccountChange, BaseTransactionSigned, BaseTxEnvelope, Call, Delegation, Eip8130Constants, - Eip8130Signed, Predeploys, TxEip8130, + AccountChange, AccountChangeChannel, BaseTransactionSigned, BaseTxEnvelope, Call, ChangeType, + CreateEntry, Delegation, Eip8130Constants, Eip8130Contracts, Eip8130Signed, InitialActor, + Predeploys, SignedAccountChanges, SignedChange, TxEip8130, +}; +use base_execution_eip8130::{ + AccountChangeApplier, ConfigChangeAuthorizer, Eip8130GasSchedule, IntrinsicGas, + IntrinsicGasInput, }; -use base_execution_eip8130::{Eip8130GasSchedule, IntrinsicGas, IntrinsicGasInput}; use base_firehose_tests::{BlockInvariants, TraceContext, run_block}; use base_test_utils::{Account, build_test_genesis_everest}; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use firehose_tracer::pb::sf::ethereum::r#type::v2::{ - self as pbeth, TransactionTrace, balance_change::Reason, + self as pbeth, TransactionTrace, account_abstraction_account_change::Change, + account_abstraction_config_change::Channel, account_abstraction_keystore_change, + balance_change::Reason, +}; +use p256::ecdsa::{ + Signature as P256Signature, SigningKey as P256SigningKey, signature::hazmat::PrehashSigner, }; +use sha2::{Digest, Sha256}; /// Contract whose code is `PUSH1 0 PUSH1 0 REVERT`. const REVERTER: Address = address!("0x00000000000000000000000000000000000ad0ad"); const NONCE_MANAGER: Address = address!("0x813000000000000000000000000000000000aa01"); const BENEFICIARY: Address = address!("0x4200000000000000000000000000000000000011"); +sol! { + struct ActorConfigAbi { + address authenticator; + uint48 expiry; + uint16 scope; + } + + struct WebAuthnAuth { + bytes32 r; + bytes32 s; + uint256 challengeIndex; + uint256 typeIndex; + bytes authenticatorData; + string clientDataJSON; + } +} + fn genesis() -> Genesis { let mut genesis = build_test_genesis_everest(); genesis.alloc.insert( @@ -83,9 +112,59 @@ fn sign(tx: TxEip8130, payer: Option) -> BaseTransactionSigned { Eip8130Signed::new(tx, sender_auth, payer_auth).into() } +/// An Everest genesis that also activates Zenith, the Keystore, with `funded` holding 1 ether. +fn keystore_genesis(funded: &[Address]) -> Genesis { + let mut genesis = genesis(); + let base = genesis.config.extra_fields.get_mut("base").expect("base upgrades"); + base["zenith"] = 0.into(); + for address in funded { + genesis.alloc.insert( + *address, + GenesisAccount { balance: U256::from(10).pow(U256::from(18)), ..Default::default() }, + ); + } + genesis +} + +/// The k1 actor id of `address`: the address right-aligned in a word. +fn k1_actor_id(address: Address) -> B256 { + address.into_word() +} + +/// `abi.encode(actorId, ActorConfig, policyData)` authorizing `actor_id` as an unrestricted admin +/// with no expiry and no policy. +fn authorize_payload(actor_id: B256, authenticator: Address) -> Vec { + (actor_id, ActorConfigAbi { authenticator, expiry: U48::ZERO, scope: 0 }, Bytes::new()) + .abi_encode_params() +} + +/// A batch of `changes` for Alice's account at sequence 0 of `channel`, signed with her key. +fn alice_batch( + genesis: &Genesis, + channel: AccountChangeChannel, + changes: Vec, +) -> SignedAccountChanges { + let alice = Account::Alice; + let mut batch = SignedAccountChanges { channel, sequence: 0, changes, signature: Bytes::new() }; + let digest = + ConfigChangeAuthorizer::changes_digest(alice.address(), genesis.config.chain_id, &batch); + let signature = alice.signer().sign_hash_sync(&digest).expect("sign"); + batch.signature = auth_blob(Eip8130Constants::K1_AUTHENTICATOR, &signature.as_bytes()); + batch +} + +/// `authenticator || data`. +fn auth_blob(authenticator: Address, data: &[u8]) -> Bytes { + [authenticator.as_slice(), data].concat().into() +} + fn run(tx: BaseTransactionSigned) -> TransactionTrace { let genesis = genesis(); - run_in(&genesis, &context(&genesis), tx) + run_on(&genesis, tx) +} + +fn run_on(genesis: &Genesis, tx: BaseTransactionSigned) -> TransactionTrace { + run_in(genesis, &context(genesis), tx) } fn run_in( @@ -221,6 +300,117 @@ fn delegation_is_a_code_change_on_the_root_call() { assert_eq!([&[0xef, 0x01, 0x00][..], target.as_slice()].concat(), code_change.new_code); } +#[test] +fn keystore_create_records_the_entry_and_the_new_account_code() { + let alice = Account::Alice; + let initial_actors = + vec![InitialActor::owner(k1_actor_id(alice.address()), Eip8130Constants::K1_AUTHENTICATOR)]; + let entry = CreateEntry { + user_salt: B256::repeat_byte(0x5a), + code: bytes!("6000"), + initial_actors: initial_actors.clone(), + }; + let account = + AccountChangeApplier::compute_address(entry.user_salt, &entry.code, &initial_actors) + .expect("address derivation"); + let genesis = keystore_genesis(&[account]); + + let mut tx = unsigned(genesis.config.chain_id, vec![vec![call(Account::Bob.address(), 1)]]); + tx.sender = Some(account); + tx.account_changes = vec![AccountChange::Create(entry.clone())]; + let signature = alice.signer().sign_hash_sync(&tx.sender_signature_hash()).expect("sign"); + let sender_auth = auth_blob(Eip8130Constants::K1_AUTHENTICATOR, &signature.as_bytes()); + let trx = run_on(&genesis, Eip8130Signed::new(tx, sender_auth, Bytes::new()).into()); + + assert_eq!(pbeth::TransactionTraceStatus::Succeeded as i32, trx.status); + assert_eq!(account.as_slice(), trx.from.as_slice()); + let input = trx.account_abstraction_trx_input.as_ref().expect("account_abstraction_trx_input"); + assert!(input.sender_auth.is_empty(), "a k1 signature is in v, r and s"); + assert_eq!(signature.r().to_be_bytes::<32>().to_vec(), trx.r); + + let Some(Change::Create(create)) = &input.account_changes[0].change else { + panic!("expected a create entry, got {:?}", input.account_changes); + }; + assert_eq!(entry.user_salt.to_vec(), create.user_salt); + assert_eq!(entry.code.to_vec(), create.code); + assert_eq!(1, create.initial_actors.len()); + assert_eq!(k1_actor_id(alice.address()).to_vec(), create.initial_actors[0].actor_id); + assert_eq!(Eip8130Constants::K1_AUTHENTICATOR.to_vec(), create.initial_actors[0].authenticator); + + let root = &trx.calls[0]; + let code_change = root + .code_changes + .iter() + .find(|change| change.address == account.to_vec()) + .expect("the created account's code is recorded on the root call"); + assert_eq!(entry.code.to_vec(), code_change.new_code); + assert!( + root.storage_changes + .iter() + .any(|change| change.address == Eip8130Contracts::ACCOUNT_CONFIG.to_vec()), + "the initial actors are Keystore storage writes" + ); + assert!( + root.logs.iter().any(|log| log.address == Eip8130Contracts::ACCOUNT_CONFIG.to_vec()), + "the Keystore logs are on the root call" + ); +} + +#[test] +fn keystore_config_change_authorizes_the_p256_actor_that_signs() { + let genesis = keystore_genesis(&[]); + let alice = Account::Alice; + let p256_key = P256SigningKey::from_slice(&[0x77; 32]).expect("p256 key"); + let point = p256_key.verifying_key().to_encoded_point(false); + let p256_actor_id = keccak256(&point.as_bytes()[1..]); + + let payload = authorize_payload(p256_actor_id, Eip8130Contracts::P256_AUTHENTICATOR); + let batch = alice_batch( + &genesis, + AccountChangeChannel::Multichain, + vec![SignedChange { + change_type: ChangeType::AuthorizeActor, + payload: payload.clone().into(), + }], + ); + + let mut tx = unsigned(genesis.config.chain_id, vec![vec![call(Account::Bob.address(), 1)]]); + tx.sender = Some(alice.address()); + tx.account_changes = vec![AccountChange::ConfigChange(batch.clone())]; + let hash = tx.sender_signature_hash(); + let signature: P256Signature = p256_key.sign_prehash(hash.as_slice()).expect("sign"); + let signature = signature.normalize_s().unwrap_or(signature); + let data = [&signature.to_bytes()[..], &point.as_bytes()[1..], &[0]].concat(); + let sender_auth = auth_blob(Eip8130Contracts::P256_AUTHENTICATOR, &data); + let trx = run_on(&genesis, Eip8130Signed::new(tx, sender_auth.clone(), Bytes::new()).into()); + + assert_eq!(pbeth::TransactionTraceStatus::Succeeded as i32, trx.status); + let input = trx.account_abstraction_trx_input.as_ref().expect("account_abstraction_trx_input"); + assert_eq!(sender_auth.to_vec(), input.sender_auth); + assert!(trx.v.is_empty() && trx.r.is_empty() && trx.s.is_empty(), "no k1 signature"); + + let Some(Change::ConfigChange(change)) = &input.account_changes[0].change else { + panic!("expected a config change entry, got {:?}", input.account_changes); + }; + assert_eq!(Channel::Multichain as i32, change.channel); + assert_eq!(0, change.sequence); + assert_eq!(batch.signature.to_vec(), change.auth); + assert_eq!(1, change.changes.len()); + assert_eq!( + account_abstraction_keystore_change::Type::AuthorizeActor as i32, + change.changes[0].r#type + ); + assert_eq!(payload, change.changes[0].payload); + + assert!( + trx.calls[0] + .logs + .iter() + .any(|log| log.address == Eip8130Contracts::ACCOUNT_CONFIG.to_vec()), + "the ActorAuthorized log is on the root call" + ); +} + #[test] fn open_payer_signature_that_does_not_recover_rejects_the_block() { let genesis = genesis(); @@ -358,3 +548,109 @@ fn call_refused_for_lack_of_new_account_gas_is_left_unlinked() { fn big(value: &Option) -> U256 { value.as_ref().map_or(U256::ZERO, |value| U256::from_be_slice(&value.bytes)) } + +#[test] +fn keystore_local_batch_authorizes_then_revokes_an_actor() { + let genesis = keystore_genesis(&[]); + let alice = Account::Alice; + let actor_id = B256::repeat_byte(0x5c); + let revoke_payload = actor_id.abi_encode(); + let batch = alice_batch( + &genesis, + AccountChangeChannel::Local, + vec![ + SignedChange { + change_type: ChangeType::AuthorizeActor, + payload: authorize_payload(actor_id, Eip8130Contracts::P256_AUTHENTICATOR).into(), + }, + SignedChange { + change_type: ChangeType::RevokeActor, + payload: revoke_payload.clone().into(), + }, + ], + ); + + let mut tx = unsigned(genesis.config.chain_id, vec![vec![call(Account::Bob.address(), 1)]]); + tx.sender = Some(alice.address()); + tx.account_changes = vec![AccountChange::ConfigChange(batch)]; + let signature = alice.signer().sign_hash_sync(&tx.sender_signature_hash()).expect("sign"); + let sender_auth = auth_blob(Eip8130Constants::K1_AUTHENTICATOR, &signature.as_bytes()); + let trx = run_on(&genesis, Eip8130Signed::new(tx, sender_auth, Bytes::new()).into()); + + assert_eq!(pbeth::TransactionTraceStatus::Succeeded as i32, trx.status); + let input = trx.account_abstraction_trx_input.as_ref().expect("account_abstraction_trx_input"); + let Some(Change::ConfigChange(change)) = &input.account_changes[0].change else { + panic!("expected a config change entry, got {:?}", input.account_changes); + }; + assert_eq!(Channel::Local as i32, change.channel); + let types: Vec = change.changes.iter().map(|change| change.r#type).collect(); + assert_eq!( + vec![ + account_abstraction_keystore_change::Type::AuthorizeActor as i32, + account_abstraction_keystore_change::Type::RevokeActor as i32, + ], + types + ); + assert_eq!(revoke_payload, change.changes[1].payload); + + let keystore_logs = trx.calls[0] + .logs + .iter() + .filter(|log| log.address == Eip8130Contracts::ACCOUNT_CONFIG.to_vec()) + .count(); + assert_eq!(2, keystore_logs, "the ActorAuthorized and ActorRevoked logs are on the root call"); +} + +#[test] +fn keystore_webauthn_sender_auth_is_recorded_as_signed() { + let genesis = keystore_genesis(&[]); + let alice = Account::Alice; + let key = P256SigningKey::from_slice(&[0x78; 32]).expect("p256 key"); + let point = key.verifying_key().to_encoded_point(false); + let (x, y) = + (B256::from_slice(&point.as_bytes()[1..33]), B256::from_slice(&point.as_bytes()[33..])); + let actor_id = keccak256([x.as_slice(), y.as_slice()].concat()); + let batch = alice_batch( + &genesis, + AccountChangeChannel::Multichain, + vec![SignedChange { + change_type: ChangeType::AuthorizeActor, + payload: authorize_payload(actor_id, Eip8130Contracts::WEBAUTHN_AUTHENTICATOR).into(), + }], + ); + + let mut tx = unsigned(genesis.config.chain_id, vec![vec![call(Account::Bob.address(), 1)]]); + tx.sender = Some(alice.address()); + tx.account_changes = vec![AccountChange::ConfigChange(batch)]; + + // An OpenZeppelin-layout WebAuthn assertion over the sender signature hash. + let challenge = URL_SAFE_NO_PAD.encode(tx.sender_signature_hash().as_slice()); + let client_json = format!( + "{{\"type\":\"webauthn.get\",\"challenge\":\"{challenge}\",\"origin\":\"https://base.org\"}}" + ); + let mut authenticator_data = vec![0xaa; 32]; + authenticator_data.push(0x01); // user present + authenticator_data.extend_from_slice(&[0, 0, 0, 1]); + let signed_digest = Sha256::new() + .chain_update(&authenticator_data) + .chain_update(Sha256::digest(client_json.as_bytes())) + .finalize(); + let signature: P256Signature = key.sign_prehash(&signed_digest).expect("sign"); + let signature = signature.normalize_s().unwrap_or(signature); + let assertion = WebAuthnAuth { + r: B256::from_slice(&signature.to_bytes()[..32]), + s: B256::from_slice(&signature.to_bytes()[32..]), + challengeIndex: U256::from(client_json.find("\"challenge\":\"").expect("challenge")), + typeIndex: U256::from(client_json.find("\"type\":\"webauthn.get\"").expect("type")), + authenticatorData: authenticator_data.into(), + clientDataJSON: client_json, + }; + let data = (assertion, x, y).abi_encode_params(); + let sender_auth = auth_blob(Eip8130Contracts::WEBAUTHN_AUTHENTICATOR, &data); + let trx = run_on(&genesis, Eip8130Signed::new(tx, sender_auth.clone(), Bytes::new()).into()); + + assert_eq!(pbeth::TransactionTraceStatus::Succeeded as i32, trx.status); + let input = trx.account_abstraction_trx_input.as_ref().expect("account_abstraction_trx_input"); + assert_eq!(sender_auth.to_vec(), input.sender_auth); + assert!(trx.v.is_empty() && trx.r.is_empty() && trx.s.is_empty(), "no k1 signature"); +} diff --git a/crates/execution/firehose/src/extras.rs b/crates/execution/firehose/src/extras.rs index d3659e73ec..21850234fb 100644 --- a/crates/execution/firehose/src/extras.rs +++ b/crates/execution/firehose/src/extras.rs @@ -18,7 +18,9 @@ use alloy_evm::Evm as _; use alloy_primitives::{Address, B256, Bytes, U256}; -use base_common_consensus::{AccountChange, Predeploys}; +use base_common_consensus::{ + AccountChange, AccountChangeChannel, ChangeType, Eip8130Constants, Predeploys, +}; use base_common_evm::{ BaseContext, BaseEvm, BaseSpecId, BaseTransaction, BaseTxTr, BaseUpgrade, DEPOSIT_TRANSACTION_TYPE, Eip8130PhaseStatuses, @@ -26,9 +28,12 @@ use base_common_evm::{ use firehose_tracer::{ firehose_debug, pb::sf::ethereum::r#type::v2::{ - AccountAbstractionAccountChange, AccountAbstractionCallInput, AccountAbstractionCallPhase, + AccountAbstractionAccountChange, AccountAbstractionAccountCreate, + AccountAbstractionCallInput, AccountAbstractionCallPhase, AccountAbstractionConfigChange, + AccountAbstractionInitialActor, AccountAbstractionKeystoreChange, AccountAbstractionTrxInput, AccountAbstractionTrxOutput, - account_abstraction_account_change, balance_change::Reason, + account_abstraction_account_change, account_abstraction_config_change, + account_abstraction_keystore_change, balance_change::Reason, }, types::{TxEvent, TxType}, utils::u256_to_protobuf, @@ -262,27 +267,7 @@ impl OpPreTxAdjust { return; }; - let account_changes = tx - .account_changes - .iter() - .map(|change| match change { - AccountChange::Delegation(delegation) => AccountAbstractionAccountChange { - change: Some(account_abstraction_account_change::Change::DelegationTarget( - if delegation.target.is_zero() { - Vec::new() - } else { - delegation.target.to_vec() - }, - )), - }, - // Keystore entries are gated by Zenith, which only genesis can activate. - AccountChange::Create(_) | AccountChange::ConfigChange(_) => panic!( - "Firehose cannot trace transaction {}: the block model has no Keystore \ - account change, so this node cannot emit this block", - tx_event.hash - ), - }) - .collect(); + let account_changes = tx.account_changes.iter().map(Self::account_change).collect(); let call_phases = tx .calls @@ -301,12 +286,16 @@ impl OpPreTxAdjust { .collect(); tx_event.to = None; - // `sender_auth` ends with the sender's `r || s || v` secp256k1 signature, after the - // 20-byte authenticator selector when the sender is named. + // `sender_auth` is the sender's `r || s || v` secp256k1 signature when the sender is + // recovered, and `authenticator || data` when it is named. let sender_auth = signed.sender_auth(); - if let Some(signature) = sender_auth.get(sender_auth.len().saturating_sub(65)..) - && signature.len() == 65 - { + let k1_signature = if tx.sender.is_none() { + Some(sender_auth.as_ref()) + } else { + sender_auth.strip_prefix(Eip8130Constants::K1_AUTHENTICATOR.as_slice()) + } + .filter(|signature| signature.len() == 65); + if let Some(signature) = k1_signature { tx_event.r = B256::from_slice(&signature[..32]); tx_event.s = B256::from_slice(&signature[32..64]); // Typed transactions record the y-parity, trimmed like a big integer. @@ -325,7 +314,7 @@ impl OpPreTxAdjust { metadata: tx.metadata.to_vec(), account_changes, call_phases, - sender_auth: Vec::new(), + sender_auth: if k1_signature.is_some() { Vec::new() } else { sender_auth.to_vec() }, }); let (_, inspector, _) = evm.components_mut(); @@ -342,4 +331,75 @@ impl OpPreTxAdjust { ], }); } + + /// Maps an EIP-8130 account change entry to the block model. + pub fn account_change(change: &AccountChange) -> AccountAbstractionAccountChange { + let change = match change { + AccountChange::Delegation(delegation) => { + account_abstraction_account_change::Change::DelegationTarget( + if delegation.target.is_zero() { + Vec::new() + } else { + delegation.target.to_vec() + }, + ) + } + AccountChange::Create(entry) => account_abstraction_account_change::Change::Create( + AccountAbstractionAccountCreate { + user_salt: entry.user_salt.to_vec(), + code: entry.code.to_vec(), + initial_actors: entry + .initial_actors + .iter() + .map(|actor| AccountAbstractionInitialActor { + actor_id: actor.actor_id.to_vec(), + authenticator: actor.authenticator.to_vec(), + scope: actor.scope.into(), + policy_data: actor.policy_data.to_vec(), + }) + .collect(), + }, + ), + AccountChange::ConfigChange(batch) => { + account_abstraction_account_change::Change::ConfigChange( + AccountAbstractionConfigChange { + channel: match batch.channel { + AccountChangeChannel::Local => { + account_abstraction_config_change::Channel::Local + } + AccountChangeChannel::Multichain => { + account_abstraction_config_change::Channel::Multichain + } + } as i32, + sequence: batch.sequence, + changes: batch + .changes + .iter() + .map(|change| AccountAbstractionKeystoreChange { + r#type: Self::keystore_change_type(change.change_type) as i32, + payload: change.payload.to_vec(), + }) + .collect(), + auth: batch.signature.to_vec(), + }, + ) + } + }; + AccountAbstractionAccountChange { change: Some(change) } + } + + /// Maps a Keystore change type to the block model. + pub const fn keystore_change_type( + change_type: ChangeType, + ) -> account_abstraction_keystore_change::Type { + match change_type { + ChangeType::AuthorizeActor => account_abstraction_keystore_change::Type::AuthorizeActor, + ChangeType::RevokeActor => account_abstraction_keystore_change::Type::RevokeActor, + ChangeType::IncrementLocalEpoch => { + account_abstraction_keystore_change::Type::IncrementLocalEpoch + } + ChangeType::Lock => account_abstraction_keystore_change::Type::Lock, + ChangeType::Unlock => account_abstraction_keystore_change::Type::Unlock, + } + } }