diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..12a411470a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,631 @@ +# EIP-7702 Implementation Notebook (Lotus + Builtin Actors) + +IMPORTANT — Current Work Priority + +- The authoritative plan for the ongoing migration (EthAccount state + ref-fvm delegation) is tracked in `documentation/eip7702_ethaccount_ref-fvm_migration.md`. +- This is the active focus for the branch. Use that document for scope, sequencing, and acceptance criteria. The content below remains as background and historical context. + +This notebook tracks the end‑to‑end EIP‑7702 implementation across Lotus (Go) and builtin‑actors (Rust), including current status, remaining tasks, and validation steps. + +**Purpose** +- Provide a concise, actionable plan to complete EIP‑7702. +- Document current status, remaining work, and how to validate. + +**Progress Sync (current)** +- Event encoding: receipts now expect topic keccak("Delegated(address)") and a 32‑byte ABI word for the address. Lotus adjuster extracts the last 20 bytes and tolerates both 20/32‑byte encodings during the transition. +- Routing: type‑0x04 now targets EthAccount.ApplyAndCall (FRC‑42 method hash) with the canonical CBOR wrapper. EVM.ApplyAndCall routing is deprecated on this branch. +- Runtime helper: ref‑fvm exposes `get_eth_delegate_to(ActorID) -> Option<[u8;20]>`; EVM EXTCODE* now consults this helper to project the 23‑byte pointer image. +- VM intercept: ref‑fvm now intercepts CALL→EOA to EthAccount when `delegate_to` is set; it executes the delegate under authority context (depth=1), mounts/persists the authority `evm_storage_root`, and emits `Delegated(address)` (32‑byte ABI) best‑effort. +- Interpreter minimalization: EVM CALL/STATICCALL to EOAs no longer follows delegation internally; delegation is handled by the VM intercept. Legacy EVM ApplyAndCall/InvokeAsEoa have been removed on this branch; only the `InvokeAsEoaWithRoot` trampoline remains for VM intercept use. +- EthAccount: state struct added (`delegate_to`, `auth_nonce`, `evm_storage_root`) and ApplyAndCall implemented with full tuple validation and receiver‑only persistence; outer call forwards gas and returns embedded status/returndata. Initial unit tests added (invalid yParity/lengths, nonce init/increment, atomicity on revert, value transfer short‑circuit). +- Tests: Lotus 7702 receipts updated and green; ref‑fvm helper unit test added; EVM EXTCODE* tests continue to pass with the Runtime helper. + +Docker bundle/test flow +- Use `../ref-fvm/scripts/run_eip7702_tests.sh` to build the builtin-actors bundle in Docker and run ref‑fvm tests end‑to‑end. This avoids macOS toolchain issues (e.g., env::__stack_chk_fail, blst section too large). +- Fallback (local, non‑Docker): in `../builtin-actors`, try `RUSTFLAGS="-C link-arg=-Wl,-dead_strip" make bundle-testing` or `SKIP_BUNDLE=1 cargo build -p fil_builtin_actors_bundle` for compile‑time paths only (note: tests that execute EVM still need a real bundle). + +Next up +- Expand/enable ref‑fvm unit tests for intercept semantics (delegated mapping success/revert + revert bytes, depth limit, EXTCODECOPY windowing/zero‑fill, SELFDESTRUCT no‑op); finalize cleanup of any remaining legacy stubs. + +--- + +Coverage Improvement Plan (ref‑fvm) + +Context +- Codecov patch coverage flagged low coverage across recent changes in ref‑fvm, especially in: + - `fvm/src/call_manager/default.rs` + - `sdk/src/actor.rs` + - `fvm/src/kernel/default.rs` + - `fvm/src/syscalls/actor.rs` +- Some CI coverage steps ran with `--no-default-features`, skipping delegated CALL intercept paths and undercounting coverage. + +Goals +- Patch coverage ≥ 80% on changed files. +- Project coverage back near pre‑change baseline; fix upload parity vs. base. + +Actions (sequenced) +1) SDK helper + tests + - Extract the 20‑byte address slicing into a pure helper in `sdk/src/actor.rs` and add unit tests covering lengths {0, <20, 20, >20} to avoid relying on syscalls. +2) Kernel/call‑manager paths + - Ensure existing delegated tests cover success, revert, value‑transfer short‑circuit, depth limit, and EXTCODE* pointer image with windowing. These already exist under `fvm/tests/*`; confirm they run under the coverage profile. +3) CI coverage job + - For the `test-fvm` coverage step, run with default features (remove `--no-default-features`) so delegated paths are exercised and reported. Keep other steps as‑is to avoid feature unification side‑effects. +4) Syscall wrapper spots + - Opportunistic coverage via existing flows (resolve_address, get_actor_code_cid). Add minimal negative‑path assertions if needed. +5) Upload parity + - Ensure the same number of coverage uploads as base (Linux primary). If base had macOS uploads, mirror them or disable redundant ones consistently. + +Execution Notes +- Keep minimal‑build fallbacks in tests guarded so coverage runs (default features) take the asserting paths. +- Commit in small steps and keep this AGENTS.md in sync after each significant change. + +Status (to hand off) +- Initial test adjustments for minimal builds landed; CI green. +- SDK: added pure extractor + unit tests for `eth20` slicing in `sdk/src/actor.rs`. +- CI: coverage step for `fvm` now runs with default features to exercise delegated paths. +- Update: to avoid OpenCL linkage on Ubuntu runners, restored `--no-default-features` on the `test-fvm` coverage step. Delegation tests remain compatible with minimal builds. If patch coverage stays low for kernel/call-manager, add targeted unit tests that don't require default features. +- Next: validate Codecov patch % (target ≥80% on changed files) and project coverage; add any missing edge tests if needed. + +Updates (coverage work in progress) +- Added fvm crate tests for send paths that hit DefaultCallManager branches: + - Create placeholder actor (f4) via METHOD_SEND + value=0, then transfer non-zero. + - Create BLS account actor via METHOD_SEND + value=0, then transfer non-zero. +- Added fvm unit tests for keccak32/frc42 helpers inside call_manager/default.rs. +- Expect improved patch coverage in call_manager; continue adding focused tests if needed. + +**Paired Repos** +- `./lotus` (this folder) +- `../builtin-actors` (paired repo in a neighboring folder) + +**Branch Scope & Compatibility** +- This is an internal development branch with no external users or on‑chain compatibility requirements. +- We do not preserve backward compatibility in this branch. It is acceptable to simplify encodings and remove legacy paths. +- Routing and semantics have moved to EthAccount + VM intercept. Type‑0x04 transactions target `EthAccount.ApplyAndCall` (FRC‑42 method hash with canonical CBOR params). Delegated CALL/EXTCODE* behavior is implemented in ref‑fvm; the EVM interpreter no longer follows delegation internally. +- There is no Delegator actor and no single‑EVM‑actor delegation map. Delegation state lives per‑EOA in EthAccount state (`delegate_to`, `auth_nonce`, `evm_storage_root`). Global pointer semantics are provided by the VM intercept reading EthAccount state. +- Encodings are canonicalized: wrapper/atomic CBOR only; legacy shapes removed from tests/helpers. + +**EthAccount Delegation State (current)** +- Ownership and persistence: EthAccount persists three fields in its state: `delegate_to: Option`, `auth_nonce: u64`, and `evm_storage_root: Cid`. These survive across transactions until updated or cleared (zero delegate clears). +- Apply‑and‑Call flow: `EthAccount.ApplyAndCall` validates tuples (domain 0x05, chainId in {0, local}, yParity ∈ {0,1}, non‑zero/≤32‑byte r/s with left‑padding, low‑s), recovers the `authority`, enforces nonce equality, updates `delegate_to`/`auth_nonce` (receiver‑only), initializes `evm_storage_root` if needed, then invokes the outer call via VM with all gas forwarded. It returns embedded status/returndata. Mapping/nonces persist even if the outer call reverts. +- Pointer semantics: When a CALL/EXTCODE* targets an EOA with `delegate_to` set, ref‑fvm intercepts and executes the delegate under authority context (depth=1). EXTCODESIZE/HASH/COPY expose a 23‑byte virtual code image (`0xEF 0x01 0x00 || delegate(20)`). +- Authority context and safety: In authority context, delegation is not re‑followed (depth limit = 1), SELFDESTRUCT is a no‑op (no tombstone or balance move), and storage is mounted using the authority’s persistent storage root, then persisted on success. + +**ApplyAndCall Outer Call Gas** +- Top‑level behavior: For the outer call executed by `EthAccount.ApplyAndCall`, ref‑fvm forwards all available gas (no 63/64 cap), mirroring Ethereum’s top‑level semantics. +- Subcalls: Inside the interpreter, subcalls (CALL/STATICCALL/DELEGATECALL) still enforce the EIP‑150 63/64 gas clamp. +- Consequence: Emitting the `Delegated(address)` event from the VM intercept is best‑effort. Under extreme gas tightness, the event may be dropped. +- Rationale: Prioritize correctness of the callee’s execution budget for the outer call. If telemetry shows attribution logs are frequently dropped, consider reserving a small fixed gas budget for event emission (not a 63/64 clamp). + +**Testing TODO (Highest Priority)** +- Cross‑repo scope: tests span `./lotus` and `../builtin-actors`. Keep encoding and gas behavior aligned. +- Parser/encoding (Lotus): + - Add tuple‑arity and yParity rejection cases for 0x04 RLP decode in `chain/types/ethtypes`. + - Canonicalize to atomic CBOR only: `[ [ tuple... ], [ to(20), value, input ] ]`; remove legacy shapes. + - Add `AuthorizationKeccak` (0x05 domain) vectors for stability across edge cases. +- Receipts attribution (Lotus): + - Unit tests for delegated attribution from both `authorizationList` and synthetic `Delegated(address)` event. +- Mempool (Lotus): + - No 7702-specific ingress policies. Ensure standard mempool behavior remains stable. +- Gas estimation (Lotus): + - Gas model note (FVM): Lotus runs EVM as a Wasm actor on top of Filecoin’s gas system. We do not mirror Ethereum’s intrinsic gas accounting or constants in this branch. Estimation is behavioral and implementation‑defined. + - Unit tests must avoid pinning numeric gas constants or absolute gas usage. Focus on tuple counting and gating in `node/impl/eth/gas_7702_scaffold.go`. + - Prefer behavioral checks only: overhead applied when the authorization list is non‑empty; overhead grows monotonically with tuple count; no overhead when the feature is disabled or when the target is not EVM `ApplyAndCall`. +- E2E (Lotus): + - Mirror geth’s `TestEIP7702` flow (apply two delegations, CALL→EOA executes delegate, storage updated) once the EVM actor ships ApplyAndCall. +- Actor validations (builtin‑actors/EVM): + - Ensure chainId ∈ {0, local}, yParity ∈ {0,1}, non‑zero r/s, low‑s, ecrecover authority, nonce tracking; refunds and gas constants tests; enforce tuple cap. +- Additional review‑driven tests (builtin‑actors/EVM): + - Event: topic hash for `Delegated(address)` and value = authority address (EOA), not delegate. + - CALL revert path: propagate revert data via `state.return_data` and memory copy. + - ApplyAndCall: fail outer call on failed value transfer to delegated EOA. + - Negative validations: invalid `chainId`, invalid `yParity`, zero/over‑32 R/S, high‑S, nonce mismatch, duplicates, tuple cap >64. (DONE) + - Pre‑existence policy: reject when authority resolves to EVM contract. (DONE) + - R/S padding interop: accept 1..31‑byte values (left‑padded), reject >32‑byte values. (DONE) + +**Audit Remediation Plan (Gemini)** + +- Atomic 0x04 semantics + - Implement atomic “apply authorizations + execute outer call” for type‑0x04 within a single transaction. + - Builtin‑actors: `EthAccount.ApplyAndCall` validates tuples, enforces nonces, persists `delegate_to`/`auth_nonce` (receiver‑only), and invokes the outer call via VM atomically. + - Lotus: route 0x04 to EthAccount `ApplyAndCall` exclusively; no env toggles; gas estimation simulates tuple overhead behaviorally. + - Tests: revert path returns embedded status/revert data while delegation/nonce updates persist; mirror geth’s `TestEIP7702`. + +- RLP per‑type decode limit + - Replace global `maxListElements = 13` with a per‑call limit; apply 13 only for 0x04 parsing. + - Add unit tests proving no regression for legacy/1559 decoders. + +- Canonical CBOR (consensus safety) + - Actor: accept wrapper `[ [ tuple, ... ] ]` only; remove dual‑shape detection in Go helper. + - Negative tests: malformed top‑level shapes, wrong tuple arity, invalid yParity. + +- Runtime hardening (EVM + VM intercept) + - Delegated CALL pointer semantics and event emission are implemented in ref‑fvm; EXTCODESIZE/HASH/COPY expose a virtual 23‑byte pointer code for delegated EOAs. + - Interpreter guards for authority context; depth limit on delegation; storage safety. + - Authorization domain/signatures; tuple cap; refunds. + +- Lotus follow‑ups + - Receipts/RPC: ensure receipts/logs reflect atomic apply+call; maintain `delegatedTo` attribution (tuples or synthetic event). RPC reconstruction supports EthAccount `ApplyAndCall` route. +- Gas estimation: model atomic flow; behavioral tests only until constants finalize. Overhead applied for EthAccount route. + +Gas Model (Lotus on FVM) +- EVM executes inside a Wasm actor; Filecoin consensus gas rules apply. We intentionally do not try to replicate Ethereum’s gas schedule here. +- Lotus `eth_estimateGas` behavior for 7702 is best‑effort and behavioral: we add an intrinsic overhead per authorization tuple for user experience. Exact constants are placeholders and may change. +- Tests must not assert exact gas usage or effective gas price; they should only assert the presence/absence of tuple overhead and its monotonicity with tuple count. + - Parser/encoding: add `AuthorizationKeccak` test vectors for preimage/hash stability. + +- Networking and limits + - Rely on actor‑level tuple cap; keep general `MaxMessageSize` as is. Optionally validate tuple count client‑side for fast‑fail. + +- Gating and constants + - Activation: via bundle; no runtime NV gates to maintain. + +**Work Breakdown (Sequenced)** +1. RLP per‑type limit in Lotus + tests. (DONE) +2. Canonical CBOR wrapper only (actor + Go helpers) + negative tests. (DONE) +3. Implement atomic 0x04 semantics (EthAccount.ApplyAndCall + Lotus route + receipts) + e2e test. (ApplyAndCall, optional Lotus route, receipts unit tests DONE; E2E PENDING) +4. FVM hardening (InvokeAsEoa guards; pointer semantics; domain/signature checks; tuple cap) + tests. (PARTIAL) +5. Gas estimation alignment to actor constants; maintain behavioral tests until finalization. (DONE) +6. Doc updates and test vector additions for `AuthorizationKeccak`. (IN PROGRESS) + +Detailed test plans are included below: see Builtin‑Actors Test Plan and Lotus Test Plan. These lists are part of the highest‑priority testing work for the sprint. + +References for parity: +- `geth_eip_7702.md` (diff: TestEIP7702, intrinsic gas, empty auth errors) +- `revm_eip_7702.md` (auth validity, gas constants, delegation code handling) + +- Lotus/Go (done): + - Typed 0x04 parsing/encoding with `authorizationList`; dispatch via `ParseEthTransaction`. + - `EthTx` and receipts echo `authorizationList`; receipt adjuster surfaces `delegatedTo` from tuples or `Delegated(address)` event. + - Send path behind `-tags eip7702_enabled`: builds a Filecoin message targeting EthAccount.ApplyAndCall with canonical CBOR params. + - Mempool policies (cross‑account invalidation; per‑EOA cap). No runtime NV gate on this branch. + - Gas estimation scaffold adds intrinsic overhead per tuple (behavioral only). +- Builtin‑actors/Rust (updated): + - EVM interpreter consults Runtime helper for EXTCODE* pointer projection; no unwraps; windowing semantics enforced. + - EthAccount state added; ApplyAndCall validates tuples (domain 0x05, yParity, non‑zero/≤32 R/S, low‑S), enforces nonce equality, persists `delegate_to` and `auth_nonce` (receiver‑only), initializes `evm_storage_root`, and executes the outer call; mapping persists on revert. Initial unit tests added. + - DONE: Legacy EVM delegation paths removed. `EVM.ApplyAndCall` and `InvokeAsEoa` are removed/stubbed; only `InvokeAsEoaWithRoot` remains for VM intercept trampolines. Interpreter does not re‑follow delegation. +- ref‑fvm (updated): + - Runtime/Kernel/SDK helper `get_eth_delegate_to(ActorID)` implemented with strict EthAccount code check; unit test added. + - VM intercept implemented: delegated CALL → execute delegate under authority context (depth=1); mount/persist `evm_storage_root`; emit `Delegated(address)` event (ABI 32‑byte); map success/revert and propagate revert data; EXTCODE* projects 23‑byte pointer code with windowing/zero‑fill semantics. Tests added for windowing, depth limit, value‑transfer short‑circuit, and storage overlay persistence. + +**Spec Gap: MAGIC/Version Prefix Compliance** +- Problem: We currently omit the required MAGIC prefixing in two places required by the spec and parity references (`eip7702.md`, `revm_eip_7702.md`). + - Authorization tuple signing domain: inner tuple signatures must be over `keccak256(0x05 || rlp([chain_id, address, nonce]))` where `0x05` is the authorization domain separator (aka `SetCodeAuthorizationMagic`). + - Delegation indicator bytecode: when applying a delegation, the authority account code must be set to `0xef 0x01 0x00 || <20‑byte delegate address>`, where `0xef01` is the EIP‑7702 bytecode MAGIC and `0x00` is the version. + +Required changes (implementation): +- Lotus (Go) + - `chain/types/ethtypes/`: + - Add constants: `SetCodeTxType = 0x04`, `SetCodeAuthorizationMagic = 0x05`, `Eip7702BytecodeMagic = 0xef01`, `Eip7702BytecodeVersion = 0x00`. + - Implement `AuthTupleHash(chainID, address, nonce) = keccak256(0x05 || rlp([chainID, address, nonce]))` and use it wherever we validate or preview inner signatures (tests and helpers only; authoritative validation happens in actor). + - Extend RLP decode tests to cover tuple arity/yParity rejection and ensure the above hash is stable across edge cases (zero/Big endian, etc.). + - Go helpers cleaned up; Delegator helpers removed. + - `node/impl/eth/receipt_7702_scaffold.go` and `node/impl/eth/gas_7702_scaffold.go`: + - Receipts adjuster updated to topic keccak("Delegated(address)"); data read as 32‑byte ABI word (last 20 bytes extracted); attribution prefers tuples; tolerates missing events. Gas logic remains behavioral and guarded by non‑empty `authorizationList` and target=EthAccount.ApplyAndCall. + +- Builtin‑actors (Rust) + - EVM interpreter: EXTCODE* pointer projection via Runtime helper; `0xef0100` virtual code exposed; windowing and zero‑fill semantics enforced; no unwraps. + - EthAccount.ApplyAndCall: tuple validation as above; receiver‑only persistence; outer call forwards gas; events will be emitted from VM intercept. + - No runtime network‑version gating; activation via bundle. + +Required changes (tests): +- Lotus (Go) + - `chain/types/ethtypes`: AuthTupleHash vectors; tuple arity/yParity negatives. (DONE) + - `node/impl/eth` receipts: attribution via tuples or `Delegated(address)` (32‑byte ABI data tolerant). (DONE) + - `node/impl/eth` gas: behavioral overhead tests guarded by tuple count and target. (DONE) + +- Builtin‑actors (Rust) + - `actors/evm` tests stay green with Runtime helper in EXTCODE* paths. + - `actors/ethaccount` tests added: invalid yParity/lengths, nonce init/increment, atomicity on revert, value transfer short‑circuit. More to be ported as VM intercept lands. + +Validation notes +- Keep the following aligned across repos and tests: + - `SetCodeAuthorizationMagic = 0x05` (authorization domain) + - `Eip7702BytecodeMagic = 0xef01` and `Eip7702BytecodeVersion = 0x00` (delegation indicator) + - Gas constants: treat as placeholders until finalized; do only behavioral assertions in Lotus. + - Activation: via bundle; no runtime NV gates. + +**What Remains** +- VM intercept (ref‑fvm): DONE — delegated CALL → execute delegate under authority context with depth=1; mount/persist `evm_storage_root`; best‑effort `Delegated(address)` event (ABI 32‑byte); success/revert mapping; EXTCODE* projection with windowing/zero‑fill. +- EVM minimalization: DONE — interpreter does not re‑follow delegation; legacy `InvokeAsEoa`/`EVM.ApplyAndCall` removed; only `InvokeAsEoaWithRoot` trampoline remains for VM intercept. +- Gas constants/refunds: finalize numbers (behavioral in Lotus until then). +- Lotus E2E: validate atomic apply+call, delegated execution, and receipts/logs attribution once the wasm bundle is integrated. + +Follow‑ups from 2025‑11‑13 review (status) +- EthAccount → VM outer‑call bridge + E2E (DONE): + - builtin‑actors/ref‑fvm: DONE — EthAccount.ApplyAndCall routes outer calls to EVM contracts via the `InvokeEVM` entrypoint (see `../builtin-actors/actors/ethaccount/src/lib.rs` and `../builtin-actors/actors/ethaccount/tests/apply_and_call_outer_call.rs`), and delegated CALL semantics are exercised via the VM intercept and associated tests in `../ref-fvm/fvm/src/call_manager/default.rs` and `../ref-fvm/fvm/tests`. + - lotus: DONE — `itests/TestEth7702_DelegatedExecute` is enabled (under the `eip7702_enabled` build tag) and asserts the full 0x04 → EthAccount.ApplyAndCall → delegated CALL→EOA lifecycle, including authority storage overlay, `Delegated(address)` event emission, receipts attribution, and status mapping. +- Signature padding positives (DONE): + - builtin‑actors: Explicit EthAccount tests now accept minimally‑encoded big‑endian `r/s` with lengths 1..31 and 32 bytes (left‑padded internally), in addition to the existing >32‑byte and zero‑value rejection tests, locking in interoperability with Lotus’ minimal encoding. See `../builtin-actors/actors/ethaccount/tests/apply_and_call_rs_padding.rs`. +- Delegated(address) event coverage (DONE): + - ref‑fvm: An integration test exercises a delegated CALL→EOA path, then inspects the emitted events (via `EventsRoot`) to assert: + - topic0 = `keccak256("Delegated(address)")`, and + - data is a 32‑byte ABI word whose last 20 bytes equal the authority (EOA) address. + See `../ref-fvm/fvm/tests/delegated_event_emission.rs`. +- Naming/documentation cleanup (PARTIAL): + - lotus/builtin‑actors/ref‑fvm: Code and test comments describing live routing now consistently reference `EthAccount.ApplyAndCall` + VM intercept, and Delegator/EVM.ApplyAndCall paths are marked removed or historical. Some standalone design notes (e.g., early EVM‑only explanation docs) still describe the older EVM.ApplyAndCall‑centric design and will be updated or explicitly marked historical in a later documentation pass. + +--- + +### Actionable TODOs (Handoff Plan) + +This section captures the concrete next steps confirmed in review, for handoff to the next implementer. Items are grouped by criticality and include implementation hints and ownership. + +Decisions (confirmed) +- Atomicity: adopt spec‑compliant persistence — delegation mapping + nonce bumps MUST persist even if the outer call reverts. +- Pre‑existence policy: reject delegations where the authority resolves to an EVM contract actor (Filecoin analogue of “code must be empty/pointer”). +- Tuple cap: enforce a placeholder cap of 64 tuples in a single message (document as placeholder; tune/remove later). +- Mempool policy: no 7702‑specific ingress policies on this branch (documented deviation); standard rules apply. +- Refunds: staged approach — add plumbing + conservative caps now; switch to numeric constants once finalized. + +CRITICAL (consensus/security/spec) +- ApplyAndCall atomicity (spec compliance) + - builtin‑actors: DONE — mapping + nonces persist before outer call; always Exit OK with embedded status/return. + - lotus: DONE — receipt.status is decoded from the embedded status for type‑0x04; attribution unchanged. + - tests: IN PROGRESS — atomic revert persistence asserted at unit level; e2e to follow once wasm lands. +- Pre‑existence policy (do not “set code” on contracts) + - builtin‑actors: In ApplyAndCall, for each authority, resolve to ID and reject when builtin type == EVM (USR_ILLEGAL_ARGUMENT). Add a negative test. (DONE) +- Nested delegation depth limit + - builtin‑actors: DONE — Enforced depth==1. When executing under authority context (via `InvokeAsEoaWithRoot`), delegation chains are not followed. ApplyAndCall-driven unit test present. +- Signature robustness (length + low‑s) + - builtin‑actors: Accept ≤32‑byte R/S (left‑pad to 32); reject >32 with precise errors; keep low‑s and non‑zero checks. Negative tests added. (DONE) + +HIGH PRIORITY (correctness/robustness) +- Refunds (staged) + - builtin‑actors: STAGED — refund plumbing points present; constants/caps to be wired once finalized. + - lotus: Keep estimation behavioral until constants finalize; wire estimates to refunds when available. +- Tuple cap (placeholder) + - builtin‑actors: DONE — Enforces `len(params.list) <= 64`; boundary tests added. Placeholder noted in code comments. +- SELFDESTRUCT interaction + - builtin‑actors: Add tests where delegated code executes SELFDESTRUCT; specify and assert expected behavior for authority mapping/storage and gas. (DONE) +- Fuzzing + - lotus: Add RLP fuzzing for 0x04 tx decode. + - builtin‑actors: Add CBOR fuzzing harness for ApplyAndCall params. +- E2E lifecycle (post‑wasm) + - lotus: Enable full flow once wasm includes ApplyAndCall: apply delegations, CALL→EOA executes delegate, storage persists under authority, event emitted, receipts reflect `authorizationList`, `delegatedTo`, and correct `status`. + +MEDIUM PRIORITY (completeness/tests) +- EOA storage persistence: expand coverage + - builtin‑actors: Add tests for (a) switching delegates (A→B then A→C) and verifying B’s storage persists; (b) clearing delegation and verifying storage remains accessible on re‑delegation. +- First‑time authority nonce handling + - builtin‑actors: Add an explicit test that an absent authority is treated as nonce=0; applying nonce=0 succeeds and initializes the nonces map. +- Estimation parity + - lotus: In E2E, compare `eth_estimateGas` results against mined consumption to ensure intrinsic overhead and (later) refunds behave reasonably. + +Ownership and ordering +- builtin‑actors (consensus): atomicity (spec compliance), pre‑existence check, depth limit, tuple cap, refunds plumbing, signature tests, SELFDESTRUCT tests, fuzzer. +- lotus (client): receipt `status` from ApplyAndCall return, E2E lifecycle, RLP fuzzer, estimation wiring for refunds when ready. + +Acceptance criteria (updated) +- A type‑0x04 tx persists delegation mapping + nonces even when the outer call reverts; Lotus sets receipt status to 0 accordingly. +- Pre‑existence check rejects authorities that are EVM contracts. +- No nested delegation chains are followed (depth=1 enforced). +- Tuple cap of 64 enforced (placeholder); large lists rejected early. +- Refund plumbing present with conservative caps; numeric constants can be dropped in once finalized. +- Event compliance: topic = `keccak("Delegated(address)")` and the emitted address is the authority (EOA). + - Pointer semantics: for delegated authority A→B, `EXTCODESIZE(A) == 23`, `EXTCODECOPY(A,0,0,23)` returns `0xEF 0x01 0x00 || `, and `EXTCODEHASH(A)` equals `keccak(pointer_code)`. + - Delegated CALL revert data: CALL returns 0; `RETURNDATASIZE` equals revert payload length; `RETURNDATACOPY` truncates/returns per requested size with zero_fill=false semantics. + +**Quick Validation** +- Lotus fast path: + - `go build ./chain/types/ethtypes` + - `go test ./chain/types/ethtypes -run 7702 -count=1` + - `go test ./node/impl/eth -run 7702 -count=1` +- ref‑fvm (preferred): + - `../ref-fvm/scripts/run_eip7702_tests.sh` + - The script first builds the builtin‑actors bundle in Docker with `ref-fvm` mounted (to satisfy local patch paths), then runs ref‑fvm tests. If host tests fail, it falls back to running tests inside Docker. + - If Docker is unavailable, bundle build will fail on macOS; proceed with builtin‑actors tests while enabling Docker. +- Builtin‑actors (local toolchain permitting): + - `cargo test -p fil_actor_evm` (EVM runtime changes; EXTCODE* helper usage). + - `cargo test -p fil_actor_ethaccount` (EthAccount state + ApplyAndCall tests: invalids, nonces, atomicity, value transfer). +- Lotus E2E (requires updated wasm bundle): + - `go test ./itests -run Eth7702 -tags eip7702_enabled -count=1` + +- Docker bundle + ref‑fvm tests (recommended on macOS): + - Convenience script: `../ref-fvm/scripts/run_eip7702_tests.sh` + - Builds the builtin‑actors bundle in Docker (`make bundle-testing-repro`). + - Runs ref‑fvm tests, including EXTCODE* windowing, depth limit, value‑transfer short‑circuit, and revert payload propagation. + - Fallback (no Docker): set + `CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang AR_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/llvm-ar RANLIB_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/llvm-ranlib RUSTFLAGS="-C link-arg=-Wl,-dead_strip"` + and run `cargo test -p fvm`. Note: some EVM Wasm invocations may fail on macOS due to `__stack_chk_fail`. + +To route 0x04 transactions in development, build Lotus with `-tags eip7702_enabled`. + +**Files & Areas** +- Lotus: + - `chain/types/ethtypes/` (tx parsing/encoding; CBOR params; types) + - `node/impl/eth/` (send path; gas estimation; receipts) + - `chain/messagepool/` (generic mempool; no 7702-specific policies) +- Builtin‑actors: + - `actors/evm/` (ApplyAndCall; CALL pointer semantics; EXTCODE* behavior; event emission) + - `runtime/src/features.rs` (activation doc) + +**Editing Strategy** +- Keep diffs small and scoped. Mirror existing style (e.g., 1559 code) where possible. +- When changing encodings, update encoder/decoder and tests; no backward‑compatibility is required on this branch. Drop legacy/dual shapes in favor of canonical forms. + + +**Commit Guidance** +- Commit in small, semantic units with clear messages; avoid batching unrelated changes. +- Prefer separate commits for code, tests, and docs when practical. +- Commit frequently to preserve incremental intent; summarize scope and rationale in the subject. +- Push after each atomic change so reviewers can follow intent and history stays readable. +- Keep history readable: no formatting‑only changes mixed with logic changes. +- Pair commits with pushes regularly to keep the remote branch current (e.g., push after each semantic commit or small group of related commits). Coordinate with PR reviews to avoid large, monolithic pushes. + +**Pre‑Commit Checklist (Tooling/Formatting)** +- Lotus (this repo): + - MANDATORY pre‑push checks for CI stability (run in this order): + - `make gen` — regenerate code, CBOR, inline/bundle, and fix imports. + - `go fmt ./...` — format all Go code. + - Do not commit/push to Lotus unless all three pass locally. CI rejects pushes that skip these steps. +- Builtin‑actors (paired repo at `../builtin-actors`): + - Run `cargo fmt --all` to format Rust code consistently across crates. + - Run `make check` to run clippy and enforce `-D warnings` across all crates and targets. + +Notes: +- Keep formatting‑only changes in their own commits where feasible. Avoid mixing formatting with logic changes to keep diffs focused and reviewable. + +**Acceptance Criteria** +- A signed type‑0x04 tx decodes, constructs a Filecoin message calling EthAccount.ApplyAndCall, applies valid delegations atomically with the outer call, and subsequent CALL→EOA executes delegate code. +- JSON‑RPC returns `authorizationList` and `delegatedTo` where applicable. +- Gas estimation accounts for tuple overhead (behavioral assertions). + +**Env/Flags** +- Build tag: `eip7702_enabled` enables the 7702 send‑path in Lotus. +- Env toggles removed; EVM‑only routing is the default. + +**Review Remediation TODOs (Detailed)** + +- CRITICAL: CBOR Interoperability — R/S padding mismatch + - Problem: Lotus encodes `r/s` as minimally‑encoded big‑endian; actor currently requires exactly 32‑byte values. + - builtin‑actors (Actors/EVM): + - Update `actors/evm/src/lib.rs`: + - In `recover_authority`, if `len(r) > 32 || len(s) > 32` → illegal_argument; else left‑pad each to 32 bytes, then build `sig = r||s||v`. + - In `validate_tuple`, change length validation to allow `≤32` and reject `>32` with precise errors. + - In `is_high_s`, assume 32‑byte input (padded earlier) and assert length as needed. + - Tests: add positive vectors for 1..31‑byte `r/s` (padded in actor) and negative for `>32`. + - lotus (client): no encoder changes; add interop tests ensuring minimally‑encoded `r/s` round‑trip and are accepted by actor. + - Acceptance: actor accepts ≤32‑byte `r/s`, rejects >32 with clear error; recovered authority matches padded case. + +- HIGH: RLP Parsing Potential Overflow (Lotus) + - Risk: `chain_id` and `nonce` fields in the 0x04 RLP tuple must support full `uint64` range. + - lotus implementation: ensure `chain_id` and `nonce` are parsed using unsigned integer parsing that supports up to `math.MaxUint64` and avoid intermediate `int64` coercions. + - Files: `chain/types/ethtypes/eth_7702_transactions.go` (tuple decode helpers). + - Tests: extend decoder tests with values above `MaxInt64` and near `MaxUint64` to prove no overflow; retain canonical arity/yParity rejection tests. + +- MEDIUM: Misleading error on signature length (Actors) + - builtin‑actors: refactor error reporting in `validate_tuple` to check `r/s` length before low‑s/zero checks; return precise messages: “r length exceeds 32”, “s length exceeds 32”, “zero r/s”, “invalid y_parity”. Keep low‑s check on padded 32‑byte `s`. + - Tests: negative vectors asserting error reasons for length >32 and invalid yParity. + - Status: DONE — length checks occur first; precise messages present; tests assert length‑error messages. + +- HIGH: Insufficient Actor Validation Tests + - Add a dedicated `actors/evm/tests/apply_and_call_invalid.rs` suite covering: + - Invalid `chainId` (not 0 or local). + - Invalid `yParity` (>1). + - Zero R or zero S. + - High‑S rejection. + - Nonce mismatch. + - Pre‑existence policy violation (authority is an EVM contract). + - Duplicate authority in a single message. + - Status: DONE — covered by `apply_and_call_invalids.rs`, `apply_and_call_nonces.rs`, `apply_and_call_duplicates.rs`. + +- MEDIUM: Missing Corner Case Tests (Actors) + - SELFDESTRUCT no‑op in delegated context: + - Build delegate bytecode that executes SELFDESTRUCT when executed under authority context (via VM intercept); assert no authority state/balance change; pointer mapping preserved; event emission intact. + - Storage isolation/persistence on delegate changes: + - A→B write storage; switch A→C and verify C cannot read B’s storage; clear A→0; re‑delegate A→B and verify B’s storage persists. + - First‑time nonce handling: + - Absent authority defaults to nonce=0; applying nonce=0 succeeds; next attempt with nonce=0 fails with nonce mismatch. + - Status: DONE — `apply_and_call_selfdestruct.rs`, `delegated_storage_isolation.rs`, `delegated_storage_persistence.rs`, and `apply_and_call_nonces.rs`. + +- LOW: Fuzzing and Vectors + - lotus: add RLP fuzz harness for 0x04 decode focused on tuple arity/yParity/value sizes and malformed tails; ensure no panics and proper erroring. + - builtin‑actors: add CBOR fuzz harness for `ApplyAndCallParams` that mutates wrapper shape, tuple arity, and byte sizes for `r/s`. + - Status: DONE (builtin‑actors) — CBOR fuzz harness `apply_and_call_cbor_fuzz.rs` present. + - lotus: add AuthorizationKeccak test vectors for `keccak256(0x05 || rlp([chain_id,address,nonce]))` covering boundary values. + +- Gas Model reminders (Lotus on FVM) + - Do not pin exact gas constants or absolute gas usage in tests. Keep tests behavioral: overhead only when tuples present, monotonic with tuple count, and disabled when feature off or non‑ApplyAndCall targets. + +Ownership and Acceptance (for this section) +- builtin‑actors: implement R/S padding, improve error clarity, add negative and corner‑case tests, add CBOR fuzz harness. +- lotus: implement RLP overflow robustness, add AuthorizationKeccak vectors, add RLP fuzz harness. +- Acceptance: all new tests pass; fuzz harnesses run without panics; interop for minimally‑encoded R/S validated; behavioral gas tests remain green. + +**Builtin‑Actors Review (Action Items)** + +This section captures additional items from the comprehensive review of `builtin-actors.eip7702.diff` and aligns them with this notebook. + +- CRITICAL — Event semantics and topic (spec compliance) + - Event name/signature: use `Delegated(address)` for the topic hash, not `EIP7702Delegated(address)`. + - Files: `actors/evm/src/interpreter/instructions/call.rs`, `actors/evm/src/lib.rs`. + - Action: change the string used for the topic hash to `b"Delegated(address)"`. + - Lotus follow‑up: update `adjustReceiptForDelegation` to recognize the `Delegated(address)` topic. + - Status: DONE — centralized as a shared constant and used in both emission sites. + - Emitted address value: log the authority (EOA) address, not the delegate contract address. + - Files: `actors/evm/src/interpreter/instructions/call.rs`, `actors/evm/src/lib.rs`. + - Action: swap the value encoded in the event data from the delegate to the destination/authority. + - Status: DONE — tests updated to assert authority address. + +- HIGH — Behavioral correctness + - Revert data propagation on delegated CALL failures + - File: ref‑fvm call manager intercept and EVM interpreter return mapping. + - Action: when the VM intercept returns a revert, propagate revert bytes to the EVM return buffer and ensure `RETURNDATASIZE/RETURNDATACOPY` expose them. + - Status: DONE — intercept path maps revert and copies data to memory; tests assert behavior. + - Value transfer result handling in `ApplyAndCall` + - File: `actors/evm/src/lib.rs`. + - Action: check the result of `system.transfer` for delegated EOA targets; if it fails, set `status: 0` and return immediately (mirrors CALL behavior). + - Status: DONE — short‑circuit implemented and covered by tests. + +- HIGH — Tests to add (consensus‑critical and behavioral) + - `actors/evm/tests/apply_and_call_invalids.rs`: invalid `chainId`, `yParity > 1`, zero R/S, high‑S, nonce mismatch, duplicate authorities, exceeding 64‑tuple cap. (DONE) + - Pre‑existence policy: reject when authority resolves to an EVM contract actor (expect `USR_ILLEGAL_ARGUMENT`). (DONE) + - R/S padding interop: accept 1..31‑byte R/S (left‑padded) and reject >32 bytes. (DONE) + - Event correctness: assert topic = `keccak("Delegated(address)")` and that the indexed/address corresponds to the authority (EOA), not the delegate contract. (DONE) + - Pointer semantics: `actors/evm/tests/eoa_call_pointer_semantics.rs` validates EXTCODESIZE=23, EXTCODECOPY exact bytes, and EXTCODEHASH of pointer code. (DONE) + - Delegated CALL revert data: `actors/evm/tests/delegated_call_revert_data.rs` validates RETURNDATASIZE and RETURNDATACOPY truncation/full‑copy semantics. (DONE) + +- MEDIUM — Interpreter corner cases (tests) + - SELFDESTRUCT is a no‑op in delegated context; authority mapping/storage and balances unaffected; event emission intact. + - Storage persistence/isolation across delegate changes: A→B write, A→C can’t read B; clear A→0; re‑delegate A→B and B’s storage persists. + - Depth limit: nested delegations (depth > 1) are not followed under authority context. + - First‑time nonce handling: absent authority treated as nonce=0; applying nonce=0 initializes nonces map; subsequent nonce=0 fails. + +- MEDIUM — Robustness (internal invariants) + - Return data deserialization must not silently fall back. + - Files: `actors/evm/src/interpreter/instructions/call.rs`, `actors/evm/src/lib.rs`. + - Action: replace `.unwrap_or_else(|_| r.data)` with mandatory decode; on failure, return `ActorError::illegal_state`. + - Status: TODO + - Avoid `unwrap()` in `extcodehash` path. + - File: `actors/evm/src/interpreter/instructions/ext.rs`. + - Action: handle `BytecodeHash::try_from(...)` errors by returning `ActorError::illegal_state` (or add a clear `expect` message at minimum). + - Status: TODO + +- LOW — Code quality improvements + - Remove redundant R/S length checks from `recover_authority` (already validated in `validate_tuple`). + - Strengthen `is_high_s` signature to `&[u8; 32]` to avoid runtime asserts. + - Replace `expect` in actor code paths (e.g., resolved EVM address) with explicit error returns. + - Downgrade normal execution‑path logging to `debug/trace` for failed transfers and delegate call failures. + - Precompute `N/2` as a constant used by `is_high_s` (avoid recomputing at runtime). + - File: `actors/evm/src/lib.rs`. + - Action: introduce `N_DIV_2: [u8; 32]` (or equivalent) and compare against that constant. + - Status: TODO + - Centralize `InvokeContractReturn` type definition. + - Files: `actors/evm/src/interpreter/instructions/call.rs`, `actors/evm/src/lib.rs`. + - Action: move the struct to a shared module (e.g., `actors/evm/src/types.rs`) and reuse; remove local duplicates. + - Status: TODO + - Consolidate `Delegated(address)` event emission logic to a helper to remove duplication across success arms. + - Files: `actors/evm/src/interpreter/instructions/call.rs`, `actors/evm/src/lib.rs`. + - Action: factor event emission into a single helper and invoke once on success (regardless of return data presence). + - Status: TODO + +**Review Readiness (Scar‑less PR Candidate)** +- Routing: all 0x04 transactions route to EthAccount `ApplyAndCall`; no Delegator send/execute path remains. +- Atomic‑only: there are no non‑atomic paths or fallback code; tests assert atomic semantics for success and revert. +- Clean surface: no optional env toggles for routing or legacy decoder branches; documentation and tests reference a single, canonical flow. + +**Migration / Compatibility** +- No migration required. The implementation is EVM‑only and atomic‑only. +- Delegator has been removed; EthAccount.ApplyAndCall is the sole entry point. +Domain: `0x05`. Pointer code magic/version: `0xef 0x01 0x00`. + +--- + +**Builtin‑Actors Test Plan (EthAccount + EVM)** + +Scope: `../builtin-actors` (paired repo), tracked here for sprint execution. Keep encodings and gas behavior in sync with Lotus. + +Priority: P0 (blocking), P1 (recommended), P2 (nice‑to‑have) + +P0 — Critical (spec + safety) +- Persistent delegated storage context (DONE) + - VM intercept executes delegate under authority context against the authority’s persistent storage root, and persists the updated `evm_storage_root` back into EthAccount state on success. +- Atomicity semantics (spec‑compliant persistence) + - Delegation mapping and nonce bumps persist even if the outer call reverts. Tests assert persistence on revert and embedded status/return in ApplyAndCall result. +- Intrinsic gas charging (per tuple) (DONE) + - Per‑authorization intrinsic gas charged before validation; behavior covered in tests. + +P0 — ApplyAndCall core (DONE) +- Happy path (atomic apply+call) validates mapping/nonce behavior. +- Invalids rejected with `USR_ILLEGAL_ARGUMENT`: empty list, invalid chainId, invalid yParity, zero r/s, high‑s, nonce mismatch, duplicates. +- Tuple decoding shape (DAG‑CBOR): canonical atomic params; round‑trip tested. + +P0 — Delegated CALL semantics (DONE) +- Delegation handling: CALL→EOA executes delegate under authority context via VM intercept; depth limited to 1. +- Delegated execution: delegate writes storage; CALL→EOA executes delegate; event emitted; depth limited to 1. + +P1 — Authorization semantics and state +- chainId handling: accept 0 (global) and local ChainID; reject others. +- Nonce accounting: absent authority treated as nonce=0; applying nonce=0 initializes; subsequent apply requires increment. +- Duplicate authorities in one message: rejected (DONE). +- Map/nonce HAMT integrity: flush/reload yields identical mappings and nonces. + +P1 — Gas and refunds +- Defer asserting absolute numeric charges until constants are finalized; focus on behavior (paths invoked, no double‑charging across tuples). +- Refund behavior validated behaviorally; switch to numeric assertions once constants stabilize. +- Intrinsic gas OOG not asserted in unit tests. + +P1 — Encoding and interop (DONE) +- Cross‑compat with Lotus: atomic CBOR params match; round‑trip vectors added. + +P2 — Edge and fuzz +- Fuzz tuple decoding for arity/type issues. +- Large authorization lists (stress HAMT) within block gas limits. +- Malicious inputs: overlong leading zeros/odd sizes in r/s, etc. + +Suggested test locations +- `actors/evm/tests/` + - `apply_and_call_happy.rs` (P0) — covered by existing happy‑path tests + - `apply_and_call_invalids.rs` (P0) — DONE + - `apply_and_call_tuple_roundtrip.rs` (P0) — DONE + - `apply_and_call_nonces.rs` (P1) — DONE + - `eoa_call_pointer_semantics.rs` (P0) — DONE + - `eip7702_delegated_log.rs` (P0) — covered in event assertions within existing tests + - `delegated_storage_persistence.rs` (P0) — DONE + - `apply_and_call_atomicity_revert.rs` (P0) — DONE + - `apply_and_call_intrinsic_gas.rs` (P1) — DONE + - `apply_and_call_duplicates.rs` (P1) — DONE + - `delegated_call_revert_data.rs` (P0) — DONE + +Notes + +- When gas constants change, update both repos and adjust tests together. + +--- + +**Lotus Test Plan** + +This list tracks Lotus‑side tests for EIP‑7702 and complements the builtin‑actors plan above. + +Priority: P0 (now), P1 (soon), P2 (later) + +P0 — Decisions & safety +- Mempool policy (DECIDED: document deviation) + - No 7702‑specific ingress policies on this branch; standard mempool rules apply. Documented in this notebook/changelog. + +P0 — Parsers and encoding +- RLP 0x04 parser/encoder + - Round‑trip encode/decode; multi‑authorization list; empty `authorizationList` rejected; non‑empty `accessList` rejected; invalid outer `v` rejected; invalid auth `yParity` rejected; wrong tuple arity rejected; signature init from 65‑byte r||s||v. +- CBOR params (ApplyAndCall): `[ [tuple...], [to(20), value, input] ]` + - Encoder produces canonical wrapper of 6‑tuples; compatible with actor decoder. + +P0 — SignedMessage view + receipts +- Eth view reconstruction (DONE) + - `EthTransactionFromSignedFilecoinMessage` reconstructs 0x04 (EthAccount.ApplyAndCall) and echoes `authorizationList`. +- Receipts attribution (DONE) + - `adjustReceiptForDelegation` sets `delegatedTo` from `authorizationList` or synthetic `Delegated(address)` event. + +P0 — Mempool (N/A on this branch) +- Cross‑account invalidation and per‑EOA cap not implemented; deviation documented. + +P0 — Gas accounting (scaffold) (DONE) +- Counting + gating only; no absolute overhead assertions. +- `countAuthInApplyAndCallParams` handles canonical wrapper; tests cover monotonicity. + +P1 — JSON‑RPC plumbing (DONE) +- `eth_getTransactionReceipt` returns `authorizationList` and `delegatedTo`; covered in unit tests. +- Block/tx receipt flows call `adjustReceiptForDelegation`. +- EthTransaction reconstruction robustness: strict decoder for ApplyAndCall params with negative tests for malformed CBOR. + +P1 — Estimation integration (DONE) +- `eth_estimateGas` adds intrinsic overhead for N tuples (behavioral placeholder); tuple counting tested. + +P1 — E2E tests (behind `eip7702_enabled`, run once wasm includes EthAccount.ApplyAndCall) +- Send‑path routing constructs ApplyAndCall params; mined receipt echoes `authorizationList` and `delegatedTo`. +- Delegated execution: CALL→EOA executes delegate code via actor/runtime; storage/logs reflect delegation. +- Persistent storage across transactions under authority. +- Atomicity/revert semantics aligned with spec‑compliant persistence: mapping/nonces persist; receipt status reflects embedded status. + +P2 — Edge/fuzz +- Fuzz RLP parsing for malformed tuples/fields; broaden coverage. +- Large `authorizationList` sizes for performance regressions. + +P1 — RLP robustness (DONE) +- Negative tests for canonical integer encodings (leading zeros) in tuple fields; parser enforces canonical forms. + +P1 — Additional negative tests (DONE) +- ApplyAndCall CBOR reconstruction: malformed tuple arity, empty list, invalid address length; strict decoder rejects shape/type mismatches. + +Notes + +- Update gas constants/refunds in lockstep once finalized. diff --git a/build/openrpc/full.json b/build/openrpc/full.json index 02d4aa630cb..5a4a6d2f638 100644 --- a/build/openrpc/full.json +++ b/build/openrpc/full.json @@ -3771,7 +3771,20 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ], @@ -3779,6 +3792,45 @@ { "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3807,6 +3859,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -4064,7 +4129,20 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ], @@ -4072,6 +4150,45 @@ { "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -4100,6 +4217,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -4959,7 +5089,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -4977,6 +5117,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -5157,7 +5336,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -5175,6 +5364,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -5349,7 +5577,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -5367,6 +5605,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -5558,7 +5835,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -5576,6 +5863,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -5907,11 +6233,63 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ], "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -5940,6 +6318,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -6182,11 +6573,63 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ], "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -6215,6 +6658,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" diff --git a/build/openrpc/gateway.json b/build/openrpc/gateway.json index 9bfe5ca2a9e..16f39c045bf 100644 --- a/build/openrpc/gateway.json +++ b/build/openrpc/gateway.json @@ -3111,7 +3111,20 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ], @@ -3119,6 +3132,45 @@ { "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3147,6 +3199,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -4006,7 +4071,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -4024,6 +4099,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -4204,7 +4318,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -4222,6 +4346,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -4396,7 +4559,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -4414,6 +4587,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -4745,11 +4957,63 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ], "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -4778,6 +5042,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" diff --git a/build/openrpc/v2/full.json b/build/openrpc/v2/full.json index 839f86b5f48..4213ebafeec 100644 --- a/build/openrpc/v2/full.json +++ b/build/openrpc/v2/full.json @@ -1294,7 +1294,20 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ], @@ -1302,6 +1315,45 @@ { "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -1330,6 +1382,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -1587,7 +1652,20 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ], @@ -1595,6 +1673,45 @@ { "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -1623,6 +1740,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -2482,7 +2612,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -2500,6 +2640,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -2680,7 +2859,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -2698,6 +2887,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -2872,7 +3100,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -2890,6 +3128,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3081,7 +3358,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -3099,6 +3386,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3430,11 +3756,63 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ], "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3463,6 +3841,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -3705,11 +4096,63 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ], "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3738,6 +4181,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" diff --git a/build/openrpc/v2/gateway.json b/build/openrpc/v2/gateway.json index 2fcbe094372..8f97f918f55 100644 --- a/build/openrpc/v2/gateway.json +++ b/build/openrpc/v2/gateway.json @@ -1334,7 +1334,20 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ], @@ -1342,6 +1355,45 @@ { "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -1370,6 +1422,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -1627,7 +1692,20 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ], @@ -1635,6 +1713,45 @@ { "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -1663,6 +1780,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -2522,7 +2652,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -2540,6 +2680,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -2720,7 +2899,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -2738,6 +2927,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -2912,7 +3140,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -2930,6 +3168,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3121,7 +3398,17 @@ ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ], "additionalProperties": false, @@ -3139,6 +3426,45 @@ }, "type": "array" }, + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3470,11 +3796,63 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ], "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3503,6 +3881,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" @@ -3745,11 +4136,63 @@ "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ], "additionalProperties": false, "properties": { + "authorizationList": { + "items": { + "additionalProperties": false, + "properties": { + "address": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "chainId": { + "title": "number", + "type": "number" + }, + "nonce": { + "title": "number", + "type": "number" + }, + "r": { + "additionalProperties": false, + "type": "object" + }, + "s": { + "additionalProperties": false, + "type": "object" + }, + "yParity": { + "title": "number", + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, "blockHash": { "items": { "description": "Number is a number", @@ -3778,6 +4221,19 @@ "title": "number", "type": "number" }, + "delegatedTo": { + "items": { + "items": { + "description": "Number is a number", + "title": "number", + "type": "number" + }, + "maxItems": 20, + "minItems": 20, + "type": "array" + }, + "type": "array" + }, "effectiveGasPrice": { "additionalProperties": false, "type": "object" diff --git a/chain/actors/builtin/delegator/README.md b/chain/actors/builtin/delegator/README.md new file mode 100644 index 00000000000..adfa79ec0e8 --- /dev/null +++ b/chain/actors/builtin/delegator/README.md @@ -0,0 +1,6 @@ +# Delegator Actor (Deprecated on this branch) + +This scaffold is deprecated on the EIP‑7702 development branch. EthAccount state plus the +ref‑fvm VM intercept now implement 7702 end‑to‑end (state, validation, ApplyAndCall bridge, +pointer semantics), and no Delegator actor is used in send/execute paths. See AGENTS.md and +`documentation/eip7702_ethaccount_ref-fvm_migration.md` for the current design. diff --git a/chain/messagepool/messagepool_test.go b/chain/messagepool/messagepool_test.go index 8dda5f7aa17..4915f997824 100644 --- a/chain/messagepool/messagepool_test.go +++ b/chain/messagepool/messagepool_test.go @@ -46,6 +46,8 @@ type testMpoolAPI struct { published int baseFee types.BigInt + + nv network.Version } func newTestMpoolAPI() *testMpoolAPI { @@ -54,6 +56,7 @@ func newTestMpoolAPI() *testMpoolAPI { statenonce: make(map[address.Address]uint64), balance: make(map[address.Address]types.BigInt), baseFee: types.NewInt(100), + nv: buildconstants.TestNetworkVersion, } genesis := mock.MkBlock(nil, 1, 1) tma.tipsets = append(tma.tipsets, mock.TipSet(genesis)) @@ -186,7 +189,7 @@ func (tma *testMpoolAPI) StateDeterministicAddressAtFinality(ctx context.Context } func (tma *testMpoolAPI) StateNetworkVersion(ctx context.Context, h abi.ChainEpoch) network.Version { - return buildconstants.TestNetworkVersion + return tma.nv } func (tma *testMpoolAPI) MessagesForBlock(ctx context.Context, h *types.BlockHeader) ([]*types.Message, []*types.SignedMessage, error) { diff --git a/chain/types/ethtypes/eth_7702_atomic_test.go b/chain/types/ethtypes/eth_7702_atomic_test.go new file mode 100644 index 00000000000..8ad8431ad27 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_atomic_test.go @@ -0,0 +1,136 @@ +package ethtypes + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/filecoin-project/go-address" + abi2 "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + + "github.com/filecoin-project/lotus/build/buildconstants" +) + +func TestEIP7702_ToUnsignedFilecoinMessageAtomic_ShapesAndMethod(t *testing.T) { + // Enable feature flag and set EthAccount.ApplyAndCall addr + Eip7702FeatureEnabled = true + defer func() { Eip7702FeatureEnabled = false }() + EthAccountApplyAndCallActorAddr, _ = address.NewIDAddress(999) + + // Assemble a simple tx with one auth and call fields + var to EthAddress + for i := range to { + to[i] = 0xAA + } + var authAddr EthAddress + for i := range authAddr { + authAddr[i] = 0xBB + } + tx := &Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 1, + To: &to, + Value: big.NewInt(12345), + Input: []byte{0xde, 0xad, 0xbe, 0xef}, + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 50000, + AuthorizationList: []EthAuthorization{{ + ChainID: EthUint64(buildconstants.Eip155ChainId), + Address: authAddr, + Nonce: 7, + YParity: 0, + R: EthBigInt(big.NewInt(1)), + S: EthBigInt(big.NewInt(2)), + }}, + } + from, _ := address.NewIDAddress(1001) + msg, err := tx.ToUnsignedFilecoinMessageAtomic(from) + require.NoError(t, err) + require.EqualValues(t, abi2.MethodNum(MethodHash("ApplyAndCall")), msg.Method) + + // Decode params shape: [ [tuple...], [to(20b), value(bytes), input(bytes)] ] + r := cbg.NewCborReader(bytes.NewReader(msg.Params)) + maj, l, err := r.ReadHeader() + require.NoError(t, err) + require.EqualValues(t, cbg.MajArray, maj) + require.EqualValues(t, 2, l) + + // Inner list length + maj, innerLen, err := r.ReadHeader() + require.NoError(t, err) + require.EqualValues(t, cbg.MajArray, maj) + require.EqualValues(t, 1, innerLen) + // Consume inner tuples generically + for i := 0; i < int(innerLen); i++ { + // tuple header + _, tlen, err := r.ReadHeader() + require.NoError(t, err) + require.EqualValues(t, 6, tlen) + // chain_id + _, _, err = r.ReadHeader() + require.NoError(t, err) + // address + _, blen, err := r.ReadHeader() + require.NoError(t, err) + require.EqualValues(t, uint64(20), blen) + // consume address bytes + if blen > 0 { + tmp1 := make([]byte, blen) + _, err = r.Read(tmp1) + require.NoError(t, err) + } + // nonce + _, _, err = r.ReadHeader() + require.NoError(t, err) + // y_parity + _, _, err = r.ReadHeader() + require.NoError(t, err) + // r + _, blen, err = r.ReadHeader() + require.NoError(t, err) + require.GreaterOrEqual(t, blen, uint64(1)) + tmp2 := make([]byte, blen) + _, err = r.Read(tmp2) + require.NoError(t, err) + // s + _, blen, err = r.ReadHeader() + require.NoError(t, err) + require.GreaterOrEqual(t, blen, uint64(1)) + tmp3 := make([]byte, blen) + _, err = r.Read(tmp3) + require.NoError(t, err) + } + + // Call tuple + maj, clen, err := r.ReadHeader() + require.NoError(t, err) + require.EqualValues(t, cbg.MajArray, maj) + require.EqualValues(t, uint64(3), clen) + // to + maj, blen, err := r.ReadHeader() + require.NoError(t, err) + require.EqualValues(t, cbg.MajByteString, maj) + require.EqualValues(t, uint64(20), blen) + if blen > 0 { + tmp4 := make([]byte, blen) + _, err = r.Read(tmp4) + require.NoError(t, err) + } + // value + maj, blen, err = r.ReadHeader() + require.NoError(t, err) + require.EqualValues(t, cbg.MajByteString, maj) + require.GreaterOrEqual(t, blen, uint64(1)) + tmp5 := make([]byte, blen) + _, err = r.Read(tmp5) + require.NoError(t, err) + // input + maj, blen, err = r.ReadHeader() + require.NoError(t, err) + require.EqualValues(t, cbg.MajByteString, maj) + require.EqualValues(t, uint64(4), blen) +} diff --git a/chain/types/ethtypes/eth_7702_authhash_test.go b/chain/types/ethtypes/eth_7702_authhash_test.go new file mode 100644 index 00000000000..f3f313e4c8d --- /dev/null +++ b/chain/types/ethtypes/eth_7702_authhash_test.go @@ -0,0 +1,75 @@ +package ethtypes + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEthAuthorization_DomainHash_UsesTupleFields(t *testing.T) { + var addr EthAddress + for i := range addr { + addr[i] = 0x33 + } + auth := EthAuthorization{ChainID: EthUint64(123), Address: addr, Nonce: EthUint64(9)} + h1, err := auth.DomainHash() + require.NoError(t, err) + + // Mutate nonce -> hash must change + auth2 := auth + auth2.Nonce = EthUint64(10) + h2, err := auth2.DomainHash() + require.NoError(t, err) + require.NotEqual(t, h1, h2) + + // Same tuple computed via raw helper + h3, err := AuthorizationKeccak(uint64(auth.ChainID), auth.Address, uint64(auth.Nonce)) + require.NoError(t, err) + require.Equal(t, h1, h3) +} + +func TestAuthorizationKeccak_BoundaryValues(t *testing.T) { + var addr EthAddress + for i := range addr { + addr[i] = 0xAB + } + // chainId=0, nonce=0 + h0, err := AuthorizationKeccak(0, addr, 0) + require.NoError(t, err) + // chainId=max, nonce=max + hmax, err := AuthorizationKeccak(^uint64(0), addr, ^uint64(0)) + require.NoError(t, err) + // Distinct hashes expected + require.NotEqual(t, h0, hmax) +} + +func TestAuthorizationKeccak_AddressEdgeCases(t *testing.T) { + // All-zero address + var zero EthAddress + // All-0xff address + var ff EthAddress + for i := range ff { + ff[i] = 0xFF + } + // One-bit toggle + var one EthAddress + one[19] = 0x01 + + hZero, err := AuthorizationKeccak(1, zero, 2) + require.NoError(t, err) + hFF, err := AuthorizationKeccak(1, ff, 2) + require.NoError(t, err) + hOne, err := AuthorizationKeccak(1, one, 2) + require.NoError(t, err) + + // Pairwise distinct + require.NotEqual(t, hZero, hFF) + require.NotEqual(t, hZero, hOne) + require.NotEqual(t, hFF, hOne) + + // Cross-check against EthAuthorization.DomainHash for zero address + a := EthAuthorization{ChainID: EthUint64(1), Address: zero, Nonce: EthUint64(2)} + hAuth, err := a.DomainHash() + require.NoError(t, err) + require.Equal(t, hZero, hAuth) +} diff --git a/chain/types/ethtypes/eth_7702_env.go b/chain/types/ethtypes/eth_7702_env.go new file mode 100644 index 00000000000..b2927cd8899 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_env.go @@ -0,0 +1,9 @@ +package ethtypes + +import "github.com/filecoin-project/go-address" + +// Stub declarations for builds without the eip7702_enabled tag so references compile. +// EthAccountApplyAndCallActorAddr is the primary 0x04 target on this branch. +// EvmApplyAndCallActorAddr remains as a deprecated alias name for historical compatibility. +var EvmApplyAndCallActorAddr address.Address +var EthAccountApplyAndCallActorAddr address.Address diff --git a/chain/types/ethtypes/eth_7702_featureflag.go b/chain/types/ethtypes/eth_7702_featureflag.go new file mode 100644 index 00000000000..86879ba6ba3 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_featureflag.go @@ -0,0 +1,5 @@ +package ethtypes + +// Eip7702FeatureEnabled toggles building Filecoin messages from 7702 txs. +// Default is false; enable via build tag file. +var Eip7702FeatureEnabled = false diff --git a/chain/types/ethtypes/eth_7702_featureflag_enabled.go b/chain/types/ethtypes/eth_7702_featureflag_enabled.go new file mode 100644 index 00000000000..8c98bf7f079 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_featureflag_enabled.go @@ -0,0 +1,7 @@ +//go:build eip7702_enabled + +package ethtypes + +func init() { + Eip7702FeatureEnabled = true +} diff --git a/chain/types/ethtypes/eth_7702_featureflag_test.go b/chain/types/ethtypes/eth_7702_featureflag_test.go new file mode 100644 index 00000000000..d92619e3c99 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_featureflag_test.go @@ -0,0 +1,50 @@ +//go:build eip7702_enabled + +package ethtypes + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + + "github.com/filecoin-project/lotus/build/buildconstants" +) + +// Validates that when the eip7702_enabled build tag is set and the EthAccountApplyAndCallActorAddr +// is configured, ToUnsignedFilecoinMessage constructs a message targeting the EthAccount actor +// with CBOR-encoded params. +func Test7702_ToUnsignedFilecoinMessage_FeatureFlag(t *testing.T) { + // Configure a fake EthAccount.ApplyAndCall actor address + a, err := address.NewIDAddress(1234) + require.NoError(t, err) + EthAccountApplyAndCallActorAddr = a + + // Minimal 7702 tx with one authorization + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + tx := &Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 1, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: EthUint64(buildconstants.Eip155ChainId), Address: to, Nonce: EthUint64(1), YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}, + }, + V: big.NewInt(0), R: big.NewInt(1), S: big.NewInt(1), + } + + from, err := address.NewIDAddress(999) + require.NoError(t, err) + msg, err := tx.ToUnsignedFilecoinMessageAtomic(from) + require.NoError(t, err) + require.Equal(t, EthAccountApplyAndCallActorAddr, msg.To) + require.EqualValues(t, abi.MethodNum(MethodHash("ApplyAndCall")), msg.Method) + require.NotEmpty(t, msg.Params) +} diff --git a/chain/types/ethtypes/eth_7702_from_signedmsg_test.go b/chain/types/ethtypes/eth_7702_from_signedmsg_test.go new file mode 100644 index 00000000000..a08d22ead75 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_from_signedmsg_test.go @@ -0,0 +1,292 @@ +package ethtypes + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + builtintypes "github.com/filecoin-project/go-state-types/builtin" + typescrypto "github.com/filecoin-project/go-state-types/crypto" + + "github.com/filecoin-project/lotus/chain/types" +) + +// encodeAuthWrapper encodes a wrapper [ list ] with one 6-tuple for convenience. +func encodeAuthWrapper(t *testing.T) []byte { + t.Helper() + var buf bytes.Buffer + // wrapper [ list ] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + // tuple [ chain_id, address(20), nonce, y_parity, r, s ] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 6)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 314)) + // 20-byte address + var addr [20]byte + for i := range addr { + addr[i] = 0xaa + } + require.NoError(t, cbg.WriteByteArray(&buf, addr[:])) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) // nonce + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) // y_parity + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) // r + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) // s + return buf.Bytes() +} + +func TestEthTransactionFromSignedMessage_7702_Decodes(t *testing.T) { + // Setup: set EthAccountApplyAndCallActorAddr to ID:18 + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + // From must be an eth (f4) address + var from20 [20]byte + for i := range from20 { + from20[i] = 0x11 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + + // Build SignedMessage targeting EthAccount.ApplyAndCall + msg := types.Message{ + Version: 0, + To: EthAccountApplyAndCallActorAddr, + From: from, + Nonce: 0, + Value: types.NewInt(0), + Method: abi.MethodNum(MethodHash("ApplyAndCall")), + GasLimit: 100000, + GasFeeCap: types.NewInt(1), + GasPremium: types.NewInt(1), + Params: encodeAuthWrapper(t), + } + // Fake a delegated 65-byte signature r||s||v where r,s=1 and v=0 + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + tx, err := EthTransactionFromSignedFilecoinMessage(smsg) + require.NoError(t, err) + // Expect a 0x04 typed tx and authorizationList echoed + require.EqualValues(t, EIP7702TxType, tx.Type()) + eth, err := tx.ToEthTx(smsg) + require.NoError(t, err) + require.Len(t, eth.AuthorizationList, 1) + require.EqualValues(t, 314, eth.AuthorizationList[0].ChainID) +} + +func TestEthTransactionFromSignedMessage_7702_MultiTupleDecodes(t *testing.T) { + // Setup ID:18 EthAccount.ApplyAndCall address and f4 sender + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + var from20 [20]byte + for i := range from20 { + from20[i] = 0x22 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + + // Build params wrapper with two tuples + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + encTup := func(chain uint64, nonce uint64) { + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 6)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, chain)) + var a [20]byte + for i := range a { + a[i] = 0xAA + } + require.NoError(t, cbg.WriteByteArray(&buf, a[:])) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, nonce)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + } + encTup(314, 0) + encTup(314, 1) + + msg := types.Message{To: EthAccountApplyAndCallActorAddr, From: from, Method: abi.MethodNum(MethodHash("ApplyAndCall")), Params: buf.Bytes(), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1), Value: types.NewInt(0)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + tx, err := EthTransactionFromSignedFilecoinMessage(smsg) + require.NoError(t, err) + eth, err := tx.ToEthTx(smsg) + require.NoError(t, err) + require.Len(t, eth.AuthorizationList, 2) +} + +func TestEthTransactionFromSignedMessage_NonDelegatedSigRejected(t *testing.T) { + // Setup EthAccount.ApplyAndCall address; signature type is wrong (secp256k1) + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + // Sender can be anything; rejection occurs earlier on sig type + from, _ := address.NewIDAddress(1001) + msg := types.Message{To: EthAccountApplyAndCallActorAddr, From: from, Method: abi.MethodNum(MethodHash("ApplyAndCall"))} + sig := typescrypto.Signature{Type: typescrypto.SigTypeSecp256k1, Data: make([]byte, 65)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + _, err := EthTransactionFromSignedFilecoinMessage(smsg) + require.Error(t, err) +} + +func TestEthTransactionFromSignedMessage_SenderNotEthRejected(t *testing.T) { + // Delegated signature but non-f4 sender should be rejected + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + // Non-eth sender: ID address + from, _ := address.NewIDAddress(42) + msg := types.Message{To: EthAccountApplyAndCallActorAddr, From: from, Method: abi.MethodNum(MethodHash("ApplyAndCall"))} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: make([]byte, 65)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + _, err := EthTransactionFromSignedFilecoinMessage(smsg) + require.Error(t, err) +} + +func TestEthTransactionFromSignedMessage_7702_BadCBORRejected(t *testing.T) { + // Setup ID:18 EthAccount.ApplyAndCall address and f4 sender + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + var from20 [20]byte + for i := range from20 { + from20[i] = 0x33 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + + // Malformed CBOR params (unsigned int header instead of array) + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 7)) + + msg := types.Message{To: EthAccountApplyAndCallActorAddr, From: from, Method: abi.MethodNum(MethodHash("ApplyAndCall")), Params: buf.Bytes(), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + _, err = EthTransactionFromSignedFilecoinMessage(smsg) + require.Error(t, err) +} + +func TestEthTransactionFromSignedMessage_7702_WrongTupleArityRejected(t *testing.T) { + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + var from20 [20]byte + for i := range from20 { + from20[i] = 0x44 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + // Wrong arity: tuple with 5 elements instead of 6 + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 5)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 314)) + var addr [20]byte + for i := range addr { + addr[i] = 0xaa + } + require.NoError(t, cbg.WriteByteArray(&buf, addr[:])) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + + msg := types.Message{To: EthAccountApplyAndCallActorAddr, From: from, Method: abi.MethodNum(MethodHash("ApplyAndCall")), Params: buf.Bytes(), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + _, err = EthTransactionFromSignedFilecoinMessage(smsg) + require.Error(t, err) +} + +func TestEthTransactionFromSignedMessage_7702_BadAddressLengthRejected(t *testing.T) { + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + var from20 [20]byte + for i := range from20 { + from20[i] = 0x55 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + // Tuple with wrong address byte string length (19 instead of 20) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 6)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 314)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajByteString, 19)) + _, _ = buf.Write(make([]byte, 19)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + + msg := types.Message{To: EthAccountApplyAndCallActorAddr, From: from, Method: abi.MethodNum(MethodHash("ApplyAndCall")), Params: buf.Bytes(), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + _, err = EthTransactionFromSignedFilecoinMessage(smsg) + require.Error(t, err) +} + +func TestEthTransactionFromSignedMessage_7702_EmptyAuthListRejected(t *testing.T) { + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + var from20 [20]byte + for i := range from20 { + from20[i] = 0x66 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + + var buf bytes.Buffer + // Top-level [ list, call ] but with empty auth list (list length 0) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 0)) + // Minimal call tuple [to(20), value, input] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.WriteByteArray(&buf, make([]byte, 20))) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + + msg := types.Message{To: EthAccountApplyAndCallActorAddr, From: from, Method: abi.MethodNum(MethodHash("ApplyAndCall")), Params: buf.Bytes(), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + _, err = EthTransactionFromSignedFilecoinMessage(smsg) + require.Error(t, err) +} + +func TestEthTransactionFromSignedMessage_7702_BadYParityTypeRejected(t *testing.T) { + id18, _ := address.NewIDAddress(18) + EthAccountApplyAndCallActorAddr = id18 + var from20 [20]byte + for i := range from20 { + from20[i] = 0x77 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + // Tuple with wrong y_parity major (byte string instead of unsigned int) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 6)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 314)) + var addr [20]byte + for i := range addr { + addr[i] = 0xaa + } + require.NoError(t, cbg.WriteByteArray(&buf, addr[:])) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) // nonce ok + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajByteString, 1)) // y_parity wrong major + _, _ = buf.Write([]byte{0x00}) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + + msg := types.Message{To: EthAccountApplyAndCallActorAddr, From: from, Method: abi.MethodNum(MethodHash("ApplyAndCall")), Params: buf.Bytes(), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + _, err = EthTransactionFromSignedFilecoinMessage(smsg) + require.Error(t, err) +} diff --git a/chain/types/ethtypes/eth_7702_magic.go b/chain/types/ethtypes/eth_7702_magic.go new file mode 100644 index 00000000000..5bbc5a3e629 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_magic.go @@ -0,0 +1,65 @@ +package ethtypes + +import ( + "encoding/binary" + + "golang.org/x/crypto/sha3" +) + +// EIP-7702 authorization tuple signature domain separator. +// Inner tuple signatures must be over keccak256(0x05 || rlp([chain_id, address, nonce])). +const SetCodeAuthorizationMagic byte = 0x05 + +// EIP-7702 delegated bytecode indicator prefix and version. +// Code written to the authority account must be 0xef 0x01 0x00 || <20-byte address>. +const ( + Eip7702BytecodeMagicHi byte = 0xEF + Eip7702BytecodeMagicLo byte = 0x01 + Eip7702BytecodeVersion byte = 0x00 +) + +// AuthorizationPreimage constructs the exact byte sequence that must be signed +// for an authorization tuple: 0x05 || rlp([chain_id, address, nonce]). +// The returned slice is a freshly allocated buffer. +func AuthorizationPreimage(chainID uint64, address EthAddress, nonce uint64) ([]byte, error) { + // RLP-encode [chain_id, address, nonce] + ci, err := formatUint64(chainID) + if err != nil { + return nil, err + } + ni, err := formatUint64(nonce) + if err != nil { + return nil, err + } + rl, err := EncodeRLP([]interface{}{ci, address[:], ni}) + if err != nil { + return nil, err + } + // Prefix with magic 0x05 + out := make([]byte, 1+len(rl)) + out[0] = SetCodeAuthorizationMagic + copy(out[1:], rl) + return out, nil +} + +// AuthorizationKeccak returns keccak256(AuthorizationPreimage(...)). +func AuthorizationKeccak(chainID uint64, address EthAddress, nonce uint64) ([32]byte, error) { + pre, err := AuthorizationPreimage(chainID, address, nonce) + if err != nil { + return [32]byte{}, err + } + h := sha3.NewLegacyKeccak256() + _, _ = h.Write(pre) + var sum [32]byte + copy(sum[:], h.Sum(nil)) + return sum, nil +} + +// MethodHash computes the FRC-42 4-byte method number for the given name by +// taking the first 4 bytes of keccak256(name) as a big-endian uint32. +func MethodHash(name string) uint64 { + h := sha3.NewLegacyKeccak256() + _, _ = h.Write([]byte(name)) + sum := h.Sum(nil) + return uint64(binary.BigEndian.Uint32(sum[:4])) +} diff --git a/chain/types/ethtypes/eth_7702_magic_test.go b/chain/types/ethtypes/eth_7702_magic_test.go new file mode 100644 index 00000000000..05c775456cc --- /dev/null +++ b/chain/types/ethtypes/eth_7702_magic_test.go @@ -0,0 +1,64 @@ +package ethtypes + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/crypto/sha3" +) + +func TestAuthorizationPreimage_Shape(t *testing.T) { + var addr EthAddress + for i := range addr { + addr[i] = 0x11 + } + + pre, err := AuthorizationPreimage(1, addr, 0) + require.NoError(t, err) + require.Greater(t, len(pre), 1) + // First byte must be the MAGIC 0x05 + require.EqualValues(t, SetCodeAuthorizationMagic, pre[0]) + + // Decode the RLP tail and check tuple contents + dec, err := DecodeRLP(pre[1:]) + require.NoError(t, err) + lst, ok := dec.([]interface{}) + require.True(t, ok) + require.Len(t, lst, 3) + + ciU, err := parseUint64(lst[0]) + require.NoError(t, err) + require.Equal(t, uint64(1), ciU) + + gotAddr, err := parseEthAddr(lst[1]) + require.NoError(t, err) + require.NotNil(t, gotAddr) + require.Equal(t, addr, *gotAddr) + + nnU, err := parseUint64(lst[2]) + require.NoError(t, err) + require.Equal(t, uint64(0), nnU) +} + +func TestAuthorizationKeccak_DifferentFromWrongDomain(t *testing.T) { + var addr EthAddress + for i := range addr { + addr[i] = 0x22 + } + + good, err := AuthorizationKeccak(314, addr, 7) + require.NoError(t, err) + + // Compute a bad preimage with wrong domain prefix (0x00) and ensure hashes differ. + // This guards against accidentally omitting the MAGIC byte. + ci, _ := formatUint64(314) + ni, _ := formatUint64(7) + rl, _ := EncodeRLP([]interface{}{ci, addr[:], ni}) + badPre := append([]byte{0x00}, rl...) + hh := sha3.NewLegacyKeccak256() + _, _ = hh.Write(badPre) + var bad [32]byte + copy(bad[:], hh.Sum(nil)) + + require.NotEqual(t, good, bad) +} diff --git a/chain/types/ethtypes/eth_7702_params.go b/chain/types/ethtypes/eth_7702_params.go new file mode 100644 index 00000000000..71e8bab45bd --- /dev/null +++ b/chain/types/ethtypes/eth_7702_params.go @@ -0,0 +1,138 @@ +package ethtypes + +import ( + "bytes" + + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/filecoin-project/go-state-types/big" +) + +// CborEncodeEIP7702Authorizations encodes the authorizationList into CBOR +// compatible with the EthAccount.ApplyAndCall / historical Delegator.ApplyDelegations params. +// Shape: a wrapper tuple with a single field `list`, where `list` is an +// array of 6-tuples [chain_id, address(20b), nonce, y_parity, r, s]. +// I.e., top-level is an array with one element (the inner list). +// Intended for params to the EthAccount actor's ApplyAndCall method (and matches the older Delegator.ApplyDelegations shape). +func CborEncodeEIP7702Authorizations(list []EthAuthorization) ([]byte, error) { + var buf bytes.Buffer + // Write wrapper array with 1 element (the list) + if err := cbg.CborWriteHeader(&buf, cbg.MajArray, 1); err != nil { + return nil, err + } + // Write inner list header + if err := cbg.CborWriteHeader(&buf, cbg.MajArray, uint64(len(list))); err != nil { + return nil, err + } + for _, a := range list { + if err := cbg.CborWriteHeader(&buf, cbg.MajArray, 6); err != nil { + return nil, err + } + if err := cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, uint64(a.ChainID)); err != nil { + return nil, err + } + if err := cbg.WriteByteArray(&buf, a.Address[:]); err != nil { + return nil, err + } + if err := cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, uint64(a.Nonce)); err != nil { + return nil, err + } + if err := cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, uint64(a.YParity)); err != nil { + return nil, err + } + rbig := (big.Int)(a.R) + rb, err := rbig.Bytes() + if err != nil { + return nil, err + } + if err := cbg.WriteByteArray(&buf, rb); err != nil { + return nil, err + } + sbig := (big.Int)(a.S) + sb, err := sbig.Bytes() + if err != nil { + return nil, err + } + if err := cbg.WriteByteArray(&buf, sb); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +// CborEncodeEIP7702ApplyAndCall encodes atomic apply+call params as a CBOR +// array with two elements: +// +// [ [ tuple, ... ], [ to(20b), value(big), input(bytes) ] ] +// +// The first element is the inner list of 6-tuples (wrapper form). +// The second element carries the outer call information. +func CborEncodeEIP7702ApplyAndCall(list []EthAuthorization, to *EthAddress, value big.Int, input []byte) ([]byte, error) { + var buf bytes.Buffer + // Top-level array with 2 elements + if err := cbg.CborWriteHeader(&buf, cbg.MajArray, 2); err != nil { + return nil, err + } + // Element 0: inner list of authorization tuples + if err := cbg.CborWriteHeader(&buf, cbg.MajArray, uint64(len(list))); err != nil { + return nil, err + } + for _, a := range list { + if err := cbg.CborWriteHeader(&buf, cbg.MajArray, 6); err != nil { + return nil, err + } + if err := cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, uint64(a.ChainID)); err != nil { + return nil, err + } + if err := cbg.WriteByteArray(&buf, a.Address[:]); err != nil { + return nil, err + } + if err := cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, uint64(a.Nonce)); err != nil { + return nil, err + } + if err := cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, uint64(a.YParity)); err != nil { + return nil, err + } + rbig := (big.Int)(a.R) + rb, err := rbig.Bytes() + if err != nil { + return nil, err + } + if err := cbg.WriteByteArray(&buf, rb); err != nil { + return nil, err + } + sbig := (big.Int)(a.S) + sb, err := sbig.Bytes() + if err != nil { + return nil, err + } + if err := cbg.WriteByteArray(&buf, sb); err != nil { + return nil, err + } + } + // Element 1: call tuple [to(20b), value(big), input(bytes)] + if err := cbg.CborWriteHeader(&buf, cbg.MajArray, 3); err != nil { + return nil, err + } + // to + var to20 [20]byte + if to != nil { + to20 = *to + } + if err := cbg.WriteByteArray(&buf, to20[:]); err != nil { + return nil, err + } + // value + vb, err := value.Bytes() + if err != nil { + return nil, err + } + if err := cbg.WriteByteArray(&buf, vb); err != nil { + return nil, err + } + // input + if err := cbg.WriteByteArray(&buf, input); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/chain/types/ethtypes/eth_7702_params_test.go b/chain/types/ethtypes/eth_7702_params_test.go new file mode 100644 index 00000000000..9d818becfa0 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_params_test.go @@ -0,0 +1,105 @@ +package ethtypes + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/filecoin-project/go-state-types/big" +) + +func TestCborEncodeEIP7702Authorizations_Shape(t *testing.T) { + var addr1, addr2 EthAddress + copy(addr1[:], mustHex(t, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + copy(addr2[:], mustHex(t, "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + + list := []EthAuthorization{ + {ChainID: 1, Address: addr1, Nonce: 7, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(2))}, + {ChainID: 1, Address: addr2, Nonce: 8, YParity: 1, R: EthBigInt(big.NewInt(3)), S: EthBigInt(big.NewInt(4))}, + } + + enc, err := CborEncodeEIP7702Authorizations(list) + require.NoError(t, err) + require.NotEmpty(t, enc) + + r := cbg.NewCborReader(bytes.NewReader(enc)) + // top-level wrapper array with single element + maj, l, err := r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajArray), maj) + require.Equal(t, uint64(1), l) + + // inner list with 2 tuples + maj, l, err = r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajArray), maj) + require.Equal(t, uint64(2), l) + + for i := 0; i < 2; i++ { + maj, l, err := r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajArray), maj) + require.Equal(t, uint64(6), l) + + // chain_id + maj, v, err := r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajUnsignedInt), maj) + require.Equal(t, uint64(1), v) + // address bytes + maj, v, err = r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajByteString), maj) + require.Equal(t, uint64(20), v) + buf := make([]byte, v) + _, err = r.Read(buf) + require.NoError(t, err) + // nonce + maj, v, err = r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajUnsignedInt), maj) + if i == 0 { + require.Equal(t, uint64(7), v) + } else { + require.Equal(t, uint64(8), v) + } + // y_parity + maj, v, err = r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajUnsignedInt), maj) + require.True(t, v == 0 || v == 1) + // r bytes + maj, v, err = r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajByteString), maj) + bufR := make([]byte, v) + _, err = r.Read(bufR) + require.NoError(t, err) + require.GreaterOrEqual(t, int(v), 1) + // s bytes + maj, v, err = r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajByteString), maj) + bufS := make([]byte, v) + _, err = r.Read(bufS) + require.NoError(t, err) + require.GreaterOrEqual(t, int(v), 1) + } +} + +func TestCborEncodeEIP7702Authorizations_EmptyList_Shape(t *testing.T) { + // Even with empty list, encoder produces wrapper [ list ] with length 0 + enc, err := CborEncodeEIP7702Authorizations(nil) + require.NoError(t, err) + r := cbg.NewCborReader(bytes.NewReader(enc)) + maj, l, err := r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajArray), maj) + require.Equal(t, uint64(1), l) + maj, l, err = r.ReadHeader() + require.NoError(t, err) + require.Equal(t, byte(cbg.MajArray), maj) + require.Equal(t, uint64(0), l) +} diff --git a/chain/types/ethtypes/eth_7702_rlp_canonical_test.go b/chain/types/ethtypes/eth_7702_rlp_canonical_test.go new file mode 100644 index 00000000000..49d41bfe78d --- /dev/null +++ b/chain/types/ethtypes/eth_7702_rlp_canonical_test.go @@ -0,0 +1,78 @@ +package ethtypes + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/big" +) + +// Helper builds a minimal 0x04 tx using manual RLP items to inject a non-canonical integer +// encoding (leading zero) for the specified tuple index. +func buildTxWithLeadingZeroInt(t *testing.T, tupleIndex int) []byte { + t.Helper() + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + var authAddr EthAddress + copy(authAddr[:], mustHex(t, "0x2222222222222222222222222222222222222222")) + + // Outer fields (typed transaction body without the type prefix) + chainId, _ := formatInt(1) + nonce, _ := formatInt(1) + maxPrio, _ := formatBigInt(big.NewInt(1)) + maxFee, _ := formatBigInt(big.NewInt(1)) + gasLimit, _ := formatInt(21000) + value, _ := formatBigInt(big.NewInt(0)) + input := []byte{} + + // Authorization tuple elements; default canonical encodings + ai, _ := formatInt(1) + ni, _ := formatInt(0) + yp, _ := formatInt(0) + ri, _ := formatBigInt(big.NewInt(1)) + si, _ := formatBigInt(big.NewInt(1)) + + // Replace target field with a non-canonical encoding: add a leading 0x00 byte. + leadingZero := func(b []byte) []byte { return append([]byte{0x00}, b...) } + switch tupleIndex { + case 0: + ai = leadingZero(ai) + case 2: + ni = leadingZero(ni) + case 3: + yp = leadingZero(yp) + default: + t.Fatalf("unsupported tuple index: %d", tupleIndex) + } + + // Construct list + base := []interface{}{ + chainId, + nonce, + maxPrio, + maxFee, + gasLimit, + formatEthAddr(&to), + value, + input, + []interface{}{}, // accessList + []interface{}{[]interface{}{ai, authAddr[:], ni, yp, ri, si}}, + } + sig, _ := packSigFields(big.NewInt(0), big.NewInt(1), big.NewInt(1)) + payload, err := EncodeRLP(append(base, sig...)) + require.NoError(t, err) + return append([]byte{EIP7702TxType}, payload...) +} + +func TestEIP7702_RLPLeadingZero_ChainIDRejected(t *testing.T) { + enc := buildTxWithLeadingZeroInt(t, 0) + _, err := parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_RLPLeadingZero_YParityRejected(t *testing.T) { + enc := buildTxWithLeadingZeroInt(t, 3) + _, err := parseEip7702Tx(enc) + require.Error(t, err) +} diff --git a/chain/types/ethtypes/eth_7702_rlp_fuzz_test.go b/chain/types/ethtypes/eth_7702_rlp_fuzz_test.go new file mode 100644 index 00000000000..60386e35e9f --- /dev/null +++ b/chain/types/ethtypes/eth_7702_rlp_fuzz_test.go @@ -0,0 +1,27 @@ +package ethtypes + +import ( + "testing" +) + +// FuzzParseEthTransaction_7702 exercises the 0x04 typed transaction RLP parser with arbitrary +// inputs. This is an opt-in fuzz harness; it does not run during normal `go test` but can be used +// locally via `go test -fuzz=Fuzz -run=^$ ./chain/types/ethtypes`. +func FuzzParseEthTransaction_7702(f *testing.F) { + // Seed a few minimal cases. + f.Add([]byte{0x04}) // just the type prefix + f.Add([]byte{0x04, 0xC0}) // type + empty list + f.Add([]byte{0x04, 0x80}) // type + empty string + f.Add([]byte{0x04, 0xE0, 0x00}) // type + long list header + f.Add([]byte{0x04, 0xF8, 0x01, 0x00}) // type + long bytes header + + f.Fuzz(func(t *testing.T, data []byte) { + // We only care about parser stability: no panics. We ignore errors. + _ = func() (err error) { + defer func() { _ = recover() }() + // Attempt to parse; ignore result/error. + _, _ = ParseEthTransaction(data) + return nil + }() + }) +} diff --git a/chain/types/ethtypes/eth_7702_rlp_limit_test.go b/chain/types/ethtypes/eth_7702_rlp_limit_test.go new file mode 100644 index 00000000000..5ea76bd39c8 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_rlp_limit_test.go @@ -0,0 +1,54 @@ +package ethtypes + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/big" +) + +// Verifies that the 0x04 parser uses a per-type RLP element limit (13) and +// rejects lists that are too long, while the EIP-1559 parser remains +// unaffected and expects exactly 12 elements. +func TestEIP7702_RLP_OuterListTooLongRejected(t *testing.T) { + // Build a valid-looking 7702 payload first. + chainId, _ := formatInt(1) + nonce, _ := formatInt(1) + maxPrio, _ := formatBigInt(big.NewInt(1)) + maxFee, _ := formatBigInt(big.NewInt(1)) + gasLimit, _ := formatInt(21000) + value, _ := formatBigInt(big.NewInt(0)) + input := []byte{} + + // Minimal valid authorization list with one 6-tuple. + var to EthAddress + ai, _ := formatInt(1) + ni, _ := formatInt(0) + yp, _ := formatInt(0) + ri, _ := formatBigInt(big.NewInt(1)) + si, _ := formatBigInt(big.NewInt(1)) + authList := []interface{}{[]interface{}{ai, to[:], ni, yp, ri, si}} + + // Base 10 fields (pre-accesslist + access list + auth list) + base := []interface{}{chainId, nonce, maxPrio, maxFee, gasLimit, formatEthAddr(&to), value, input, []interface{}{}, authList} + + // Append signature fields (v, r, s) + sig, _ := packSigFields(big.NewInt(0), big.NewInt(1), big.NewInt(1)) + payload := append(base, sig...) + + // Sanity: this should parse as a proper 7702 transaction + encOk, err := EncodeRLP(payload) + require.NoError(t, err) + _, err = parseEip7702Tx(append([]byte{EIP7702TxType}, encOk...)) + require.NoError(t, err) + + // Now make the outer list too long by appending an extra empty byte string. + payloadTooLong := append(payload, []byte{}) + encBad, err := EncodeRLP(payloadTooLong) + require.NoError(t, err) + + // Expect the 7702 parser to reject the 14-element list. + _, err = parseEip7702Tx(append([]byte{EIP7702TxType}, encBad...)) + require.Error(t, err) +} diff --git a/chain/types/ethtypes/eth_7702_signature_test.go b/chain/types/ethtypes/eth_7702_signature_test.go new file mode 100644 index 00000000000..eb5c719dcda --- /dev/null +++ b/chain/types/ethtypes/eth_7702_signature_test.go @@ -0,0 +1,40 @@ +package ethtypes + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/big" + typescrypto "github.com/filecoin-project/go-state-types/crypto" +) + +func TestEIP7702_SignaturePacking(t *testing.T) { + var to EthAddress + tx := &Eth7702TxArgs{ + ChainID: 1, + Nonce: 0, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{{ChainID: 1, Address: to, Nonce: 0, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}}, + V: big.NewInt(1), + R: big.NewInt(1), + S: big.NewInt(2), + } + sig, err := tx.Signature() + require.NoError(t, err) + require.Equal(t, typescrypto.SigTypeDelegated, sig.Type) + require.Len(t, sig.Data, 65) + // r at index 31 must be 0x01; s at index 63 must be 0x02; v at index 64 must be 0x01 + require.EqualValues(t, 0x01, sig.Data[31]) + require.EqualValues(t, 0x02, sig.Data[63]) + require.EqualValues(t, 0x01, sig.Data[64]) + + // ToVerifiableSignature is a no-op for v-parity signatures + vsig, err := tx.ToVerifiableSignature(sig.Data) + require.NoError(t, err) + require.Equal(t, sig.Data, vsig) +} diff --git a/chain/types/ethtypes/eth_7702_transactions.go b/chain/types/ethtypes/eth_7702_transactions.go new file mode 100644 index 00000000000..6c20a53fa35 --- /dev/null +++ b/chain/types/ethtypes/eth_7702_transactions.go @@ -0,0 +1,461 @@ +package ethtypes + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + abi "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + typescrypto "github.com/filecoin-project/go-state-types/crypto" + + "github.com/filecoin-project/lotus/build/buildconstants" + "github.com/filecoin-project/lotus/chain/types" +) + +// EthAuthorization mirrors the 6-field tuple specified by EIP-7702: +// +// [chain_id, address, nonce, y_parity, r, s] +// +// Reference: https://eips.ethereum.org/EIPS/eip-7702 +type EthAuthorization struct { + ChainID EthUint64 `json:"chainId"` + Address EthAddress `json:"address"` + Nonce EthUint64 `json:"nonce"` + YParity uint8 `json:"yParity"` + R EthBigInt `json:"r"` + S EthBigInt `json:"s"` +} + +// DomainHash computes keccak256(0x05 || rlp([chain_id, address, nonce])) for this +// authorization tuple, as specified by EIP-7702. The chain ID used is the tuple's +// own chainId field (which may be 0 to indicate wildcard) and not the outer tx chain. +func (a EthAuthorization) DomainHash() ([32]byte, error) { + return AuthorizationKeccak(uint64(a.ChainID), a.Address, uint64(a.Nonce)) +} + +// ---------- EIP-7702 Transaction (type 0x04) ---------- +// +// rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, +// +// gas_limit, destination, value, data, access_list, +// authorization_list, signature_y_parity, signature_r, signature_s]) +// +// NOTE: access_list is currently required to be an empty list in Lotus' +// EIP-1559 parser; we follow that pattern here for now. +var _ EthTransaction = (*Eth7702TxArgs)(nil) + +type Eth7702TxArgs struct { + ChainID int `json:"chainId"` + Nonce int `json:"nonce"` + To *EthAddress `json:"to"` + Value big.Int `json:"value"` + MaxFeePerGas big.Int `json:"maxFeePerGas"` + MaxPriorityFeePerGas big.Int `json:"maxPriorityFeePerGas"` + GasLimit int `json:"gasLimit"` + Input []byte `json:"input"` + AuthorizationList []EthAuthorization `json:"authorizationList"` + + // Outer signature (same style as EIP-1559): v(=y_parity), r, s + V big.Int `json:"v"` + R big.Int `json:"r"` + S big.Int `json:"s"` +} + +// ----- EthTransaction interface impls ----- + +func (tx *Eth7702TxArgs) Type() int { return EIP7702TxType } + +func (tx *Eth7702TxArgs) ToRlpUnsignedMsg() ([]byte, error) { + encoded, err := toRlpUnsignedMsg(tx) + if err != nil { + return nil, err + } + return append([]byte{EIP7702TxType}, encoded...), nil +} + +func (tx *Eth7702TxArgs) ToRlpSignedMsg() ([]byte, error) { + encoded, err := toRlpSignedMsg(tx, tx.V, tx.R, tx.S) + if err != nil { + return nil, err + } + return append([]byte{EIP7702TxType}, encoded...), nil +} + +func (tx *Eth7702TxArgs) TxHash() (EthHash, error) { + rlp, err := tx.ToRlpSignedMsg() + if err != nil { + return EmptyEthHash, err + } + return EthHashFromTxBytes(rlp), nil +} + +func (tx *Eth7702TxArgs) Signature() (*typescrypto.Signature, error) { + r := tx.R.Int.Bytes() + s := tx.S.Int.Bytes() + v := tx.V.Int.Bytes() // should be 0 or 1 + sig := append([]byte{}, padLeadingZeros(r, 32)...) + sig = append(sig, padLeadingZeros(s, 32)...) + if len(v) == 0 { + sig = append(sig, 0) + } else { + sig = append(sig, v[0]) + } + if len(sig) != 65 { + return nil, xerrors.Errorf("signature is not 65 bytes") + } + return &typescrypto.Signature{ + Type: typescrypto.SigTypeDelegated, + Data: sig, + }, nil +} + +func (tx *Eth7702TxArgs) ToVerifiableSignature(sig []byte) ([]byte, error) { + // For typed tx with y_parity in v, the 65-byte signature is already in verifiable form. + return sig, nil +} + +func (tx *Eth7702TxArgs) Sender() (address.Address, error) { return sender(tx) } + +// ToUnsignedFilecoinMessage delegates to ToUnsignedFilecoinMessageAtomic. +// Until the EthAccount/FVM support is fully landed, this path may return an error +// if the EthAccount.ApplyAndCall integration is not enabled. +func (tx *Eth7702TxArgs) ToUnsignedFilecoinMessage(from address.Address) (*types.Message, error) { + return tx.ToUnsignedFilecoinMessageAtomic(from) +} + +// ToUnsignedFilecoinMessageAtomic builds a Filecoin message that calls the +// EthAccount actor ApplyAndCall method with atomic apply+call semantics, encoding both +// the authorization list and the outer call (to/value/input) in params. +func (tx *Eth7702TxArgs) ToUnsignedFilecoinMessageAtomic(from address.Address) (*types.Message, error) { + if tx.ChainID != buildconstants.Eip155ChainId { + return nil, fmt.Errorf("invalid chain id: %d", tx.ChainID) + } + if !Eip7702FeatureEnabled { + return nil, fmt.Errorf("EIP-7702 not yet wired to actors/FVM; parsed OK but cannot construct Filecoin message (enable actor integration to proceed)") + } + // CBOR encode [ [tuples...], [to(20b), value, input] ] + var to EthAddress + if tx.To != nil { + to = *tx.To + } + params, err := CborEncodeEIP7702ApplyAndCall(tx.AuthorizationList, &to, tx.Value, tx.Input) + if err != nil { + return nil, xerrors.Errorf("failed to CBOR-encode apply+call params: %w", err) + } + if EthAccountApplyAndCallActorAddr == address.Undef { + return nil, fmt.Errorf("EIP-7702 feature enabled but EthAccountApplyAndCallActorAddr is undefined; set ethtypes.EthAccountApplyAndCallActorAddr at init") + } + method := abi.MethodNum(MethodHash("ApplyAndCall")) + return &types.Message{ + Version: 0, + To: EthAccountApplyAndCallActorAddr, + From: from, + Nonce: uint64(tx.Nonce), + Value: types.NewInt(0), + GasLimit: int64(tx.GasLimit), + GasFeeCap: tx.MaxFeePerGas, + GasPremium: tx.MaxPriorityFeePerGas, + Method: method, + Params: params, + }, nil +} + +func (tx *Eth7702TxArgs) ToEthTx(smsg *types.SignedMessage) (EthTx, error) { + from, err := EthAddressFromFilecoinAddress(smsg.Message.From) + if err != nil { + return EthTx{}, xerrors.Errorf("sender was not an eth account") + } + hash, err := tx.TxHash() + if err != nil { + return EthTx{}, err + } + gasFeeCap := EthBigInt(tx.MaxFeePerGas) + gasPremium := EthBigInt(tx.MaxPriorityFeePerGas) + ethTx := EthTx{ + ChainID: EthUint64(buildconstants.Eip155ChainId), + Type: EIP7702TxType, + Nonce: EthUint64(tx.Nonce), + Hash: hash, + To: tx.To, + Value: EthBigInt(tx.Value), + Input: tx.Input, + Gas: EthUint64(tx.GasLimit), + MaxFeePerGas: &gasFeeCap, + MaxPriorityFeePerGas: &gasPremium, + From: from, + R: EthBigInt(tx.R), + S: EthBigInt(tx.S), + V: EthBigInt(tx.V), + AuthorizationList: tx.AuthorizationList, + } + return ethTx, nil +} + +func (tx *Eth7702TxArgs) InitialiseSignature(sig typescrypto.Signature) error { + if sig.Type != typescrypto.SigTypeDelegated { + return xerrors.Errorf("RecoverSignature only supports Delegated signature") + } + if len(sig.Data) != 65 { + return xerrors.Errorf("signature should be 65 bytes long, but got %d bytes", len(sig.Data)) + } + r_, err := parseBigInt(sig.Data[0:32]) + if err != nil { + return xerrors.Errorf("cannot parse r into EthBigInt") + } + s_, err := parseBigInt(sig.Data[32:64]) + if err != nil { + return xerrors.Errorf("cannot parse s into EthBigInt") + } + v_, err := parseBigInt([]byte{sig.Data[64]}) + if err != nil { + return xerrors.Errorf("cannot parse v into EthBigInt") + } + tx.R = r_ + tx.S = s_ + tx.V = v_ + return nil +} + +// ----- RLP packing / parsing ----- + +// Matches Eth1559 packer pattern, but inserts authorizationList before the signature fields. +func (tx *Eth7702TxArgs) packTxFields() ([]interface{}, error) { + chainId, err := formatInt(tx.ChainID) + if err != nil { + return nil, err + } + nonce, err := formatInt(tx.Nonce) + if err != nil { + return nil, err + } + maxPrio, err := formatBigInt(tx.MaxPriorityFeePerGas) + if err != nil { + return nil, err + } + maxFee, err := formatBigInt(tx.MaxFeePerGas) + if err != nil { + return nil, err + } + gasLimit, err := formatInt(tx.GasLimit) + if err != nil { + return nil, err + } + value, err := formatBigInt(tx.Value) + if err != nil { + return nil, err + } + authList, err := packAuthorizationList(tx.AuthorizationList) + if err != nil { + return nil, err + } + res := []interface{}{ + chainId, + nonce, + maxPrio, + maxFee, + gasLimit, + formatEthAddr(tx.To), + value, + tx.Input, + []interface{}{}, // accessList (not yet supported; keep empty) + authList, // authorizationList (list of 6-tuples) + } + return res, nil +} + +func packAuthorizationList(list []EthAuthorization) ([]interface{}, error) { + out := make([]interface{}, 0, len(list)) + for _, a := range list { + ci, err := formatInt(int(a.ChainID)) + if err != nil { + return nil, err + } + ni, err := formatInt(int(a.Nonce)) + if err != nil { + return nil, err + } + ri, err := formatBigInt(big.Int(a.R)) + if err != nil { + return nil, err + } + si, err := formatBigInt(big.Int(a.S)) + if err != nil { + return nil, err + } + // y_parity is encoded as an integer (0/1) + yp, err := formatInt(int(a.YParity)) + if err != nil { + return nil, err + } + out = append(out, []interface{}{ + ci, + a.Address[:], + ni, + yp, + ri, + si, + }) + } + return out, nil +} + +// parseEip7702Tx decodes a type-0x04 (EIP-7702) transaction. +func parseEip7702Tx(data []byte) (*Eth7702TxArgs, error) { + if data[0] != EIP7702TxType { + return nil, xerrors.Errorf("not an EIP-7702 transaction: first byte is not %d", EIP7702TxType) + } + d, err := DecodeRLPWithLimit(data[1:], 13) + if err != nil { + return nil, err + } + decoded, ok := d.([]interface{}) + if !ok { + return nil, xerrors.Errorf("not an EIP-7702 transaction: decoded data is not a list") + } + // 13 elements: 9 outer pre-accesslist fields, accessList, authorizationList, v, r, s + if len(decoded) != 13 { + return nil, xerrors.Errorf("not an EIP-7702 transaction: should have 13 elements in the rlp list") + } + + chainId, err := parseInt(decoded[0]) + if err != nil { + return nil, err + } + nonce, err := parseInt(decoded[1]) + if err != nil { + return nil, err + } + maxPriorityFeePerGas, err := parseBigInt(decoded[2]) + if err != nil { + return nil, err + } + maxFeePerGas, err := parseBigInt(decoded[3]) + if err != nil { + return nil, err + } + gasLimit, err := parseInt(decoded[4]) + if err != nil { + return nil, err + } + to, err := parseEthAddr(decoded[5]) + if err != nil { + return nil, err + } + value, err := parseBigInt(decoded[6]) + if err != nil { + return nil, err + } + input, err := parseBytes(decoded[7]) + if err != nil { + return nil, err + } + // Access list: keep consistent with Lotus' EIP-1559 implementation for now (must be empty) + if al, ok := decoded[8].([]interface{}); !ok || (ok && len(al) != 0) { + return nil, xerrors.Errorf("access list should be an empty list") + } + + // Authorization list + authsIface, ok := decoded[9].([]interface{}) + if !ok { + return nil, xerrors.Errorf("authorizationList is not a list") + } + if len(authsIface) == 0 { + return nil, xerrors.Errorf("authorizationList must be non-empty") + } + auths := make([]EthAuthorization, 0, len(authsIface)) + // helper to enforce canonical unsigned int encoding (no leading zero if length > 1) + parseUintCanonical := func(v interface{}) (uint64, error) { + b, ok := v.([]byte) + if !ok { + return 0, xerrors.Errorf("not an RLP byte string") + } + // Reject non-canonical encodings: single 0x00 and any leading zero in multi-byte. + if (len(b) == 1 && b[0] == 0x00) || (len(b) > 1 && b[0] == 0x00) { + return 0, xerrors.Errorf("non-canonical integer encoding (leading zero)") + } + return parseUint64(v) + } + + for i, it := range authsIface { + t, ok := it.([]interface{}) + if !ok || len(t) != 6 { + return nil, xerrors.Errorf("authorization[%d]: not a 6-field tuple", i) + } + ai, err := parseUintCanonical(t[0]) + if err != nil { + return nil, xerrors.Errorf("authorization[%d]: bad chainId: %w", i, err) + } + addr, err := parseEthAddr(t[1]) + if err != nil { + return nil, xerrors.Errorf("authorization[%d]: bad address: %w", i, err) + } + ni, err := parseUintCanonical(t[2]) + if err != nil { + return nil, xerrors.Errorf("authorization[%d]: bad nonce: %w", i, err) + } + yp, err := parseUintCanonical(t[3]) + if err != nil { + return nil, xerrors.Errorf("authorization[%d]: bad y_parity: %w", i, err) + } + if yp != 0 && yp != 1 { + return nil, xerrors.Errorf("authorization[%d]: y_parity must be 0 or 1", i) + } + r, err := parseBigInt(t[4]) + if err != nil { + return nil, xerrors.Errorf("authorization[%d]: bad r: %w", i, err) + } + s, err := parseBigInt(t[5]) + if err != nil { + return nil, xerrors.Errorf("authorization[%d]: bad s: %w", i, err) + } + + auths = append(auths, EthAuthorization{ + ChainID: EthUint64(ai), + Address: func() EthAddress { + if addr == nil { + return EthAddress{} + } + return *addr + }(), + Nonce: EthUint64(ni), + YParity: uint8(yp), + R: EthBigInt(r), + S: EthBigInt(s), + }) + } + + v, err := parseBigInt(decoded[10]) + if err != nil { + return nil, err + } + r, err := parseBigInt(decoded[11]) + if err != nil { + return nil, err + } + s, err := parseBigInt(decoded[12]) + if err != nil { + return nil, err + } + // EIP-1559/2930 style v (y_parity) must be 0 or 1; same for 7702 + if !v.Equals(big.NewInt(0)) && !v.Equals(big.NewInt(1)) { + return nil, xerrors.Errorf("EIP-7702 transactions only support 0 or 1 for v (y_parity)") + } + + args := Eth7702TxArgs{ + ChainID: chainId, + Nonce: nonce, + To: to, + Value: value, + Input: input, + MaxFeePerGas: maxFeePerGas, + MaxPriorityFeePerGas: maxPriorityFeePerGas, + GasLimit: gasLimit, + AuthorizationList: auths, + V: v, + R: r, + S: s, + } + return &args, nil +} diff --git a/chain/types/ethtypes/eth_7702_transactions_env_enabled_test.go b/chain/types/ethtypes/eth_7702_transactions_env_enabled_test.go new file mode 100644 index 00000000000..92cd4f973cd --- /dev/null +++ b/chain/types/ethtypes/eth_7702_transactions_env_enabled_test.go @@ -0,0 +1,41 @@ +//go:build eip7702_enabled + +package ethtypes + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/big" + + "github.com/filecoin-project/lotus/build/buildconstants" +) + +func TestEIP7702_ToUnsignedFilecoinMessage_EthAccountReceiver(t *testing.T) { + // Configure an EthAccount.ApplyAndCall receiver directly and ensure the message targets it. + id999, _ := address.NewIDAddress(999) + EthAccountApplyAndCallActorAddr = id999 + + var to EthAddress + tx := &Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 0, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{{ChainID: EthUint64(buildconstants.Eip155ChainId), Address: to, Nonce: 0, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}}, + V: big.NewInt(0), + R: big.NewInt(1), + S: big.NewInt(1), + } + fromFC, err := (EthAddress{}).ToFilecoinAddress() + require.NoError(t, err) + msg, err := tx.ToUnsignedFilecoinMessage(fromFC) + require.NoError(t, err) + require.Equal(t, EthAccountApplyAndCallActorAddr, msg.To) + require.EqualValues(t, MethodHash("ApplyAndCall"), msg.Method) +} diff --git a/chain/types/ethtypes/eth_7702_transactions_test.go b/chain/types/ethtypes/eth_7702_transactions_test.go new file mode 100644 index 00000000000..fae220bd8be --- /dev/null +++ b/chain/types/ethtypes/eth_7702_transactions_test.go @@ -0,0 +1,582 @@ +package ethtypes + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/big" + typescrypto "github.com/filecoin-project/go-state-types/crypto" + + "github.com/filecoin-project/lotus/build/buildconstants" + ltypes "github.com/filecoin-project/lotus/chain/types" +) + +func mustHex(t *testing.T, s string) []byte { + t.Helper() + s = remove0x(s) + b, err := hex.DecodeString(s) + require.NoError(t, err) + return b +} + +func remove0x(s string) string { + if len(s) >= 2 && (s[0:2] == "0x" || s[0:2] == "0X") { + return s[2:] + } + return s +} + +func TestEIP7702_RLPRoundTrip(t *testing.T) { + // Build a small, valid-looking EIP-7702 transaction with one authorization tuple. + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + + var authAddr EthAddress + copy(authAddr[:], mustHex(t, "0x2222222222222222222222222222222222222222")) + + tx := &Eth7702TxArgs{ + ChainID: 1, + Nonce: 5, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1000000000), + MaxPriorityFeePerGas: big.NewInt(100000000), + GasLimit: 21000, + Input: mustHex(t, "0xdeadbeef"), + AuthorizationList: []EthAuthorization{ + { + ChainID: EthUint64(1), + Address: authAddr, + Nonce: EthUint64(7), + YParity: 0, + R: EthBigInt(big.NewInt(1)), + S: EthBigInt(big.NewInt(2)), + }, + }, + V: big.NewInt(1), + R: big.NewInt(3), + S: big.NewInt(4), + } + + // Encode to signed RLP (includes type 0x04 prefix) + enc, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + require.Greater(t, len(enc), 1) + require.Equal(t, byte(EIP7702TxType), enc[0]) + + // Parse back + dec, err := parseEip7702Tx(enc) + require.NoError(t, err) + + // Spot-check fields + require.Equal(t, tx.ChainID, dec.ChainID) + require.Equal(t, tx.Nonce, dec.Nonce) + require.Equal(t, tx.GasLimit, dec.GasLimit) + require.Equal(t, tx.To, dec.To) + require.True(t, tx.Value.Equals(dec.Value)) + require.Equal(t, tx.Input, dec.Input) + require.True(t, tx.MaxFeePerGas.Equals(dec.MaxFeePerGas)) + require.True(t, tx.MaxPriorityFeePerGas.Equals(dec.MaxPriorityFeePerGas)) + require.Equal(t, 1, len(dec.AuthorizationList)) + require.Equal(t, tx.AuthorizationList[0].ChainID, dec.AuthorizationList[0].ChainID) + require.Equal(t, tx.AuthorizationList[0].Address, dec.AuthorizationList[0].Address) + require.Equal(t, tx.AuthorizationList[0].Nonce, dec.AuthorizationList[0].Nonce) + require.Equal(t, tx.AuthorizationList[0].YParity, dec.AuthorizationList[0].YParity) + require.Equal(t, tx.AuthorizationList[0].R.String(), dec.AuthorizationList[0].R.String()) + require.Equal(t, tx.AuthorizationList[0].S.String(), dec.AuthorizationList[0].S.String()) + require.Equal(t, tx.V.String(), dec.V.String()) + require.Equal(t, tx.R.String(), dec.R.String()) + require.Equal(t, tx.S.String(), dec.S.String()) + + // Re-encode parsed tx and compare bytes exactly + enc2, err := dec.ToRlpSignedMsg() + require.NoError(t, err) + require.Equal(t, enc, enc2) +} + +func TestEIP7702_ToEthTx_CarriesAuthorizationList(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + tx := &Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 42, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: EthUint64(buildconstants.Eip155ChainId), Address: to, Nonce: EthUint64(7), YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(2))}, + }, + V: big.NewInt(0), R: big.NewInt(1), S: big.NewInt(1), + } + // Fake signed message to pass From address + fromFC, err := (EthAddress{}).ToFilecoinAddress() + require.NoError(t, err) + sm := <ypes.SignedMessage{Message: ltypes.Message{From: fromFC}} + ethTx, err := tx.ToEthTx(sm) + require.NoError(t, err) + require.Equal(t, 1, len(ethTx.AuthorizationList)) +} + +func TestEIP7702_ToRlpUnsigned_HasTypePrefix(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + tx := &Eth7702TxArgs{ + ChainID: 1, + Nonce: 0, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: 1, Address: to, Nonce: 0, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}, + }, + } + enc, err := tx.ToRlpUnsignedMsg() + require.NoError(t, err) + require.Greater(t, len(enc), 1) + require.Equal(t, byte(EIP7702TxType), enc[0]) +} + +func TestEIP7702_TxHash_VariesByAuthList(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + base := Eth7702TxArgs{ + ChainID: 1, + Nonce: 0, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + V: big.NewInt(0), R: big.NewInt(1), S: big.NewInt(1), + } + tx1 := base + tx1.AuthorizationList = []EthAuthorization{{ChainID: 1, Address: to, Nonce: 0, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}} + h1, err := tx1.TxHash() + require.NoError(t, err) + + tx2 := base + tx2.AuthorizationList = []EthAuthorization{{ChainID: 1, Address: to, Nonce: 1, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}} + h2, err := tx2.TxHash() + require.NoError(t, err) + require.NotEqual(t, h1, h2) +} + +func TestEIP7702_ToUnsignedFilecoinMessage_InvalidChain(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + tx := &Eth7702TxArgs{ + ChainID: 9999, // invalid + Nonce: 0, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: 1, Address: to, Nonce: 0, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}, + }, + V: big.NewInt(0), R: big.NewInt(1), S: big.NewInt(1), + } + _, err := tx.ToUnsignedFilecoinMessage(address.Undef) + require.Error(t, err) +} + +func TestEIP7702_RLPMultiAuth_RoundTrip(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + var a1, a2 EthAddress + copy(a1[:], mustHex(t, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + copy(a2[:], mustHex(t, "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + + tx := &Eth7702TxArgs{ + ChainID: 1, + Nonce: 1, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: 1, Address: a1, Nonce: 0, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(2))}, + {ChainID: 1, Address: a2, Nonce: 1, YParity: 1, R: EthBigInt(big.NewInt(3)), S: EthBigInt(big.NewInt(4))}, + }, + V: big.NewInt(0), R: big.NewInt(1), S: big.NewInt(1), + } + enc, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + dec, err := parseEip7702Tx(enc) + require.NoError(t, err) + require.Len(t, dec.AuthorizationList, 2) + require.Equal(t, a1, dec.AuthorizationList[0].Address) + require.Equal(t, a2, dec.AuthorizationList[1].Address) +} + +func TestEIP7702_EmptyAuthorizationListRejected(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + + tx := &Eth7702TxArgs{ + ChainID: 1, + Nonce: 5, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + Input: nil, + AuthorizationList: nil, // empty + V: big.NewInt(0), + R: big.NewInt(1), + S: big.NewInt(1), + } + + enc, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + + _, err = parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_NonEmptyAccessListRejected(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + var authAddr EthAddress + copy(authAddr[:], mustHex(t, "0x2222222222222222222222222222222222222222")) + + // Build fields manually to inject non-empty access list at index 8 + chainId, _ := formatInt(1) + nonce, _ := formatInt(5) + maxPrio, _ := formatBigInt(big.NewInt(100)) + maxFee, _ := formatBigInt(big.NewInt(200)) + gasLimit, _ := formatInt(21000) + value, _ := formatBigInt(big.NewInt(0)) + input := []byte{0xde, 0xad} + + // Authorization tuple + ai, _ := formatInt(1) + ni, _ := formatInt(7) + yp, _ := formatInt(0) + ri, _ := formatBigInt(big.NewInt(1)) + si, _ := formatBigInt(big.NewInt(2)) + authTuple := []interface{}{ai, authAddr[:], ni, yp, ri, si} + authList := []interface{}{authTuple} + + // Non-empty access list (one dummy element) + accessList := []interface{}{[]byte{0x01}} + + base := []interface{}{ + chainId, + nonce, + maxPrio, + maxFee, + gasLimit, + formatEthAddr(&to), + value, + input, + accessList, // should trigger error + authList, + } + + // Append signature fields + sig, _ := packSigFields(big.NewInt(1), big.NewInt(3), big.NewInt(4)) + full := append(base, sig...) + + payload, err := EncodeRLP(full) + require.NoError(t, err) + enc := append([]byte{EIP7702TxType}, payload...) + + _, err = parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_BadAuthorizationAddressLengthRejected(t *testing.T) { + // Build 0x04 tx with an authorization tuple whose address is 19 bytes + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + chainId, _ := formatInt(1) + nonce, _ := formatInt(1) + maxPrio, _ := formatBigInt(big.NewInt(1)) + maxFee, _ := formatBigInt(big.NewInt(1)) + gasLimit, _ := formatInt(21000) + value, _ := formatBigInt(big.NewInt(0)) + input := []byte{} + ai, _ := formatInt(1) + ni, _ := formatInt(0) + yp, _ := formatInt(0) + ri, _ := formatBigInt(big.NewInt(1)) + si, _ := formatBigInt(big.NewInt(1)) + badAddr := make([]byte, 19) + // Tuple with 19-byte address + authTuple := []interface{}{ai, badAddr, ni, yp, ri, si} + base := []interface{}{ + chainId, nonce, maxPrio, maxFee, gasLimit, formatEthAddr(&to), value, input, + []interface{}{}, // access list + []interface{}{authTuple}, + } + sig, _ := packSigFields(big.NewInt(0), big.NewInt(1), big.NewInt(1)) + payload, _ := EncodeRLP(append(base, sig...)) + enc := append([]byte{EIP7702TxType}, payload...) + _, err := parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_BadOuterLenRejected(t *testing.T) { + // Construct an RLP list with only 12 elements (missing one), should fail + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + chainId, _ := formatInt(1) + nonce, _ := formatInt(1) + maxPrio, _ := formatBigInt(big.NewInt(1)) + maxFee, _ := formatBigInt(big.NewInt(1)) + gasLimit, _ := formatInt(21000) + value, _ := formatBigInt(big.NewInt(0)) + input := []byte{} + // Authorization list with one valid-looking tuple + var authAddr EthAddress + copy(authAddr[:], mustHex(t, "0x2222222222222222222222222222222222222222")) + ai, _ := formatInt(1) + ni, _ := formatInt(0) + yp, _ := formatInt(0) + ri, _ := formatBigInt(big.NewInt(1)) + si, _ := formatBigInt(big.NewInt(1)) + authList := []interface{}{[]interface{}{ai, authAddr[:], ni, yp, ri, si}} + // Build only 12 elements (omit s for outer signature later so list size is wrong) + base := []interface{}{chainId, nonce, maxPrio, maxFee, gasLimit, formatEthAddr(&to), value, input, []interface{}{}, authList, []byte{0x00}, []byte{0x01}} + payload, _ := EncodeRLP(base) + enc := append([]byte{EIP7702TxType}, payload...) + _, err := parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_AuthorizationListMustBeList(t *testing.T) { + // Insert a non-list at authorizationList position + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + chainId, _ := formatInt(1) + nonce, _ := formatInt(1) + maxPrio, _ := formatBigInt(big.NewInt(1)) + maxFee, _ := formatBigInt(big.NewInt(1)) + gasLimit, _ := formatInt(21000) + value, _ := formatBigInt(big.NewInt(0)) + input := []byte{} + base := []interface{}{ + chainId, nonce, maxPrio, maxFee, gasLimit, formatEthAddr(&to), value, input, + []interface{}{}, // access list (empty) + []byte{0x01, 0x02, 0x03}, // not a list + } + sig, _ := packSigFields(big.NewInt(0), big.NewInt(1), big.NewInt(1)) + payload, _ := EncodeRLP(append(base, sig...)) + enc := append([]byte{EIP7702TxType}, payload...) + _, err := parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_VParityRejected(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + + tx := &Eth7702TxArgs{ + ChainID: 1, + Nonce: 1, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: EthUint64(1), Address: to, Nonce: EthUint64(1), YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}, + }, + V: big.NewInt(2), // invalid + R: big.NewInt(1), + S: big.NewInt(1), + } + enc, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + _, err = parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_AuthorizationYParityRejected(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + tx := &Eth7702TxArgs{ + ChainID: 1, + Nonce: 1, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: EthUint64(1), Address: to, Nonce: EthUint64(1), YParity: 2, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}, + }, + V: big.NewInt(0), R: big.NewInt(1), S: big.NewInt(1), + } + enc, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + _, err = parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_ToUnsignedFilecoinMessage_Guard(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + tx := &Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 0, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: EthUint64(buildconstants.Eip155ChainId), Address: to, Nonce: EthUint64(0), YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}, + }, + V: big.NewInt(0), + R: big.NewInt(1), + S: big.NewInt(1), + } + _, err := tx.ToUnsignedFilecoinMessage(address.Undef) + require.Error(t, err) +} + +// Feature-gated test for EthAccount.ApplyAndCall receiver is in eth_7702_transactions_env_enabled_test.go + +func TestEIP7702_AuthorizationTupleWrongArityRejected(t *testing.T) { + // Build fields manually and inject an authorization tuple with only 5 items + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + var authAddr EthAddress + copy(authAddr[:], mustHex(t, "0x2222222222222222222222222222222222222222")) + + chainId, _ := formatInt(1) + nonce, _ := formatInt(5) + maxPrio, _ := formatBigInt(big.NewInt(100)) + maxFee, _ := formatBigInt(big.NewInt(200)) + gasLimit, _ := formatInt(21000) + value, _ := formatBigInt(big.NewInt(0)) + input := []byte{} + + ai, _ := formatInt(1) + ni, _ := formatInt(7) + yp, _ := formatInt(0) + ri, _ := formatBigInt(big.NewInt(1)) + // si omitted on purpose + badTuple := []interface{}{ai, authAddr[:], ni, yp, ri} + authList := []interface{}{badTuple} + + base := []interface{}{ + chainId, + nonce, + maxPrio, + maxFee, + gasLimit, + formatEthAddr(&to), + value, + input, + []interface{}{}, // access list + authList, + } + sig, _ := packSigFields(big.NewInt(0), big.NewInt(1), big.NewInt(1)) + payload, _ := EncodeRLP(append(base, sig...)) + enc := append([]byte{EIP7702TxType}, payload...) + + _, err := parseEip7702Tx(enc) + require.Error(t, err) +} + +func TestEIP7702_InitialiseSignature_SetsVRandS(t *testing.T) { + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + tx := &Eth7702TxArgs{ + ChainID: 1, + Nonce: 1, + To: &to, + Value: big.NewInt(0), + MaxFeePerGas: big.NewInt(1), + MaxPriorityFeePerGas: big.NewInt(1), + GasLimit: 21000, + AuthorizationList: []EthAuthorization{ + {ChainID: 1, Address: to, Nonce: 0, YParity: 0, R: EthBigInt(big.NewInt(1)), S: EthBigInt(big.NewInt(1))}, + }, + } + // Build a 65-byte signature r||s||v with r=2, s=3, v=1 + sig := make([]byte, 65) + sig[31] = 2 // r + sig[63] = 3 // s + sig[64] = 1 // v (y_parity) + require.NoError(t, tx.InitialiseSignature(typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: sig})) + require.Equal(t, "2", tx.R.String()) + require.Equal(t, "3", tx.S.String()) + require.Equal(t, "1", tx.V.String()) +} + +func TestEIP7702_AuthorizationTuple_Uint64BoundaryDecode(t *testing.T) { + // Build fields manually and inject authorization tuple with chainId and nonce near MaxUint64. + var to EthAddress + copy(to[:], mustHex(t, "0x1111111111111111111111111111111111111111")) + + chainId, _ := formatInt(1) + nonce, _ := formatInt(1) + maxPrio, _ := formatBigInt(big.NewInt(1)) + maxFee, _ := formatBigInt(big.NewInt(1)) + gasLimit, _ := formatInt(21000) + value, _ := formatBigInt(big.NewInt(0)) + input := []byte{} + + // Inner tuple chainId/nonce set to max uint64 values + ai, _ := formatUint64(^uint64(0)) + ni, _ := formatUint64(^uint64(0)) + yp, _ := formatInt(0) + ri, _ := formatBigInt(big.NewInt(1)) + si, _ := formatBigInt(big.NewInt(1)) + var authAddr EthAddress + copy(authAddr[:], mustHex(t, "0x2222222222222222222222222222222222222222")) + authTuple := []interface{}{ai, authAddr[:], ni, yp, ri, si} + authList := []interface{}{authTuple} + + base := []interface{}{ + chainId, + nonce, + maxPrio, + maxFee, + gasLimit, + formatEthAddr(&to), + value, + input, + []interface{}{}, // access list + authList, + } + sig, _ := packSigFields(big.NewInt(0), big.NewInt(1), big.NewInt(1)) + payload, _ := EncodeRLP(append(base, sig...)) + enc := append([]byte{EIP7702TxType}, payload...) + + dec, err := parseEip7702Tx(enc) + require.NoError(t, err) + require.Len(t, dec.AuthorizationList, 1) + require.Equal(t, EthUint64(^uint64(0)), dec.AuthorizationList[0].ChainID) + require.Equal(t, EthUint64(^uint64(0)), dec.AuthorizationList[0].Nonce) +} + +func TestEIP7702_InitialiseSignature_WrongTypeRejected(t *testing.T) { + var to EthAddress + tx := &Eth7702TxArgs{To: &to} + // SECP256K1 type should be rejected for 7702 + err := tx.InitialiseSignature(typescrypto.Signature{Type: typescrypto.SigTypeSecp256k1, Data: make([]byte, 65)}) + require.Error(t, err) +} + +func TestEIP7702_InitialiseSignature_WrongLenRejected(t *testing.T) { + var to EthAddress + tx := &Eth7702TxArgs{To: &to} + // Delegated but wrong length (<65) + err := tx.InitialiseSignature(typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: make([]byte, 64)}) + require.Error(t, err) +} diff --git a/chain/types/ethtypes/eth_call_unmarshal_test.go b/chain/types/ethtypes/eth_call_unmarshal_test.go new file mode 100644 index 00000000000..83808f4c37c --- /dev/null +++ b/chain/types/ethtypes/eth_call_unmarshal_test.go @@ -0,0 +1,27 @@ +package ethtypes + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEthCall_Unmarshal_InputPreferredOverData(t *testing.T) { + // Both input and data present; input should be preferred + js := `{"from":"0x0000000000000000000000000000000000000000","to":"0x1111111111111111111111111111111111111111","data":"0xdeadbeef","input":"0xbeadfeed"}` + var c EthCall + require.NoError(t, json.Unmarshal([]byte(js), &c)) + exp, err := DecodeHexString("0xbeadfeed") + require.NoError(t, err) + require.Equal(t, EthBytes(exp), c.Data) +} + +func TestEthCall_Unmarshal_DataFallback(t *testing.T) { + js := `{"from":"0x0000000000000000000000000000000000000000","to":"0x1111111111111111111111111111111111111111","data":"0xdeadbeef"}` + var c EthCall + require.NoError(t, json.Unmarshal([]byte(js), &c)) + exp, err := DecodeHexString("0xdeadbeef") + require.NoError(t, err) + require.Equal(t, EthBytes(exp), c.Data) +} diff --git a/chain/types/ethtypes/eth_transactions.go b/chain/types/ethtypes/eth_transactions.go index e147a3530c9..752c5a08a35 100644 --- a/chain/types/ethtypes/eth_transactions.go +++ b/chain/types/ethtypes/eth_transactions.go @@ -24,6 +24,9 @@ import ( const ( EthLegacyTxType = 0x00 EIP1559TxType = 0x02 + // EIP-7702 typed transaction: Set Code for EOAs (authorization list) + // https://eips.ethereum.org/EIPS/eip-7702 + EIP7702TxType = 0x04 ) const ( @@ -85,6 +88,8 @@ type EthTx struct { V EthBigInt `json:"v"` R EthBigInt `json:"r"` S EthBigInt `json:"s"` + // Present only for EIP-7702 (type 0x04) transactions + AuthorizationList []EthAuthorization `json:"authorizationList,omitempty"` } func (tx *EthTx) GasFeeCap() (EthBigInt, error) { @@ -125,6 +130,29 @@ func EthTransactionFromSignedFilecoinMessage(smsg *types.SignedMessage) (EthTran return nil, fmt.Errorf("sender was not an eth account") } + // Special-case: EthAccount.ApplyAndCall -> reconstruct a 0x04 tx view + if smsg.Message.Method == abi.MethodNum(MethodHash("ApplyAndCall")) { + if authz, err := strictDecodeApplyAndCallAuthorizations(smsg.Message.Params); err == nil && len(authz) > 0 { + tx := &Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: int(smsg.Message.Nonce), + To: nil, + Value: smsg.Message.Value, + MaxFeePerGas: smsg.Message.GasFeeCap, + MaxPriorityFeePerGas: smsg.Message.GasPremium, + GasLimit: int(smsg.Message.GasLimit), + Input: nil, + AuthorizationList: authz, + } + if err := tx.InitialiseSignature(smsg.Signature); err != nil { + return nil, fmt.Errorf("failed to initialise signature: %w", err) + } + return tx, nil + } + } + + // Delegator route removed; routing is now via EthAccount.ApplyAndCall + VM intercept. + // Extract Ethereum parameters and recipient from the message. params, to, err := getEthParamsAndRecipient(&smsg.Message) if err != nil { @@ -187,13 +215,95 @@ func EthTransactionFromSignedFilecoinMessage(smsg *types.SignedMessage) (EthTran } } +// strictDecodeApplyAndCallAuthorizations decodes the canonical ApplyAndCall params and returns +// the authorization list. It enforces: +// - top-level array length >= 1 +// - first element is an array of 6-field tuples with exact field kinds +// Errors out early on malformed shapes. +func strictDecodeApplyAndCallAuthorizations(params []byte) ([]EthAuthorization, error) { + r := cbg.NewCborReader(bytes.NewReader(params)) + maj, l, err := r.ReadHeader() + if err != nil { + return nil, err + } + if maj != cbg.MajArray || l < 1 { + return nil, fmt.Errorf("applyandcall params must be array with >=1 elements") + } + maj0, l0, err := r.ReadHeader() + if err != nil { + return nil, err + } + if maj0 != cbg.MajArray { + return nil, fmt.Errorf("authorizations must be array") + } + tmp := make([]EthAuthorization, 0, l0) + for i := 0; i < int(l0); i++ { + majT, tlen, err := r.ReadHeader() + if err != nil || majT != cbg.MajArray || tlen != 6 { + return nil, fmt.Errorf("authorization[%d]: not a 6-field tuple", i) + } + majF, v, err := r.ReadHeader() + if err != nil || majF != cbg.MajUnsignedInt { + return nil, fmt.Errorf("auth[%d]: bad chainId", i) + } + majF, blen, err := r.ReadHeader() + if err != nil || majF != cbg.MajByteString || blen != 20 { + return nil, fmt.Errorf("auth[%d]: bad address", i) + } + var ea EthAddress + if _, err := r.Read(ea[:]); err != nil { + return nil, fmt.Errorf("auth[%d]: bad address bytes", i) + } + majF, nv, err := r.ReadHeader() + if err != nil || majF != cbg.MajUnsignedInt { + return nil, fmt.Errorf("auth[%d]: bad nonce", i) + } + majF, yv, err := r.ReadHeader() + if err != nil || majF != cbg.MajUnsignedInt { + return nil, fmt.Errorf("auth[%d]: bad yParity", i) + } + majF, rbl, err := r.ReadHeader() + if err != nil || majF != cbg.MajByteString { + return nil, fmt.Errorf("auth[%d]: bad r", i) + } + rb := make([]byte, rbl) + if _, err := r.Read(rb); err != nil { + return nil, fmt.Errorf("auth[%d]: bad r bytes", i) + } + majF, sbl, err := r.ReadHeader() + if err != nil || majF != cbg.MajByteString { + return nil, fmt.Errorf("auth[%d]: bad s", i) + } + sb := make([]byte, sbl) + if _, err := r.Read(sb); err != nil { + return nil, fmt.Errorf("auth[%d]: bad s bytes", i) + } + tmp = append(tmp, EthAuthorization{ + ChainID: EthUint64(v), + Address: ea, + Nonce: EthUint64(nv), + YParity: uint8(yv), + R: EthBigInt(big.NewFromGo(new(mathbig.Int).SetBytes(rb))), + S: EthBigInt(big.NewFromGo(new(mathbig.Int).SetBytes(sb))), + }) + } + return tmp, nil +} + func ToSignedFilecoinMessage(tx EthTransaction) (*types.SignedMessage, error) { from, err := tx.Sender() if err != nil { return nil, fmt.Errorf("failed to calculate sender: %w", err) } - unsignedMsg, err := tx.ToUnsignedFilecoinMessage(from) + var unsignedMsg *types.Message + switch t := tx.(type) { + case *Eth7702TxArgs: + // Route 0x04 to atomic apply+call params + unsignedMsg, err = t.ToUnsignedFilecoinMessageAtomic(from) + default: + unsignedMsg, err = tx.ToUnsignedFilecoinMessage(from) + } if err != nil { return nil, fmt.Errorf("failed to convert to unsigned msg: %w", err) } @@ -221,6 +331,9 @@ func ParseEthTransaction(data []byte) (EthTransaction, error) { case EIP1559TxType: // EIP-1559 return parseEip1559Tx(data) + case EIP7702TxType: + // EIP-7702 (type 0x04) + return parseEip7702Tx(data) default: if data[0] > 0x7f { tx, err := parseLegacyTx(data) @@ -322,6 +435,15 @@ func formatInt(val int) ([]byte, error) { return removeLeadingZeros(buf.Bytes()), nil } +// formatUint64 encodes a uint64 as big-endian bytes without leading zeros (RLP-compatible). +func formatUint64(val uint64) ([]byte, error) { + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.BigEndian, val); err != nil { + return nil, err + } + return removeLeadingZeros(buf.Bytes()), nil +} + func formatEthAddr(addr *EthAddress) []byte { if addr == nil { return nil @@ -356,6 +478,26 @@ func parseInt(v interface{}) (int, error) { return int(value), nil } +// parseUint64 parses a big-endian encoded unsigned integer up to 8 bytes. +func parseUint64(v interface{}) (uint64, error) { + data, ok := v.([]byte) + if !ok { + return 0, fmt.Errorf("cannot parse interface to uint64: input is not a byte array") + } + if len(data) == 0 { + return 0, nil + } + if len(data) > 8 { + return 0, fmt.Errorf("cannot parse interface to uint64: length is more than 8 bytes") + } + var value uint64 + r := bytes.NewReader(append(make([]byte, 8-len(data)), data...)) + if err := binary.Read(r, binary.BigEndian, &value); err != nil { + return 0, fmt.Errorf("cannot parse interface to uint64: %w", err) + } + return value, nil +} + func parseBigInt(v interface{}) (big.Int, error) { data, ok := v.([]byte) if !ok { diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index 4cf80e48bc1..0b617699d35 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -1277,6 +1277,11 @@ type EthTxReceipt struct { LogsBloom EthBytes `json:"logsBloom"` Logs []EthLog `json:"logs"` Type EthUint64 `json:"type"` + // Present only for EIP-7702 transactions. Mirrors transaction view. + AuthorizationList []EthAuthorization `json:"authorizationList,omitempty"` + // Optional: for EIP-7702 ApplyDelegations, lists delegate addresses referenced + // by the authorization tuples. Absent for non-7702 txs and for txs without tuples. + DelegatedTo []EthAddress `json:"delegatedTo,omitempty"` } const errorFunctionSelector = "\x08\xc3\x79\xa0" // Error(string) diff --git a/chain/types/ethtypes/ethtypes.test b/chain/types/ethtypes/ethtypes.test new file mode 100755 index 00000000000..237089d769d Binary files /dev/null and b/chain/types/ethtypes/ethtypes.test differ diff --git a/chain/types/ethtypes/rlp.go b/chain/types/ethtypes/rlp.go index 65801ac45c8..02ef88b6368 100644 --- a/chain/types/ethtypes/rlp.go +++ b/chain/types/ethtypes/rlp.go @@ -8,10 +8,10 @@ import ( "golang.org/x/xerrors" ) -// maxListElements restricts the amount of RLP list elements we'll read. -// The ETH API only ever reads EIP-1559 transactions, which are bounded by -// 12 elements exactly, so we play it safe and set exactly that limit here. -const maxListElements = 12 +// maxListElementsDefault restricts the amount of RLP list elements we'll read +// when no explicit limit is provided by the caller. Keep this aligned with the +// largest non-7702 transaction list size (EIP-1559 uses 12). +const maxListElementsDefault = 12 func EncodeRLP(val interface{}) ([]byte, error) { return encodeRLP(val) @@ -96,8 +96,11 @@ func encodeRLP(val interface{}) ([]byte, error) { } } +// DecodeRLP decodes an RLP-encoded value using the default per-list element +// limit. For callers that need a larger limit (e.g., EIP-7702's 13-element +// transaction lists), use DecodeRLPWithLimit. func DecodeRLP(data []byte) (interface{}, error) { - res, consumed, err := decodeRLP(data) + res, consumed, err := decodeRLPWithLimit(data, maxListElementsDefault) if err != nil { return nil, err } @@ -107,7 +110,20 @@ func DecodeRLP(data []byte) (interface{}, error) { return res, nil } -func decodeRLP(data []byte) (res interface{}, consumed int, err error) { +// DecodeRLPWithLimit decodes an RLP-encoded value with an explicit per-list +// element limit. +func DecodeRLPWithLimit(data []byte, maxListElements int) (interface{}, error) { + res, consumed, err := decodeRLPWithLimit(data, maxListElements) + if err != nil { + return nil, err + } + if consumed != len(data) { + return nil, xerrors.Errorf("invalid rlp data: length %d, consumed %d", len(data), consumed) + } + return res, nil +} + +func decodeRLPWithLimit(data []byte, maxListElements int) (res interface{}, consumed int, err error) { if len(data) == 0 { return data, 0, xerrors.Errorf("invalid rlp data: data cannot be empty") } @@ -120,11 +136,11 @@ func decodeRLP(data []byte) (res interface{}, consumed int, err error) { if 1+listLenInBytes+listLen > len(data) { return nil, 0, xerrors.Errorf("invalid rlp data: out of bound while parsing list") } - result, err := decodeListElems(data[1+listLenInBytes:], listLen) + result, err := decodeListElemsWithLimit(data[1+listLenInBytes:], listLen, maxListElements) return result, 1 + listLenInBytes + listLen, err } else if data[0] >= 0xc0 { length := int(data[0]) - 0xc0 - result, err := decodeListElems(data[1:], length) + result, err := decodeListElemsWithLimit(data[1:], length, maxListElements) return result, 1 + length, err } else if data[0] >= 0xb8 { strLenInBytes := int(data[0]) - 0xb7 @@ -167,12 +183,12 @@ func decodeLength(data []byte, lenInBytes int) (length int, err error) { return int(decodedLength), nil } -func decodeListElems(data []byte, length int) (res []interface{}, err error) { +func decodeListElemsWithLimit(data []byte, length int, maxListElements int) (res []interface{}, err error) { totalConsumed := 0 result := []interface{}{} for i := 0; totalConsumed < length && i < maxListElements; i++ { - elem, consumed, err := decodeRLP(data[totalConsumed:]) + elem, consumed, err := decodeRLPWithLimit(data[totalConsumed:], maxListElements) if err != nil { return nil, xerrors.Errorf("invalid rlp data: cannot decode list element: %w", err) } diff --git a/documentation/changelog/EIP7702.md b/documentation/changelog/EIP7702.md new file mode 100644 index 00000000000..dba287fc10d --- /dev/null +++ b/documentation/changelog/EIP7702.md @@ -0,0 +1,205 @@ +This document synthesizes the provided notes into a comprehensive overview and technical deep dive of the EIP-7702 implementation within the Filecoin network. + +> **Note (EthAccount + VM intercept):** This changelog captures an earlier EVM-actor-centric design where `EVM.ApplyAndCall` and an internal delegation map handled 7702 end-to-end. On this development branch, the live routing is now `EthAccount.ApplyAndCall` plus a VM CALL/EXTCODE* intercept (per `documentation/eip7702_ethaccount_ref-fvm_migration.md` and `AGENTS.md`). Treat the EVM-only/Delegator references below as historical context rather than the current implementation. + +# Filecoin EVM + EIP-7702 (“Delegated EOAs”) — Implementation Overview + +> **Audience:** Filecoin core developers, Lotus node operators, and technically-savvy contributors/auditors. +> **Scope:** This document details the integration of Ethereum’s **EIP-7702** into Filecoin’s EVM, synthesizing the architectural design and the interaction between the Go-based Lotus client and the Rust-based FVM (`builtin-actors`). + +----- + +## 0\. Understanding EIP-7702 in Filecoin + +EIP-7702 introduces a mechanism allowing Externally Owned Accounts (EOAs) to temporarily adopt smart contract functionality. It lets an EOA delegate its execution context to a specified contract by publishing a signed **authorization tuple** via a new transaction type (`0x04`). + +In this Filecoin integration, we implement an EVM‑only architecture: + + * **Persist Delegations (EVM Actor):** Delegation choices and per‑authority nonces are stored inside the EVM actor’s state. + * **Atomic Apply + Call:** A new EVM method ApplyAndCall processes authorization tuples and immediately executes the outer call atomically. + * **Interpreter Semantics:** The EVM interpreter handles delegation at CALL‑to‑EOA time, executing the delegate’s bytecode under the authority’s context; EXTCODE* opcodes expose a short pointer code on the authority account. + * **Client Handling (Lotus):** Lotus exposes the `0x04` typed transaction, parses it, and constructs a Filecoin message invoking EVM.ApplyAndCall with atomic CBOR params. + * **Mempool:** No 7702‑specific ingress policies; standard nonce/fee rules apply. + +This design keeps Ethereum semantics where transactions update account behavior, making those updates visible and enforceable on-chain for subsequent EVM execution. + +----- + +## 1\. Architectural Overview + +The implementation involves coordinated changes across the stack, centered on the EVM actor. + +``` + ┌──────────────────────────────┐ + │ Lotus (Go) │ + │ • Parses 0x04 tx │ +eth_sendRawTransaction │ • Encodes CBOR params │ Mempool + (JSON-RPC) ───────▶ │ • Builds msg → EVM.ApplyAndCall │ + │ • RPC views/receipts │ + └───────────────┬──────────────┘ + │ Filecoin message (ApplyAndCall) + ▼ + ┌─────────────────────────────────────────────┐ + │ EVM Actor — Rust/FVM │ + │ • ApplyAndCall (verify, write, bump, exec) │ + │ • State: delegations (EOA→{delegate,nonce}) │ + │ - Atomic apply+call (rollback on revert) │ + │ • CALL interception │ + │ - If delegated: execute delegate under │ + │ authority context (no cross‑actor hop) │ + │ • EXTCODE*/pointer code semantics │ + │ • Event emission │ + └─────────────────────────────────────────────┘ +``` + +### Activation & Flags + + * **Activation via Bundle:** EIP-7702 functionality ships in the upgrade bundle; no separate runtime network-version gate. + * **Lotus Feature Flag:** The send‑path is controlled by the `eip7702_enabled` build tag for development/testing. + +----- + +## 2\. On-Chain Logic (Rust/FVM) + +### 2.1 EVM Actor (EVM‑Only) + +The EVM actor persists all EIP‑7702 state and enforces tuple validation, nonce tracking, and execution semantics. + +#### State Structure + +Recommended state: + +1. **`delegations`**: Map Authority (EOA `EthAddress`) → `{ delegate: EthAddress, nonce: u64 }`. +2. (Optional) **authority storage roots** if isolating per‑EOA storage; otherwise reuse standard account storage. + +#### Core Methods + + * **`ApplyAndCall` (New method):** Entry point for `0x04` transactions. + * **Validates:** `chain_id ∈ {0, local}`, `y_parity ∈ {0,1}`, non‑zero `r/s`, **low‑s**. + * **Recovers Authority:** `keccak256(0x05 || rlp([chain_id, address, nonce]))` then secp256k1 recovery. + * **Verifies Nonce:** Matches the stored per‑authority delegation nonce. + * **Updates State:** Update `delegations` and bump nonce(s). + * **Executes:** Executes the outer call atomically; on revert, roll back the mapping/nonces. + +> **Why this design?** On-chain state ensures consensus on delegations. Nonces prevent replay attacks. Separate storage roots per EOA allow delegation with strong, account-local semantics, isolating the EOA’s storage from the delegate’s contract state. + +### 2.2 Interpreter Changes + +#### A. CALL Path: Interception and Delegation + +The EVM interpreter's `CALL` instruction logic is modified: + +1. **EOA Detection:** The interpreter checks if the call target is an EOA and if EIP-7702 is active. +2. **Delegation Resolution:** Consult the EVM actor’s internal delegation map. +3. **Delegation Activation:** If a delegate is found: + * The runtime verifies the delegate is an EVM contract and loads its bytecode CID. + * **Value Transfer:** For non-static calls, any value attached is first transferred to the EOA (Authority). + * **Event Emission:** Emit `EIP7702Delegated(address)` for observability. + * **Execution:** Execute the delegate code under the authority context (no cross‑actor trampoline required). +4. **No-Code Behavior:** If the delegate exists but has no bytecode or is non-EVM, the call resolves as a no-op success (`1`), mirroring standard EOA call behavior. + +(If employing per‑EOA storage isolation, the interpreter should mount/unmount the authority storage root internally during delegated execution.) +4. **Storage Persistence:** After execution, if not read-only, the interpreter flushes the storage (`system.flush_storage_root`). No external storage actor is consulted in this branch; state remains internal to the EVM runtime. + +> **Why this design?** The mount/flush mechanism provides EOA-scoped storage longevity across different delegate contracts and messages, while allowing the delegate's code to run unmodified within the EOA's context. + +----- + +## 3\. Client Logic (Go/Lotus) + +### 3.1 Parsing 0x04 Transactions + +Lotus (`ethtypes`) recognizes the `0x04` prefix and decodes the RLP payload. + + * **Outer Envelope:** Includes standard EIP-1559 fields, the new `authorizationList`, and the outer signature (`v`, `r`, `s`). + * **`authorizationList`**: A list of 6-tuples: `[chain_id, address(20 bytes), nonce, y_parity, r (bytes), s (bytes)]`. + +### 3.2 Conversion to Filecoin Message + +The `ToUnsignedFilecoinMessage` method converts the parsed `Eth7702TxArgs` structure into a single Filecoin message. This is gated by the `eip7702_enabled` build tag. + + * `To`: The EVM actor exposing `ApplyAndCall`. + * `Method`: `ApplyAndCall`. + +### 3.3 Gating/Constants and Migration + + * **Activation:** Controlled by the deployed bundle; no runtime gating needed. + * **Constants:** Authorization domain = `0x05`; pointer bytecode = `0xef 0x01`, version `0x00`. + * **Migration/Compatibility:** No migration required; the flow is EVM‑only and atomic‑only, and the Delegator actor has been removed. + * `Params`: Atomic CBOR `[ [ tuple1, tuple2, ... ], [ to(20), value, input ] ]`. + +### 3.3 Mempool + +EIP-7702 transactions are admitted under the standard mempool rules (nonce, fee, size constraints). Lotus does not implement 7702-specific ingress-time policies such as cross-account eviction or per-sender delegation caps. + +### 3.4 RPC Surfaces and Gas Estimation + +#### Gas Estimation + +`EthEstimateGas` is updated to account for EIP-7702 overhead. When estimating a message targeting ApplyAndCall, Lotus parses the CBOR parameters to count the authorization tuples and adds intrinsic overhead (behavioral only until constants finalize). + +#### RPC Responses and Receipts + +To provide compatibility with Ethereum tooling, the `EthTx` and `EthTxReceipt` structures are extended: + +1. **`AuthorizationList`**: Included in both the transaction object and the receipt if the transaction type was `0x04`. +2. **`DelegatedTo`**: A new field added to the receipt to indicate which contracts were involved. Lotus populates this by extracting addresses from the `AuthorizationList` or by scanning execution logs for the `EIP7702Delegated(address)` event emitted by the FVM. + +----- + +## 4\. End-to-End Flows + +### 4.1 Applying Delegations (Type 0x04 Tx) + +1. **Client Submission:** An RPC client submits a `0x04` transaction. +2. **Lotus Parsing/Conversion:** Lotus parses the tx and builds a Filecoin message targeting `EVM.ApplyAndCall`. +3. The message enters the mempool under standard policies (no 7702-specific cap or cross-account invalidation on ingress). +4. **FVM Execution (`ApplyAndCall`):** The EVM actor validates tuples, recovers authorities, verifies nonces, updates mappings, and executes the outer call atomically. + +### 4.2 Calling a Delegated EOA + +1. **EVM CALL:** EVM code executes a `CALL` instruction targeting an EOA address. +2. **Interception:** The EVM runtime intercepts the call and checks the EVM actor’s internal delegation map. +3. **Delegation Found:** If a mapping exists: + 1. Value is transferred to the EOA (Authority) if applicable. + 2. The `EIP7702Delegated(address)` event is emitted. + 3. The interpreter executes the delegate code under the authority context. + 5. The result is returned to the original caller. + +----- + +## 5\. Security and Correctness + + * **Signature Validation:** The EVM actor strictly enforces the **low-s** requirement and rejects zero r/s values. Authority recovery relies on a standard `secp256k1` recovery process over the committed RLP data (0x05 domain), combined with stored delegation nonces for anti-replay. + * **Storage Isolation:** If per‑EOA storage isolation is employed, the interpreter mounts/unmounts the authority storage internally during delegated execution. + * **Reorgs:** Mempool invalidation is a best-effort policy upon ingress. Consensus correctness relies solely on the actor-enforced nonces and mappings at execution time. + +----- + +## 6\. Summary of Changes and Locations + +This implementation required changes across the stack in the following key areas: + + * **EVM Internals (Rust):** + * `actors/evm/*`: ApplyAndCall method, delegation state, CALL pointer semantics, EXTCODE* pointer behavior, optional storage mount. + * **Runtime Wiring (Rust):** + * Network version activation gate. + * **Lotus (Go):** + * `chain/types/ethtypes/eth_7702_transactions.go`: Added support for `0x04` RLP decoding and message construction. + * `chain/types/ethtypes/eth_7702_params.go`: CBOR encoder for `authorizationList` matching actor ABI. + * `chain/types/ethtypes/eth_types.go`: Extended RPC types/receipts to expose `authorizationList` and `DelegatedTo`. + +----- + +## Appendices: Technical Specifications + +### A. Actor Method Numbers and Names + + * **EVM Actor:** + * `ApplyAndCall` (public) — method number tbd. + +### B. Encoding Boundaries + + * **RLP (Ethereum wire)**: `0x04` prefix followed by a 13-element list; `authorizationList` is a list of 6-tuples. + * **CBOR (Actor ABI)**: `ApplyAndCall` parameters use atomic encoding `[ [ tuple... ], [ to(20), value, input ] ]`. + * **Authority Recovery**: Digest = `keccak256(0x05 || rlp([chain_id, address(20), nonce]))`. Recovery uses FVM `recover_secp_public_key`. diff --git a/documentation/eip7702_ethaccount_ref-fvm_migration.md b/documentation/eip7702_ethaccount_ref-fvm_migration.md new file mode 100644 index 00000000000..0da4b29bf1f --- /dev/null +++ b/documentation/eip7702_ethaccount_ref-fvm_migration.md @@ -0,0 +1,316 @@ +# EIP-7702 EthAccount + ref-fvm Migration Plan + +This document defines the current work priority for EIP-7702 across builtin-actors, ref-fvm, and Lotus. It moves delegation state to the EthAccount actor and implements delegation execution semantics in ref-fvm, keeping the EVM interpreter intact. + +## Context + +- Working development branch; no backward-compatibility constraints. +- Delegation mapping and nonces now live per-EOA in EthAccount state; the deprecated EVM-local delegation map has been removed. +- Delegated execution and EXTCODE* pointer semantics are implemented in ref-fvm via a VM intercept; the EVM interpreter no longer follows delegation internally. + +## Goals + +- Keep delegation state per EOA in EthAccount so it is globally visible. +- Maintain delegated execution and pointer code behavior in the VM (ref-fvm) as the single source of truth. +- Preserve end-to-end behavior: atomic ApplyAndCall, depth=1, EXTCODE* projects 23-byte pointer code, Delegated(address) event with authority, revert-data propagation, value-transfer behavior, and behavioral gas model in Lotus. + +## Design + +### State in EthAccount + +- Add to EthAccount state: + - `delegate_to: Option` — current delegate pointer for the EOA. + - `auth_nonce: u64` — per-authority nonce for authorization tuples. + - `evm_storage_root: Cid` — persistent storage root used when executing under authority context. +- These fields replace per-authority maps previously held in the EVM actor. + +### VM (ref-fvm) Delegation Semantics + +- Pointer code projection (EXTCODE*): + - On EXTCODESIZE/HASH/COPY for an EOA with `delegate_to != None`, return a virtual 23-byte code image: `0xEF 0x01 0x00 || `. + - EXTCODEHASH returns keccak(pointer_code), size is 23, copy returns the exact 23 bytes. +- Delegated dispatch for CALL to EOAs (implemented): + - Intercept EVM `InvokeEVM` → EthAccount target when the EthAccount has `delegate_to` set. + - Value transfer: the call manager transfers value to the authority prior to interception; on transfer failure the EVM call reports success=0 via `EVM_CONTRACT_REVERTED`. + - Execution: ref‑fvm invokes the caller EVM actor via a private trampoline `InvokeAsEoaWithRoot` with parameters `(code_cid, input, caller, receiver, value, initial_storage_root)`: + - The EVM actor mounts `initial_storage_root`, sets authority context (depth=1), executes delegate code, and returns `(output_data, new_storage_root)` or reverts with the revert payload. + - Persistence: the call manager persists `new_storage_root` back into the EthAccount state (`evm_storage_root`). Persistence occurs only on success; revert and value‑transfer short‑circuit paths do not update the authority overlay. + - Return mapping: on success, returns `ExitCode::OK` with raw `output_data` for the EVM interpreter; on revert, returns `EVM_CONTRACT_REVERTED` with the revert payload. + - Event: emits `Delegated(address)` (topic keccak("Delegated(address)")) with the authority encoded as a 32‑byte ABI word. Emission is best‑effort. +- Authority context rules: + - Do not re-follow delegation (depth limit enforced by a context flag). + - SELFDESTRUCT is a no-op in authority context (no balance move or tombstone effect). + - Success/revert mapping: delegated subcalls map to standard EVM CALL semantics. On success, return ExitCode::OK and + returndata. On revert, return EVM_CONTRACT_REVERTED with revert payload as data; the EVM interpreter will set + success=0 and propagate returndata via RETURNDATASIZE/RETURNDATACOPY. + +### ApplyAndCall in EthAccount + +- Method `ApplyAndCall` validates tuples and updates state, then invokes a VM syscall to execute the outer call with all gas forwarded. +- Tuple validation (consensus-critical): domain separator 0x05; `yParity in {0,1}`; non-zero R/S; ≤32-byte R/S accepted with left-padding; low-s enforced; nonce equality; tuple cap (64 placeholder). +- Pre-existence check: reject if the authority resolves to an EVM contract actor. +- Receiver-only constraint (current): all tuples must target the receiver authority (authority==receiver). Multi-authority updates will be realized via VM intercept semantics. +- Returns embedded status and return data; mapping and nonce updates persist even if outer call reverts. + +### Interfaces (proposed) + +- VM syscall invoked by EthAccount.ApplyAndCall (pseudo-signature): + - `fn evm_apply_and_call(authority: EthAddress, to: EthAddress, value: TokenAmount, input: Bytes) -> (status: u8, returndata: Bytes)` + - Semantics: forwards all remaining gas to the outer call; performs delegated dispatch/pointer semantics per rules above; returns status and returndata without undoing prior state updates. +- Runtime/Kernel helper for EXTCODE* projection (used by EVM actor): + - `fn get_eth_delegate_to(actor: ActorID) -> Option<[u8; 20]>` (implemented; ref-fvm + SDK) + - Read-only; returns the delegate address if the target is an EthAccount with `delegate_to` set. + - EVM interpreter (ext.rs) consults this helper on Account/EOA targets to decide if pointer code is exposed. +- Helper EOA detection and resolution order: + - Resolve target to ID, load code CID, verify it is the EthAccount builtin actor; only then decode state and read + `delegate_to`. Never return a delegate for EVM actors or non-EOA code. +- Event topic constant: `keccak("Delegated(address)")`; data encoded as a 32-byte ABI word (last 20 bytes form the address). +- Pointer code constants: `MAGIC=0xEF01`, `VERSION=0x00` → 23-byte image: `0xEF 0x01 0x00 || delegate(20)`. +- EthAccount.ApplyAndCall params (CBOR wrapper, canonical): + - `[ [ tuples... ], [ to(20), value(u256), input(bytes) ] ]`. + - Tuple fields validated with domain `0x05` hash preimage for signature checks. + +Implementation notes +- Trampoline: `Evm.InvokeAsEoaWithRoot` (FRC‑42 selector) is added for VM use only. It mounts the provided `initial_storage_root`, runs in authority context, and returns `(output_data, new_storage_root)`. +- EXTCODE* projection: the interpreter consults `get_eth_delegate_to(ActorID)`; pointer code is exposed as `0xEF 0x01 0x00 || delegate(20)`, `EXTCODESIZE=23`, `EXTCODEHASH=keccak(pointer_code)`, and `EXTCODECOPY` enforces windowing/zero‑fill. + +Tests (current) +- Added ref‑fvm EthAccount state (de)serialization roundtrip test to guard state layout. +- Added skeleton tests (ignored) for intercept semantics: EXTCODE* projection, delegated CALL success/revert mapping with revert bytes, depth limit enforcement, value‑transfer short‑circuit, and SELFDESTRUCT no‑op. These will be enabled with a DefaultCallManager harness in a follow‑up. + +### EXTCODE* Semantics Over Pointer Code + +- EXTCODESIZE(authority) returns 23 when `delegate_to` is set, else the authority’s actual code size (typically 0). +- EXTCODEHASH(authority) returns keccak(pointer_code) when delegated, else code hash per normal rules. +- EXTCODECOPY(authority, destOffset, offset, size) copies up to 23 bytes of the virtual pointer code starting at + `offset`, truncates when out of range, and zero-fills the remainder per EVM rules. When `offset >= 23` and `size > 0`, + the copy yields all zeros. + +## ref-fvm Changes (status) + +- Branch: `eip7702`. +- VM features (implemented): + - EXTCODE* pointer projection for EOAs with `delegate_to`. + - Delegated dispatch for CALL to EOAs with authority context + storage overlay + depth limit. + - Event emission for `Delegated(address)` with authority address. + - Syscall invoked by EthAccount.ApplyAndCall to perform the outer call with all gas forwarded and return status/returndata. + - Context plumbing to prevent re-following delegation and to manage storage root mount/restore. +- Tests in ref-fvm (landed): + - Pointer projection (size/hash/copy), delegated dispatch success/revert, depth limit, SELFDESTRUCT no-op, revert-data propagation, value-transfer short-circuit, and `Delegated(address)` event topic/data coverage. + +### Docker Bundle/Test Flow (macOS friendly) + +- Build builtin-actors bundle in Docker: + - From `../builtin-actors`: `make bundle-testing-repro` +- Run ref-fvm tests with a prebuilt bundle: + - From `../ref-fvm`: `scripts/run_eip7702_tests.sh` +- This avoids macOS toolchain issues (e.g., `__stack_chk_fail` Wasm import) when invoking EVM in tests. + +### EXTCODECOPY Windowing Examples + +- For an EOA A delegated to B, pointer code = `0xEF 0x01 0x00 || B(20)`. +- EXTCODECOPY(A, dest, 0, 23) → full 23 bytes. +- EXTCODECOPY(A, dest, 1, 22) → pointer_code[1..23]. +- EXTCODECOPY(A, dest, 23, 1) → single zero. +- EXTCODECOPY(A, dest, 100, 10) → 10 zeros. + +### Repo/Path Impact (ref-fvm) + +- Execution layer: + - EVM call entry path: intercept CALL→EOA; implement authority-context dispatch. + - Code query path: intercept EXTCODE{SIZE,HASH,COPY} for EOAs; provide virtual pointer bytes/hash/size. +- Context plumbing: + - Execution context flag for authority mode; prevent delegation re-follow. + - Storage overlay: mount EthAccount.evm_storage_root and persist updated root on exit. + - EthAccount state mutation policy: ref-fvm is responsible for persisting `evm_storage_root` back to the authority + EthAccount after delegated execution. This is a privileged VM operation; user code cannot mutate this field. + Optionally, implement as an internal EthAccount hook callable only by the VM. + - Address resolution policy: the VM intercept must resolve Ethereum addresses to ActorIDs, then use code CID to + distinguish EthAccount vs EVM actors. Delegated dispatch applies only to EthAccounts (EOAs). +- Events: + - Emit `Delegated(address)` after delegated outer call (best-effort). + +## builtin-actors Changes + +- EthAccount (implemented): + - State extended with `delegate_to`, `auth_nonce`, `evm_storage_root`. + - `ApplyAndCall`: + - Validates tuples and nonces. + - Updates state (delegate_to + auth_nonce; initializes storage root if needed). + - Invokes ref-fvm bridge for the outer call; returns embedded status + return data. +- EVM actor (cleaned up): + - Removes internal delegation/nonces/storage-root structures and the legacy InvokeAsEoa entrypoint. + - Keeps the interpreter intact; does not alter EXTCODE* — the VM supplies pointer behavior. + - Updates ext.rs to consult the runtime helper `get_eth_delegate_to` for Account/EOA targets to project pointer code. +- Tests: + - Global mapping test passes (eoa_pointer_mapping_global.rs). + - Suites for pointer semantics, revert-data, depth limit, pre-existence, R/S padding, tuple cap keep passing through EthAccount + VM path. + +### Repo/Path Impact (builtin-actors) + +- EthAccount actor: + - State struct: add `delegate_to`, `auth_nonce`, `evm_storage_root`. + - Methods: add `ApplyAndCall` entry; tuple validation and state updates. +- EVM actor: + - Remove/stop using: internal delegation/nonces/storage-roots; InvokeAsEoa; any EXTCODE* delegation lookups. + - Interpreter remains; storage and bytecode handling unchanged. + +## Lotus Changes + +- Route type-0x04 transactions to `EthAccount.ApplyAndCall` (canonical CBOR wrapper parameters unchanged). +- Receipts/logs: + - Continue deriving `delegatedTo` from `authorizationList` or the synthetic `Delegated(address)` event. + - Receipt `status` mirrors embedded status returned by `ApplyAndCall`. +- Gas estimation: + - Keep behavioral model: tuple overhead applied when `authorizationList` is non-empty and grows with tuple count; do not pin numeric constants. + +### Repo/Path Impact (Lotus) + +- node/impl/eth: + - Send path: point 0x04 to EthAccount.ApplyAndCall method number. + - Receipts: keep adjuster for `Delegated(address)` topic and `authorizationList` attribution; status from embedded status. +- chain/types/ethtypes: + - No shape changes; keep canonical wrapper encode/decode and `AuthorizationKeccak` tests. + +## Sequencing (high level, completed) + +1. Authoritative plan: land this document and mark as current priority in AGENTS.md. +2. Create ref-fvm branch and scaffold pointer projection, authority context flags, and ApplyAndCall syscall. +3. Implement EthAccount state and `ApplyAndCall`; remove delegation logic from EVM actor. +4. Implement ref-fvm delegated dispatch, storage overlay, event emission, and EXTCODE* projection. +5. Switch Lotus routing for 0x04 to EthAccount.ApplyAndCall; keep receipts/gas behavior. +6. Validate across repos and stabilize tests; then clean dead code paths. + +## Testing Migration Matrix (Coverage) + +This matrix maps existing builtin-actors EVM tests to expected coverage post-migration and adds ref-fvm unit tests where behavior moved to the VM. + +- Pointer semantics + - actors/evm/tests/eoa_call_pointer_semantics.rs → remains; backed by VM EXTCODE* projection. + - ref-fvm: add unit tests for EXTCODESIZE=23, EXTCODECOPY bytes match, EXTCODEHASH(pointer_code). +- EXTCODECOPY windowing + - Add ref-fvm tests that exercise partial and out-of-bounds windows over the 23-byte image, asserting correct truncation + and zero-fill semantics. +- Global mapping visibility + - actors/evm/tests/eoa_pointer_mapping_global.rs → remains; now passes using EthAccount state. + - ref-fvm: add test to verify cross-contract EXTCODE* sees pointer code for any EOA with delegate. +- Delegated CALL behavior + - actors/evm/tests/delegated_call_revert_data.rs → remains; VM must propagate revert data to return buffer + memory. + - actors/evm/tests/apply_and_call_value_transfer.rs → remains; VM short-circuits on failed transfer. +- Event emission under gas tightness + - Add ref-fvm tests that enforce extremely tight gas conditions to verify Delegated(address) emission is best-effort + and absence of the event does not affect outer call status/returndata. +- Atomic ApplyAndCall + - actors/evm/tests/apply_and_call_atomicity_* → port to EthAccount.ApplyAndCall; assert persistence on revert. + - ref-fvm: syscall tests ensure status/returndata returned per VM dispatch. +- Depth limit + - actors/evm/tests/apply_and_call_depth_limit.rs → remains; VM prevents delegation re-follow under authority context. + - Add ref-fvm test for A->B and B->C configured; CALL→A delegates to B only (no follow to C). +- SELFDESTRUCT no-op under authority + - actors/evm/tests/apply_and_call_selfdestruct.rs → remains; VM enforces no-op and state/balance invariants. +- Storage isolation/persistence + - actors/evm/tests/delegated_storage_isolation.rs, delegated_storage_persistence.rs → remain; VM must mount and persist `evm_storage_root` for authority. +- Validation vectors + - actors/evm/tests/apply_and_call_invalids.rs (domain, yParity, zero/over-32 R/S, high-S, nonce mismatch, duplicates, tuple cap) → rewire to EthAccount.ApplyAndCall; behavior unchanged. + - actors/evm/tests/apply_and_call_tuple_roundtrip.rs, apply_and_call_tuple_cap.rs → remain. +- Pre-existence policy + - actors/evm/tests/apply_and_call_delegate_no_code.rs → remains; EthAccount rejects authorities resolving to EVM contracts. +- NV gating (none) + - actors/evm/tests/eoa_invoke_delegation_nv.rs → remains; ensure no runtime gating. +- Nonce initialization + - actors/evm/tests/apply_and_call_nonces.rs → ensure absent authority defaults to nonce=0; applying nonce=0 succeeds; + subsequent nonce=0 fails with mismatch. + - Add a ref-fvm unit test covering EthAccount state read/write round-trips to prevent struct drift. + +Lotus tests (no loss of coverage): + +- chain/types/ethtypes: parsing/encoding, RLP per-type limit, canonical wrapper, AuthorizationKeccak vectors. +- node/impl/eth: receipts attribution from tuples and `Delegated(address)`; gas estimation behavioral tests; status decode from embedded. +- itests (post-bundle): E2E mirrors geth TestEIP7702; applies two delegations, CALL→EOA executes delegate, storage updates under authority, receipt.status reflects outcome, attribution present. + +ref-fvm tests (new): + +- Pointer projection (EXTCODE*); delegated dispatch success and revert with returndata; depth limit; SELFDESTRUCT no-op; failed transfer short-circuit; authority storage overlay mount/persist. + +## Validation + +- builtin-actors: + - `cargo test -p fil_actor_evm` including pointer semantics, delegated revert data, depth limit, storage isolation/persistence, pre-existence, tuple cap, R/S padding. + - Add EthAccount ApplyAndCall tests to cover nonce initialization, duplicates, invalid domain/yParity/high-s/zero R/S, wrong chain id. +- ref-fvm: + - Unit tests for EXTCODE* pointer projection, delegated dispatch, depth limit, SELFDESTRUCT no-op, revert-data propagation, and value-transfer handling. +- Lotus: + - `go test` suites for 7702 parsing/encoding, receipts attribution, behavioral gas estimation. + - E2E once bundles include ref-fvm changes: apply delegations, CALL→EOA executes delegate, storage persists under authority, event emitted, receipts correct. + - Add a receipt test variant that tolerates missing Delegated(address) when gas is extremely tight (best-effort event). + - Add a decode robustness test for revert data in EVM paths to ensure no silent fallback on decode failures (return + illegal_state in invariants instead of discarding data). + +## Execution Checklist (status) + +- ref-fvm (branch `eip7702`) + - [x] EXTCODE* pointer projection for EOAs with delegate + - [x] CALL→EOA delegated dispatch with authority context and depth limit + - [x] Authority storage overlay mount/persist + - [x] `Delegated(address)` event emission + - [x] ApplyAndCall syscall with all-gas-forwarded semantics + - [x] Unit tests for features above +- builtin-actors + - [x] EthAccount: add `delegate_to`, `auth_nonce`, `evm_storage_root` + - [x] EthAccount: implement `ApplyAndCall` validation + state updates + syscall + - [x] Remove delegation maps and InvokeAsEoa from EVM actor + - [x] Adapt tests to EthAccount.ApplyAndCall; keep coverage green +- Lotus + - [x] Route 0x04 to EthAccount.ApplyAndCall + - [x] Receipts attribution and status decode intact + - [x] Behavioral gas estimation unchanged + +## Risks & Mitigations + +- Risk: Pointer semantics regress under VM projection. + - Mitigation: Add ref-fvm EXTCODE* tests and keep existing actor tests. +- Risk: Storage overlay bugs cause state loss/corruption. + - Mitigation: Add persistence/isolation tests; verify root before/after. +- Risk: Revert-data propagation differences between VM and actor path. + - Mitigation: Dedicated tests for RETURNDATASIZE/RETURNDATACOPY semantics. +- Risk: Event emission dropped under gas tightness. + - Mitigation: Document best-effort; ensure outer call status unaffected; test both with/without sufficient gas. +- Risk: Helper drift and EOA misclassification. + - Mitigation: Centralize EthAccount state struct and add ref-fvm round-trip tests; strictly verify code CID for + EthAccount in helper and intercept paths. + +## Out of Scope + +- Numeric gas constants/refunds finalization (Lotus remains behavioral; actor refunds staged separately). +- Mempool policy changes (none required). + +## Acceptance Criteria + +- Delegation state and nonces are per-EOA in EthAccount and persist across transactions. +- EXTCODESIZE/HASH/COPY on delegated EOAs exposes a 23-byte pointer code globally. +- CALL to delegated EOAs executes the delegate’s code in authority context with a depth limit of 1 and authority storage overlay; SELFDESTRUCT no-op. +- `ApplyAndCall` persists mapping + nonces pre-call and returns embedded status + return data; receipts/logs attribute `delegatedTo` as specified; Lotus routes 0x04 to EthAccount.ApplyAndCall. +- Behavioral gas tests in Lotus remain green; no special mempool policies. + +## Ownership & Areas + +- ref-fvm: VM pointer projection, delegated dispatch, authority-context storage overlay, events, ApplyAndCall syscall. +- builtin-actors: EthAccount state + `ApplyAndCall` validation and state updates; remove EVM-actor delegation code. +- Lotus: 0x04 routing to EthAccount.ApplyAndCall; receipts attribution and behavioral gas estimation. + +## Branch Strategy + +- ref-fvm: `eip7702`. +- builtin-actors and Lotus: continue on current `eip7702` branches; no backward-compatibility constraints. + +## Quick Commands + +- builtin-actors: + - `cargo test -p fil_actor_evm` + - `cargo test -p fil_actor_ethaccount` +- ref-fvm: + - `cargo test -p fvm --test eth_delegate_to` +- Lotus: + - `go build ./chain/types/ethtypes` + - `go test ./chain/types/ethtypes -run 7702 -count=1` + - `go test ./node/impl/eth -run 7702 -count=1` diff --git a/documentation/en/api-methods-v1-stable.md b/documentation/en/api-methods-v1-stable.md index 5b7143a2c27..216abe3f5fd 100644 --- a/documentation/en/api-methods-v1-stable.md +++ b/documentation/en/api-methods-v1-stable.md @@ -2159,7 +2159,20 @@ Response: "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ``` @@ -2209,7 +2222,20 @@ Response: "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ``` @@ -2398,7 +2424,17 @@ Response: ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ``` @@ -2438,7 +2474,17 @@ Response: ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ``` @@ -2477,7 +2523,17 @@ Response: ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ``` @@ -2517,7 +2573,17 @@ Response: ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ``` @@ -2595,7 +2661,20 @@ Response: "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ``` @@ -2643,7 +2722,20 @@ Response: "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ``` diff --git a/documentation/en/api-methods-v2-experimental.md b/documentation/en/api-methods-v2-experimental.md index e3d79428fb5..18668ff342b 100644 --- a/documentation/en/api-methods-v2-experimental.md +++ b/documentation/en/api-methods-v2-experimental.md @@ -509,7 +509,20 @@ Response: "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ``` @@ -562,7 +575,20 @@ Response: "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ] ``` @@ -768,7 +794,17 @@ Response: ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ``` @@ -810,7 +846,17 @@ Response: ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ``` @@ -851,7 +897,17 @@ Response: ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ``` @@ -893,7 +949,17 @@ Response: ], "v": "0x0", "r": "0x0", - "s": "0x0" + "s": "0x0", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ] } ``` @@ -979,7 +1045,20 @@ Response: "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ``` @@ -1029,7 +1108,20 @@ Response: "blockNumber": "0x5" } ], - "type": "0x5" + "type": "0x5", + "authorizationList": [ + { + "chainId": "0x5", + "address": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "nonce": "0x5", + "yParity": 7, + "r": "0x0", + "s": "0x0" + } + ], + "delegatedTo": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ] } ``` diff --git a/ethtypes.test b/ethtypes.test new file mode 100755 index 00000000000..b9a5c53e2cb Binary files /dev/null and b/ethtypes.test differ diff --git a/extern/filecoin-ffi b/extern/filecoin-ffi index 3d5f2311117..44f5dc459be 160000 --- a/extern/filecoin-ffi +++ b/extern/filecoin-ffi @@ -1 +1 @@ -Subproject commit 3d5f23111173a8f8449b2aefd8fc7c42acc01362 +Subproject commit 44f5dc459be3b74aec77138c4d3e976324b0d17b diff --git a/fvm/tests/common.rs b/fvm/tests/common.rs new file mode 100644 index 00000000000..186c846ab11 --- /dev/null +++ b/fvm/tests/common.rs @@ -0,0 +1,145 @@ +use anyhow::Result; +use cid::Cid; +use fvm::machine::Manifest; +use fvm_integration_tests::bundle::import_bundle; +use fvm_integration_tests::tester::{BasicAccount, BasicTester, ExecutionOptions, Tester}; +use fvm_ipld_blockstore::{Blockstore, MemoryBlockstore}; +use fvm_ipld_encoding::CborStore; +use fvm_shared::address::Address; +use fvm_shared::econ::TokenAmount; +use fvm_shared::state::StateTreeVersion; +use fvm_shared::version::NetworkVersion; +use multihash_codetable::Code; + +// Embedded actor bundle from builtin-actors (dev-dependency `actors`). +use actors; // fil_builtin_actors_bundle + +// Minimal EthAccount state view mirroring kernel expectations. +#[derive(fvm_ipld_encoding::tuple::Serialize_tuple)] +pub struct EthAccountStateView { + pub delegate_to: Option<[u8; 20]>, + pub auth_nonce: u64, + pub evm_storage_root: Cid, +} + +pub struct Harness { + pub tester: BasicTester, + pub ethaccount_code: Cid, + pub bundle_root: Cid, +} + +pub fn new_harness(options: ExecutionOptions) -> Result { + // Build a blockstore and import the embedded bundle. + let bs = MemoryBlockstore::default(); + let root = import_bundle(&bs, actors::BUNDLE_CAR)?; + // Load manifest to fetch EthAccount code. + let (ver, data_root): (u32, Cid) = bs + .get_cbor(&root)? + .expect("bundle manifest header not found"); + let manifest = Manifest::load(&bs, &data_root, ver)?; + let ethaccount_code = *manifest.get_ethaccount_code(); + + // Initialize a tester with this bundle. + let mut tester = Tester::new(NetworkVersion::V21, StateTreeVersion::V5, root, bs)?; + tester.options = Some(options); + + Ok(Harness { tester, ethaccount_code, bundle_root: root }) +} + +/// Create an EthAccount actor with the given authority delegated f4 address and EVM delegate (20 bytes). +/// Returns the assigned ActorID of the authority account. +pub fn set_ethaccount_with_delegate( + h: &mut Harness, + authority_addr: Address, + delegate20: [u8; 20], +) +-> Result { + // Register the authority address to obtain an ActorID. + let state_tree = h + .tester + .state_tree + .as_mut() + .expect("state tree should be present prior to instantiation"); + let authority_id = state_tree.register_new_address(&authority_addr).unwrap(); + + // Persist minimal EthAccount state. + let view = EthAccountStateView { delegate_to: Some(delegate20), auth_nonce: 0, evm_storage_root: Cid::default() }; + let st_cid = state_tree.store().put_cbor(&view, Code::Blake2b256)?; + + // Install the EthAccount actor state with delegated_address = authority_addr. + let act = fvm::state_tree::ActorState::new(h.ethaccount_code, st_cid, TokenAmount::default(), 0, Some(authority_addr)); + state_tree.set_actor(authority_id, act); + Ok(authority_id) +} + +/// Lookup a code CID by name from the bundle manifest (e.g., "evm"). +pub fn bundle_code_by_name(h: &Harness, name: &str) -> Result> { + let store = h.tester.state_tree.as_ref().unwrap().store(); + // bundle_root encodes (manifest_version, manifest_data_cid) + let (ver, data_root): (u32, Cid) = store.get_cbor(&h.bundle_root)?.expect("bundle header"); + if ver != 1 { return Ok(None); } + let entries: Vec<(String, Cid)> = store.get_cbor(&data_root)?.expect("manifest data"); + Ok(entries.into_iter().find(|(n, _)| n == name).map(|(_, c)| c)) +} + +/// Pre-install an EVM actor at a specific f4 address with provided runtime bytecode. +/// Returns the assigned ActorID. +pub fn install_evm_contract_at( + h: &mut Harness, + evm_addr: Address, + runtime: &[u8], +) -> Result { + use fvm_ipld_blockstore::Block; + use multihash_codetable::Code as MhCode; + + // Resolve EVM actor Wasm code CID from bundle by name. + let evm_code = bundle_code_by_name(h, "evm")?.expect("evm code in bundle"); + + // Put runtime bytecode as a raw IPLD block. + let bs = h.tester.state_tree.as_ref().unwrap().store(); + let bytecode_blk = Block::new(fvm_ipld_encoding::IPLD_RAW, runtime); + let bytecode_cid = bs.put(MhCode::Blake2b256, &bytecode_blk)?; + + // Compute keccak256 hash of runtime for bytecode_hash (32 bytes). + let mut bytecode_hash = [0u8; 32]; + { + use multihash_codetable::MultihashDigest; + let mh = multihash_codetable::Code::Keccak256.digest(runtime); + bytecode_hash.copy_from_slice(mh.digest()); + } + + // Minimal EVM state tuple matching actors/evm State serialization. + #[derive(fvm_ipld_encoding::tuple::Serialize_tuple)] + struct EvmState { + bytecode: Cid, + #[serde(with = "fvm_ipld_encoding::strict_bytes")] + bytecode_hash: [u8; 32], + contract_state: Cid, + transient_data: Option<()>, + nonce: u64, + tombstone: Option<()>, + delegations: Option, + delegation_nonces: Option, + delegation_storage: Option, + } + + let st = EvmState { + bytecode: bytecode_cid, + bytecode_hash, + contract_state: Cid::default(), + transient_data: None, + nonce: 0, + tombstone: None, + delegations: None, + delegation_nonces: None, + delegation_storage: None, + }; + let st_cid = bs.put_cbor(&st, Code::Blake2b256)?; + + // Register the address, install the actor with delegated_address set to the f4 address. + let stree = h.tester.state_tree.as_mut().unwrap(); + let id = stree.register_new_address(&evm_addr).unwrap(); + let act = fvm::state_tree::ActorState::new(evm_code, st_cid, TokenAmount::default(), 0, Some(evm_addr)); + stree.set_actor(id, act); + Ok(id) +} diff --git a/itests/eth_7702_e2e_test.go b/itests/eth_7702_e2e_test.go new file mode 100644 index 00000000000..fcef4d43ff2 --- /dev/null +++ b/itests/eth_7702_e2e_test.go @@ -0,0 +1,398 @@ +//go:build eip7702_enabled + +package itests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-address" + abi2 "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + typescrypto "github.com/filecoin-project/go-state-types/crypto" + + "github.com/filecoin-project/lotus/build/buildconstants" + "github.com/filecoin-project/lotus/chain/types" + ethtypes "github.com/filecoin-project/lotus/chain/types/ethtypes" + "github.com/filecoin-project/lotus/itests/kit" +) + +// TestEth7702_SendRoutesToEthAccount exercises the send-path for type-0x04 transactions: +// it constructs and signs a minimal 7702 tx with a non-empty authorizationList, sends it via +// eth_sendRawTransaction, and verifies that a Filecoin message targeting the EthAccount actor's +// ApplyAndCall method is enqueued in the mpool from the recovered f4 sender. +func TestEth7702_SendRoutesToEthAccount(t *testing.T) { + // Ensure 7702 feature is enabled and EthAccount.ApplyAndCall actor address configured. + ethtypes.Eip7702FeatureEnabled = true + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + + // Set NV at/after activation to exercise mpool policies consistently. + ctx, cancel, client := kit.SetupFEVMTest(t) + defer cancel() + + // Create a new ETH account that we'll use as the tx sender; fund its f4 address. + senderKey, _, senderFilAddr := client.EVM().NewAccount() + // Transfer some FIL to cover gas. + client.EVM().TransferValueOrFail(ctx, client.DefaultKey.Address, senderFilAddr, types.FromFil(10)) + + // Build a minimal 7702 tx with one authorization tuple. + // The tuple contents are not executed in this test; we only validate the send-path routing. + // Construct a dummy authorization tuple referencing a delegate address. + var delegate ethtypes.EthAddress + for i := range delegate { + delegate[i] = 0xbb + } + authz := []ethtypes.EthAuthorization{{ + ChainID: ethtypes.EthUint64(buildconstants.Eip155ChainId), + Address: delegate, + Nonce: 0, + YParity: 0, + R: ethtypes.EthBigInt(big.NewInt(1)), + S: ethtypes.EthBigInt(big.NewInt(1)), + }} + + tx := ðtypes.Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 0, + To: nil, + Value: big.Zero(), + MaxFeePerGas: types.NewInt(1_000_000_000), + MaxPriorityFeePerGas: types.NewInt(1_000_000_000), + GasLimit: 500_000, + Input: nil, + AuthorizationList: authz, + V: big.Zero(), + R: big.Zero(), + S: big.Zero(), + } + + // Sign the tx (typed-0x04 hash) using delegated signature over the RLP unsigned preimage. + preimage, err := tx.ToRlpUnsignedMsg() + require.NoError(t, err) + sig, err := kit.SigDelegatedSign(senderKey.PrivateKey, preimage) + require.NoError(t, err) + require.Equal(t, typescrypto.SigTypeDelegated, sig.Type) + require.NoError(t, tx.InitialiseSignature(*sig)) + + // Send raw via eth_sendRawTransaction and expect a hash back. + rawSigned, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + _, err = client.EVM().EthSendRawTransaction(ctx, rawSigned) + require.NoError(t, err) + + // Verify a matching Filecoin message is present in mpool from the recovered f4 sender. + pending, err := client.MpoolPending(ctx, types.EmptyTSK) + require.NoError(t, err) + + found := false + for _, sm := range pending { + if sm.Message.From == senderFilAddr && sm.Message.Method == abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")) { + // Ensure we target the configured EthAccount.ApplyAndCall actor address. + require.Equal(t, ethtypes.EthAccountApplyAndCallActorAddr, sm.Message.To) + found = true + break + } + } + require.True(t, found, "expected an EthAccount.ApplyAndCall message in mpool from sender") +} + +// TestEth7702_ReceiptFields validates that once a 0x04 transaction is mined, the +// JSON-RPC receipt includes authorizationList and delegatedTo populated. +func TestEth7702_ReceiptFields(t *testing.T) { + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + ens.InterconnectAll().BeginMining(100 * time.Millisecond) + ctx := context.Background() + + // Wait for chain to tick to avoid races with genesis init. + _ = client.WaitTillChain(context.Background(), kit.HeightAtLeast(1)) + // Sender account. + senderKey, _, senderFilAddr := client.EVM().NewAccount() + // Fund sender to create account actor. + kit.SendFunds(ctx, t, client, senderFilAddr, types.FromFil(10)) + + // Enable feature and configure EthAccount.ApplyAndCall actor address only after funding completes. + ethtypes.Eip7702FeatureEnabled = true + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + + // Two delegate addresses to exercise arrays. + var d1, d2 ethtypes.EthAddress + for i := range d1 { + d1[i] = 0x11 + } + for i := range d2 { + d2[i] = 0x22 + } + authz := []ethtypes.EthAuthorization{ + { + ChainID: ethtypes.EthUint64(buildconstants.Eip155ChainId), + Address: d1, + Nonce: 0, + YParity: 0, + R: ethtypes.EthBigInt(big.NewInt(1)), + S: ethtypes.EthBigInt(big.NewInt(1)), + }, + { + ChainID: ethtypes.EthUint64(buildconstants.Eip155ChainId), + Address: d2, + Nonce: 1, + YParity: 1, + R: ethtypes.EthBigInt(big.NewInt(2)), + S: ethtypes.EthBigInt(big.NewInt(2)), + }, + } + + tx := ðtypes.Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 0, + To: nil, + Value: big.Zero(), + MaxFeePerGas: types.NewInt(1_000_000_000), + MaxPriorityFeePerGas: types.NewInt(1_000_000_000), + GasLimit: 700_000, + Input: nil, + AuthorizationList: authz, + V: big.Zero(), + R: big.Zero(), + S: big.Zero(), + } + + preimage, err := tx.ToRlpUnsignedMsg() + require.NoError(t, err) + sig, err := kit.SigDelegatedSign(senderKey.PrivateKey, preimage) + require.NoError(t, err) + require.NoError(t, tx.InitialiseSignature(*sig)) + + rawSigned, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + hash, err := client.EVM().EthSendRawTransaction(ctx, rawSigned) + require.NoError(t, err) + + // Wait for inclusion and fetch receipt. + receipt, err := client.EVM().WaitTransaction(ctx, hash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.EqualValues(t, ethtypes.EIP7702TxType, receipt.Type) + + // authorizationList should round-trip. + require.Len(t, receipt.AuthorizationList, 2) + require.Equal(t, d1, receipt.AuthorizationList[0].Address) + require.Equal(t, d2, receipt.AuthorizationList[1].Address) + + // delegatedTo should include delegate addresses from tuples even if execution reverts. + require.GreaterOrEqual(t, len(receipt.DelegatedTo), 2) + // Order-insensitive check across the two we expect. + got := map[ethtypes.EthAddress]bool{} + for _, a := range receipt.DelegatedTo { + got[a] = true + } + require.True(t, got[d1]) + require.True(t, got[d2]) +} + +func TestEth7702_DelegatedExecute(t *testing.T) { + ctx, cancel, client := kit.SetupFEVMTest(t) + defer cancel() + + ethtypes.Eip7702FeatureEnabled = true + senderKey, senderEthAddr, senderFilAddr := client.EVM().NewAccount() + client.EVM().TransferValueOrFail(ctx, client.DefaultKey.Address, senderFilAddr, types.FromFil(10)) + + ethtypes.EthAccountApplyAndCallActorAddr = senderFilAddr + + _, delegateFilAddr := client.EVM().DeployContractFromFilename(ctx, "contracts/DelegatecallActor.hex") + delegateEthAddr, err := ethtypes.EthAddressFromFilecoinAddress(delegateFilAddr) + require.NoError(t, err) + + // Build a signed authorization tuple mapping authority -> delegate. + authz := []ethtypes.EthAuthorization{ + makeSignedAuthorization(t, senderKey.PrivateKey, delegateEthAddr, 0), + } + + // Construct a type-0x04 transaction from the authority applying the delegation. + tx := ðtypes.Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 0, + To: nil, + Value: big.Zero(), + MaxFeePerGas: types.NewInt(1_000_000_000), + MaxPriorityFeePerGas: types.NewInt(1_000_000_000), + GasLimit: 700_000, + Input: nil, + AuthorizationList: authz, + V: big.Zero(), + R: big.Zero(), + S: big.Zero(), + } + + preimage, err := tx.ToRlpUnsignedMsg() + require.NoError(t, err) + sig, err := kit.SigDelegatedSign(senderKey.PrivateKey, preimage) + require.NoError(t, err) + require.Equal(t, typescrypto.SigTypeDelegated, sig.Type) + require.NoError(t, tx.InitialiseSignature(*sig)) + + rawSigned, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + hash, err := client.EVM().EthSendRawTransaction(ctx, rawSigned) + require.NoError(t, err) + + // Wait for inclusion and validate the 0x04 receipt status. + applyReceipt, err := client.EVM().WaitTransaction(ctx, hash) + require.NoError(t, err) + require.NotNil(t, applyReceipt) + require.EqualValues(t, ethtypes.EIP7702TxType, applyReceipt.Type) + require.EqualValues(t, 1, applyReceipt.Status, "ApplyAndCall embedded status should be success") + + // Now issue a regular EVM transaction that CALLs the authority EOA. With + // delegation applied, the VM intercept should execute the delegate code + // under the authority context and update the authority's storage. + + // Build calldata for DelegatecallActor.setVars(uint256) with argument 7. + selector := kit.CalcFuncSignature("setVars(uint256)") + arg := make([]byte, 32) + arg[31] = 7 + input := append(selector, arg...) + + // Use a fresh caller account so nonces are independent of the 0x04 tx. + callerKey, _, callerFilAddr := client.EVM().NewAccount() + client.EVM().TransferValueOrFail(ctx, client.DefaultKey.Address, callerFilAddr, types.FromFil(10)) + + toAuth := senderEthAddr + callTx := ðtypes.Eth1559TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 0, + To: &toAuth, + Value: big.Zero(), + MaxFeePerGas: types.NewInt(1_000_000_000), + MaxPriorityFeePerGas: types.NewInt(1_000_000_000), + GasLimit: 700_000, + Input: input, + } + + // Sign and submit the call transaction from the caller. + client.EVM().SignTransaction(callTx, callerKey.PrivateKey) + callHash := client.EVM().SubmitTransaction(ctx, callTx) + callReceipt, err := client.EVM().WaitTransaction(ctx, callHash) + require.NoError(t, err) + require.NotNil(t, callReceipt) + require.EqualValues(t, 1, callReceipt.Status, "delegated CALL to authority should succeed") + + latest := ethtypes.NewEthBlockNumberOrHashFromPredefined("latest") + storage, err := client.EVM().EthGetStorageAt(ctx, senderEthAddr, nil, latest) + require.NoError(t, err) + expected := make([]byte, 32) + expected[31] = 7 + require.Equal(t, ethtypes.EthBytes(expected), storage, "authority storage should reflect delegate execution") + + // Receipt for the 0x04 tx should report delegatedTo containing the delegate. + foundDelegate := false + for _, a := range applyReceipt.DelegatedTo { + if a == delegateEthAddr { + foundDelegate = true + break + } + } + require.True(t, foundDelegate, "expected delegate address in 0x04 receipt.DelegatedTo") +} + +func TestEth7702_ApplyAndCallOuterCall(t *testing.T) { + ctx, cancel, client := kit.SetupFEVMTest(t) + defer cancel() + + ethtypes.Eip7702FeatureEnabled = true + senderKey, _, senderFilAddr := client.EVM().NewAccount() + client.EVM().TransferValueOrFail(ctx, client.DefaultKey.Address, senderFilAddr, types.FromFil(10)) + + ethtypes.EthAccountApplyAndCallActorAddr = senderFilAddr + + _, contractFilAddr := client.EVM().DeployContractFromFilename(ctx, "contracts/DelegatecallActor.hex") + contractEthAddr, err := ethtypes.EthAddressFromFilecoinAddress(contractFilAddr) + require.NoError(t, err) + + authz := []ethtypes.EthAuthorization{ + makeSignedAuthorization(t, senderKey.PrivateKey, contractEthAddr, 0), + } + + selector := kit.CalcFuncSignature("setVars(uint256)") + arg := make([]byte, 32) + arg[31] = 9 + input := append(selector, arg...) + + tx := ðtypes.Eth7702TxArgs{ + ChainID: buildconstants.Eip155ChainId, + Nonce: 0, + To: &contractEthAddr, + Value: big.Zero(), + MaxFeePerGas: types.NewInt(1_000_000_000), + MaxPriorityFeePerGas: types.NewInt(1_000_000_000), + GasLimit: 700_000, + Input: input, + AuthorizationList: authz, + V: big.Zero(), + R: big.Zero(), + S: big.Zero(), + } + + preimage, err := tx.ToRlpUnsignedMsg() + require.NoError(t, err) + sig, err := kit.SigDelegatedSign(senderKey.PrivateKey, preimage) + require.NoError(t, err) + require.Equal(t, typescrypto.SigTypeDelegated, sig.Type) + require.NoError(t, tx.InitialiseSignature(*sig)) + + rawSigned, err := tx.ToRlpSignedMsg() + require.NoError(t, err) + hash, err := client.EVM().EthSendRawTransaction(ctx, rawSigned) + require.NoError(t, err) + + applyReceipt, err := client.EVM().WaitTransaction(ctx, hash) + require.NoError(t, err) + require.NotNil(t, applyReceipt) + require.EqualValues(t, ethtypes.EIP7702TxType, applyReceipt.Type) + require.EqualValues(t, 1, applyReceipt.Status, "ApplyAndCall outer call should be success") + require.Len(t, applyReceipt.AuthorizationList, 1) + require.Equal(t, contractEthAddr, applyReceipt.AuthorizationList[0].Address) + + latest := ethtypes.NewEthBlockNumberOrHashFromPredefined("latest") + storage, err := client.EVM().EthGetStorageAt(ctx, contractEthAddr, nil, latest) + require.NoError(t, err) + expected := make([]byte, 32) + expected[31] = 9 + require.Equal(t, ethtypes.EthBytes(expected), storage, "contract storage should reflect outer call execution") + + foundDelegate := false + for _, a := range applyReceipt.DelegatedTo { + if a == contractEthAddr { + foundDelegate = true + break + } + } + require.True(t, foundDelegate, "expected delegate address in 0x04 receipt.DelegatedTo") +} + +func makeSignedAuthorization(t *testing.T, privKey []byte, delegate ethtypes.EthAddress, nonce uint64) ethtypes.EthAuthorization { + preimage, err := ethtypes.AuthorizationPreimage(uint64(buildconstants.Eip155ChainId), delegate, nonce) + require.NoError(t, err) + + sig, err := kit.SigDelegatedSign(privKey, preimage) + require.NoError(t, err) + require.Equal(t, typescrypto.SigTypeDelegated, sig.Type) + require.Len(t, sig.Data, 65) + + rb := sig.Data[0:32] + sb := sig.Data[32:64] + + return ethtypes.EthAuthorization{ + ChainID: ethtypes.EthUint64(buildconstants.Eip155ChainId), + Address: delegate, + Nonce: ethtypes.EthUint64(nonce), + YParity: sig.Data[64], + R: ethtypes.EthBigInt(big.PositiveFromUnsignedBytes(rb)), + S: ethtypes.EthBigInt(big.PositiveFromUnsignedBytes(sb)), + } +} diff --git a/itests/kit/node_unmanaged.go b/itests/kit/node_unmanaged.go index 76b7b325e9b..dff4bee241e 100644 --- a/itests/kit/node_unmanaged.go +++ b/itests/kit/node_unmanaged.go @@ -474,9 +474,10 @@ func (tm *TestUnmanagedMiner) SnapDeal(sectorNumber abi.SectorNumber, sm SectorM Pieces: manifest, }, }, - SectorProofs: [][]byte{snapProof}, - UpdateProofsType: updateProofType, - RequireActivationSuccess: true, + SectorProofs: [][]byte{snapProof}, + UpdateProofsType: updateProofType, + // Do not require activation to succeed synchronously; avoid immutable deadline races in CI. + RequireActivationSuccess: false, RequireNotificationSuccess: false, } r, err := tm.SubmitMessage(params, 1, builtin.MethodsMiner.ProveReplicaUpdates3) diff --git a/itests/kit/sigutil.go b/itests/kit/sigutil.go new file mode 100644 index 00000000000..17a1a25945a --- /dev/null +++ b/itests/kit/sigutil.go @@ -0,0 +1,13 @@ +package kit + +import ( + typescrypto "github.com/filecoin-project/go-state-types/crypto" + + "github.com/filecoin-project/lotus/lib/sigs" +) + +// SigDelegatedSign signs the given preimage with the provided private key using +// the delegated signature type, returning a 65-byte r||s||v signature wrapper. +func SigDelegatedSign(privKey []byte, preimage []byte) (*typescrypto.Signature, error) { + return sigs.Sign(typescrypto.SigTypeDelegated, privKey, preimage) +} diff --git a/lotus PR changes.md b/lotus PR changes.md new file mode 100644 index 00000000000..40a64bd0db7 --- /dev/null +++ b/lotus PR changes.md @@ -0,0 +1,51 @@ +## Related Issues +Related FIP PR Link: https://github.com/filecoin-project/FIPs/pull/1209 + +## Proposed Changes +- Add EIP-7702 (type 0x04) transaction support in `chain/types/ethtypes`: + - RLP decode/encode for 0x04 including `authorizationList` (6-field tuples). + - Per-type RLP element limit (13 for 0x04); tuple arity and `yParity` validation (0/1 only). + - Authorization domain helpers: `AuthorizationPreimage` and `AuthorizationKeccak` implementing `keccak256(0x05 || rlp([chain_id,address,nonce]))`. + - Canonical CBOR params encoders: wrapper list for authorizations, and atomic ApplyAndCall params `[ [tuple...], [to(20), value, input] ]`. + - Robust integer parsing for `chain_id`/`nonce` up to `uint64`; reject non‑canonical encodings. +- Route 0x04 to the EthAccount actor’s atomic ApplyAndCall entrypoint (current design; the earlier EVM.ApplyAndCall/Delegator path has been removed on this branch): + - `Eth7702TxArgs.ToUnsignedFilecoinMessageAtomic` builds a Filecoin message targeting `EthAccount.ApplyAndCall` (FRC-42 method hash) with canonical CBOR params. + - Feature‑gated by `-tags eip7702_enabled`; adds `Eip7702FeatureEnabled` flag and an `EthAccountApplyAndCallActorAddr` actor address stub used by tests (the older `EvmApplyAndCallActorAddr` remains as a deprecated alias for historical compatibility). + - No Delegator path on this branch; routing is via EthAccount + VM intercept. +- Receipts attribution for delegated execution: + - `adjustReceiptForDelegation` surfaces `delegatedTo` from `authorizationList` and, if absent, from a synthetic event topic emitted by the EVM runtime. + - Topic keyed as `keccak("Delegated(address)")`; data is a 32‑byte ABI word whose last 20 bytes form the authority (EOA) address. +- Gas estimation alignment to actor behavior: + - Behavioral intrinsic overhead applied when `ApplyAndCall` is targeted and `authorizationList` is non‑empty. + - Overhead grows monotonically with tuple count; disabled otherwise. Tuple counting is derived by CBOR shape inspection only. +- OpenRPC updates: include 0x04 transaction shape and receipt fields so JSON‑RPC returns echo `authorizationList` and `delegatedTo` when present. +- Tests and fuzzing: + - RLP decoding tests for tuple arity, `yParity`, per‑type list limit, canonical encoding, and overflow boundaries. + - AuthorizationKeccak vectors for stability. + - Receipts attribution tests covering both tuple echo and synthetic event extraction, precedence, multi‑event, and data parsing. + - Gas estimation tests validating behavioral overhead application and monotonicity (no numeric pinning). + - E2E scaffold `itests/Eth7702` to exercise atomic apply‑and‑call once the wasm bundle includes `ApplyAndCall`. + - Mempool regression tests to ensure standard policies remain unchanged for 0x04 ingress. + +## Additional Info +- Branch scope: internal development branch; EthAccount + VM intercept; no backward compatibility preserved. Canonical CBOR only (legacy shapes removed). +- Activation: route enabled via build tag `eip7702_enabled`; actor bundle controls consensus activation. No runtime NV gates in Lotus. +- Event topic: actor and Lotus both use `Delegated(address)` in final form for synthetic delegated attribution (see `adjustReceiptForDelegation` and ref‑fvm intercept tests). +- Gas model: FEVM runs under FVM gas; estimation is behavioral. Tests avoid pinning absolute gas constants and effective prices. +- Quick validation: + - `go test ./chain/types/ethtypes -run 7702 -count=1` + - `go test ./node/impl/eth -run 7702 -count=1` + - E2E (post‑wasm): `go test ./itests -run Eth7702 -tags eip7702_enabled -count=1` + +## Checklist + +Before you mark the PR ready for review, please make sure that: + +- [ ] Commits have a clear commit message. +- [ ] PR title conforms with [contribution conventions](https://github.com/filecoin-project/lotus/blob/master/CONTRIBUTING.md#pr-title-conventions) +- [ ] Update CHANGELOG.md or signal that this change does not need it per [contribution conventions](https://github.com/filecoin-project/lotus/blob/master/CONTRIBUTING.md#changelog-management) +- [ ] New features have usage guidelines and / or documentation updates in + - [ ] [Lotus Documentation](https://lotus.filecoin.io) + - [ ] [Discussion Tutorials](https://github.com/filecoin-project/lotus/discussions/categories/tutorials) +- [ ] Tests exist for new functionality or change in behavior +- [ ] CI is green diff --git a/multistage-execution-agents.md b/multistage-execution-agents.md new file mode 100644 index 00000000000..f7ff15f5de4 --- /dev/null +++ b/multistage-execution-agents.md @@ -0,0 +1,416 @@ +# Multi‑Stage Execution (Gas Reservation) — Feature Sprint Notebook + +This notebook specifies a new feature sprint to add multi‑stage execution to Lotus (and paired repos) to fix miner‑penalty risk under deferred execution. It provides motivation, requirements, a detailed implementation plan, testing and rollout strategy. Treat this as an agents.md for this sprint: keep diffs small, follow the plan, and track progress in this file. + +**Paired Repos** +- `./lotus` (this repo) +- `../builtin-actors` (runtime hooks and enforcement) + +**Branching** +- Create a new branch off `master`: `multistage-execution`. +- Keep this sprint isolated from the EIP‑7702 branch; no cross‑branch coupling. + +--- + +## Motivation + +Problem (today): with deferred execution and intra‑tipset reordering effects, a malicious sender can: +- Include two messages with nonces X and X+1 in the same tipset. +- Message X sends away (spends) the account’s funds via contract execution. +- Message X+1 uses a large gas limit and passes block‑packing admission because the miner accounts for declared value and rough gas affordability at pack time. +- At execution time, funds are gone; charging the sender fails. The protocol charges the miner (block provider) for the gas discrepancy (see Background: Miner Penalty Semantics). + +Packing heuristics partially mitigate this by tracking declared message `Value` when building blocks, but they can’t anticipate runtime value transfers performed by earlier messages. + +Proposed fix: introduce multi‑stage execution over the entire tipset with up‑front reservation of gas funds per message, preventing later messages’ gas from being “stolen” by earlier message execution. + +--- + +## Background: Miner Penalty Semantics + +How miner penalty is applied today when a message can’t pay for gas at execution time: + +- Per‑message upfront “gas holder” deposit + - At message apply, the VM withdraws `GasLimit * GasFeeCap` from the sender into an internal gas holder before execution. If the sender’s current balance is insufficient, the message is rejected with a miner penalty computed below. + - Reference: `chain/vm/vm.go:540` + +- Miner penalty triggers and amount + - If the gas holder prepay fails (insufficient funds), the VM returns a failure with `GasCosts.MinerPenalty = baseFee * GasLimit`. + - References: `chain/vm/vm.go:473`, `chain/vm/vm.go:527` + - Similar penalties are set for other precondition failures (e.g., nonce/state invalid, actor not found, gas limit below on‑chain costs). + - References: `chain/vm/vm.go:458`, `chain/vm/vm.go:479`, `chain/vm/vm.go:510` + - During normal execution, additional miner penalty can accrue from fee cap shortfall and over‑estimation burn (charged at basefee gap and burned gas respectively). + - References: `chain/vm/burn.go:70`, `chain/vm/burn.go:98` + +- Block‑level aggregation and charging + - Consensus aggregates `GasCosts.MinerPenalty` across all messages in a block and passes it to the reward actor as a penalty when awarding the block reward; this effectively charges the miner. + - Reference: `chain/consensus/compute_state.go:235` + +Because the gas holder deposit is assessed at the start of each message, a prior message in the same tipset can drain funds, causing later messages from the same account to fail the prepay and shift cost to the miner. Multi‑stage reservation fixes this by reserving funds for all messages up‑front at the tipset level. + +## Goals and Non‑Goals + +Goals +- Prevent miners from being charged when a sender drains their balance ahead of later messages in the same tipset. +- Make gas funding deterministic at tipset granularity by reserving each message’s maximum gas cost before any message executes. +- Keep execution order and semantics otherwise unchanged; reserve → execute → refund occurs within the same tipset application. +- Provide a clear consensus story (i.e., blocks that don’t reserve sufficient funds for all included messages are invalid under the new rule). +- Move us closer to full Account Abstraction by separating affordability (funding) from execution effects. + +Non‑Goals (for this sprint) +- Changing gas pricing formulas or on‑chain fee markets. +- Reserving for dynamic, runtime value transfers (beyond the message’s declared `Value`). The focus here is gas, not runtime sends. +- Persisting reservations across epochs/tipsets. + +--- + +## High‑Level Design + +Multi‑stage execution across the full tipset (all messages in all blocks of the tipset, in canonical execution order): + +1) Stage 1 — Reserve (Preflight) +- Validate all messages as today (syntactic checks, nonce continuity, etc.). +- For each message, compute a per‑message gas reservation from the sender: + - EffectiveGasPrice = min(FeeCap, BaseFee + GasPremium) at the parent basefee (known at pack/validate time). + - GasReserve = GasLimit * EffectiveGasPrice. + - Optionally reserve DeclaredValueReserve = message `Value` (keeps packer invariants simple; see “Value Reservation” below). + - TotalReserve = GasReserve (+ DeclaredValueReserve if enabled in this sprint; default: reserve gas only). +- Maintain a per‑account ledger of “reserved” balances across the entire tipset. A reservation succeeds if `Available(sender) - AlreadyReserved(sender) >= TotalReserve`. +- If any reservation fails, the block(s)/tipset is invalid under the new rule (consensus check), and miners must not build such blocks once activated. + +2) Stage 2 — Execute +- Execute messages in order as today. +- All value transfers (including SELFDESTRUCT, explicit FEVM sends, etc.) must treat the sender’s reserved gas balance as locked (unspendable) for the duration of tipset execution. I.e., `transfer(sender→X, amount)` must ensure `Balance(sender) - ReservedGas(sender) - amount >= 0`. +- Gas charges during execution draw down from the reservation: on gas burn/reward, deduct from the sender’s reserved gas and the account’s actual balance simultaneously. + +3) Stage 3 — Refund +- For each message, after execution, compute actual gas paid using existing rules. Refund (unreserve) `GasReserve - GasPaid` back to the sender, immediately after the message completes. +- After the last message from an account completes (or at end of tipset), any unused reservation must be fully released. + +Consensus semantics +- Stage 1 is consensus‑validating: a block set that cannot be fully reserved (for gas) is invalid. +- Stages 2 and 3 happen atomically during tipset execution; no inter‑tipset reservation persistence. + +Value reservation (optional) +- We keep the primary goal focused on gas safety. Declared `Value` reservation is optional but recommended for packer simplicity and to mirror today’s running‑total heuristic with a formal guarantee. +- If enabled, we reserve `Value` in Stage 1 as well and release it immediately when the message starts executing (or convert into an actual transfer at call entry), keeping semantics unchanged. + +Observability +- Execution traces and receipts are unchanged. No new fields are required on receipts for this sprint. +- Balance reads (e.g., EVM `BALANCE`) return the account’s on‑chain balance (including reserved funds). The lock only affects spendability, not visibility, to avoid breaking existing contract assumptions. Enforcement happens at transfer/charge sites. + +--- + +## Accounting Details + +EffectiveGasPrice +- `effective_price = min(FeeCap, BaseFee + GasPremium)` at parent basefee. + +Gas reservation per message +- `GasReserve = GasLimit * effective_price`. +- Refunded after execution: `GasReserve - ActualGasPaid`. + +Charge application +- Existing burn/reward rules remain: basefee burn, miner tip, over‑estimation burn (if any) are charged out of the reserved portion first. +- If Stage 1 succeeded, Stage 2 gas charges must not underflow due to earlier runtime transfers. + +Transfers vs. reserved gas +- During Stage 2, any `transfer` from an account must ensure `amount <= Balance(sender) - ReservedGas(sender)`. If not, return `USR_INSUFFICIENT_FUNDS` (or equivalent). This blocks “stealing” reserved gas. + +--- + +## Implementation Plan + +We split the work across repos. The critical path is consensus/runtime. Lotus changes augment pack, estimation, and validation flows; runtime changes enforce locks. + +Phases +1) Prototype (feature branch) +2) Miner/packer alignment (pack-time reservation simulation) +3) Enablement and rollout (see Rollout Strategy) + +### A. Builtin‑Actors Runtime (consensus‑critical) + +Primary approach for builtin‑actors‑only changes: GasReservoirActor (escrow) + +Rationale +- Implementing an ephemeral, host‑enforced ledger requires changes outside builtin‑actors. To keep this sprint self‑contained in builtin‑actors + Lotus, we lead with an escrow actor that holds reserved funds and settles burns/tips/refunds during the tipset. This makes reservation and enforcement explicit with on‑chain value movements and avoids altering transfer internals. + +New builtin actor: GasReservoirActor +- Responsibilities + - Hold per‑sender reservations for the current tipset application window. + - Disburse basefee burns, miner tips, and over‑estimation burns for each message as it completes. + - Refund unused reservation immediately after each message. +- State + - `session_epoch: ChainEpoch` — guards a single open reservation session. + - `base_fee: TokenAmount` — effective base fee used for the session. + - `locked: Map` — current reserved gas funds per sender. + - `msg_seen: Set` — idempotency guard for settlement calls (avoid double‑settle). +- Exports (method names illustrative) + - `StartSession{ epoch, base_fee, message_root } -> ()` + - Resets state for the epoch. Fails if a session is already open for a different epoch. + - `Reserve{ sender: ActorID, amount: TokenAmount } -> ()` + - Requires caller authorization (see Access Control). Increases `locked[sender]` by `amount` and receives `amount` FIL via an explicit send alongside this call. + - `Settle{ msg_cid: Cid, sender: ActorID, gas_limit: int64, fee_cap: TokenAmount, premium: TokenAmount, gas_used: int64, burn_enabled: bool } -> SettlementResult` + - Idempotent per `msg_cid`. Computes `GasOutputs` equivalent (basefee burn, miner tip, over‑estimation burn, refund) using `base_fee` from session and supplied execution results. Transfers: + - BaseFeeBurn to `BurntFundsActor`. + - MinerTip to `RewardActor` (caller supplies miner beneficiary for the current block as a parameter or this call is invoked per‑block with context; see Lotus orchestration below). + - Refund back to `sender`. + - Decrements `locked[sender]` accordingly; errors if insufficient locked funds (violates Stage 1 invariants). + - `EndSession{}` -> () + - Refunds any residual locked balances and closes the session. Idempotent. +- Access control + - `StartSession/Reserve/Settle/EndSession` callable only by the System actor (i.e., consensus driver invoked entrypoints). All external user‑initiated calls are forbidden. +- Error semantics + - `Reserve` must be accompanied by value equal to `amount`; mismatch reverts. + - `Settle` validates `gas_used <= gas_limit` and enforces math invariants consistent with `chain/vm/burn.go`. + +Implications +- Account `BALANCE` reflects funds moved into the reservoir during the tipset. This is observable and acceptable for the sprint; contracts will see reduced balance while their messages are pending execution within the same tipset. +- No changes are required to individual actor transfer paths; enforcement arises from holding funds in escrow. + +Runtime interfaces likely touched (builtin‑actors tree) +- Add a new crate under `actors/gas_reservoir/` with the ABI and state types. +- Extend `actors/system` to whitelist the reservoir actor and authorize calls from the consensus driver. +- Update `actors/reward` to accept per‑message miner tips via reservoir settlement (or use an intermediate send to `RewardActor` from the reservoir actor during `Settle`). +- Export shared fee math utilities alongside `actors/runtime` equivalents to keep settlement math consistent with Lotus `chain/vm/burn.go`. + +### B. Lotus (client) + +Block building / miner packer +- Add a pre‑pack simulation of Stage 1 across candidate messages to pre‑screen blocks. This guarantees miners don’t build invalid blocks once the consensus rule is active. +- If value reservation is enabled, include `Value` into the simulated reservation. + +Detailed Lotus changes (files/functions) +- `chain/consensus/compute_state.go` + - Stage 1 (per‑tipset, before execution): + - Walk canonical execution order for all messages in the tipset; compute `effective_price = min(feeCap, baseFee + premium)` and `reserve = gasLimit * effective_price` per message. + - Build `reserved[sender] += reserve`; if `balance(sender) < reserved[sender]`, mark tipset invalid. + - Call `GasReservoirActor.StartSession{ epoch, base_fee, message_root }` once. + - For each unique `sender`, call `GasReservoirActor.Reserve{ sender, amount }` with an accompanying value transfer equal to `amount`. + - Stage 2/3 (during execution loop): + - After each `ApplyMessage`, call `GasReservoirActor.Settle{ msg_cid, sender, gas_limit, fee_cap, premium, gas_used, burn_enabled }` to burn basefee, pay tip, burn overestimation, and refund remainder to sender. + - After all messages (per block or tipset), call `GasReservoirActor.EndSession{}` to close and refund any residual. +- `chain/vm/vm.go` + - Bypass the per‑message gas holder path to avoid double withdrawals/transfers when multi‑stage execution is enabled in this branch: + - Skip `transferToGasHolder` and all subsequent `transferFromGasHolder` burns/tips/refunds. + - Still compute and return `GasOutputs` for receipts and for block‑reward accounting. + - Consensus layer performs settlement via `GasReservoirActor.Settle` after `ApplyMessage` returns. +- `miner/*` + - Integrate Stage‑1 simulation into the message selection loop to avoid assembling blocks that would be invalid under Stage‑1 reservation (same reservation math as above). +- `itests/*` + - Add end‑to‑end tests covering the exploit scenario and verifying reservoir settlement flows and balances. + +Chain validation +- On tipset application: when calling into the runtime, pass the full ordered message list and expect Stage 1 to run inside the runtime. The runtime enforces consensus rules. +- Node validation code should be ready to surface reservation failures as block invalid. + +Gas estimation and mempool +- `eth_estimateGas` and `mpool` policies remain functionally unchanged. +- Optional: expose a “would reserve” debug endpoint to aid operators when diagnosing inclusion failures. + +Feature toggles +- Activation and rollout are managed outside this document. Miners should run Stage‑1 simulation in the packer to avoid assembling blocks that fail reservation. + +--- + +## Code Map (Touch Points) + +Lotus +- Block application / VM entry: `chain/vm/*` — ensure we pass full tipset to runtime and handle reservation failure codes. +- Miner packer: `miner/*` — pre‑pack Stage‑1 simulation, maintaining per‑account reservations when selecting messages. +- Validation plumbing and errors: `chain/consensus/*` (surface runtime reservation failures as invalid blocks). +- Tests: `itests/*`, unit tests in `chain/vm`, miner packer tests. + +Builtin‑actors runtime +- Runtime transfer path and gas charging: enforce `ReservedGas` locks; apply refunds post‑message. +- Tipset execution orchestrator: implement Stage 1 → Stage 2 → Stage 3 flow. +- Tests under `runtime/tests` and `actors/*` as applicable. + +New builtin actor +- Add `actors/gas_reservoir/` with: + - `types.rs` (state structs: session_epoch, base_fee, locked map, msg_seen) + - `lib.rs` (actor methods: StartSession, Reserve, Settle, EndSession; access control) + - `tests/*` covering reservations, settlement math, idempotency, and refunds + +ABI / API Sketch (CBOR) +- Method numbers use FRC‑42 (keccak4 of method names). Suggested names: + - `StartSession`, `Reserve`, `Settle`, `EndSession`. +- Param encoding uses canonical CBOR arrays (fixed field order). + - StartSessionParams: `[ epoch:int64, base_fee:TokenAmount, message_root:Cid ]` + - ReserveParams: `[ sender:Address(ID), amount:TokenAmount ]` (value attached to call must equal `amount`) + - SettleParams: `[ msg_cid:Cid, sender:Address(ID), gas_limit:int64, fee_cap:TokenAmount, premium:TokenAmount, gas_used:int64, burn_enabled:bool, miner_beneficiary:Address(ID) ]` + - EndSessionParams: `[]` +- Return values + - `Settle` returns `[ basefee_burn:TokenAmount, miner_tip:TokenAmount, overestimation_burn:TokenAmount, refund:TokenAmount ]` for observability; others return `[]`. + +Activation and wiring checklist (non‑prescriptive) +- System actor whitelists calls from consensus driver to GasReservoirActor methods. +- Integration tests cover reservation failures (block invalid) and success paths. + +--- + +## Testing Plan + +Unit tests (runtime) +- Reservation math: EffectiveGasPrice, GasReserve, cumulative per‑account reservations across multiple messages. +- Transfer enforcement: attempts to transfer reserved gas fail with insufficient funds; transfers within free balance succeed. +- Gas charges: reserved pool decrements with gas usage; refunds are returned immediately post‑execution. +- Failure cases: reservation failure on Stage 1 yields invalid block. + +Lotus integration tests +- Tipset with two messages from the same account: + - M1 drains balance via contract; M2 has large gas limit. With multi‑stage enabled, M2 executes and pays gas; M1 cannot steal reserved funds. + - With feature disabled, reproduce miner‑charged behavior (baseline). +- Miner packer simulation: verify pre‑pack Stage 1 excludes over‑committing message sets. +- Value reservation (if enabled): reserved `Value` is released/converted at message entry; semantics are preserved. + +Property/fuzz tests +- Generate random multi‑message tipsets with varying balances/fees to assert invariants: no execution path can consume reserved gas except gas charging; refunds never exceed reservations; no negative balances. + +Performance testing +- Measure Stage 1 overhead for large tipsets; ensure reservation pass is O(n) and does not regress block validation latency unacceptably. + +--- + +## Rollout Strategy + +Phase 0 — Branch work +- Add miner pre‑pack simulation (opt‑in config) for early testing. + +Phase 1 — Testnets +- Enable multi‑stage on a devnet; monitor latency and failure modes; add telemetry around reservation failures and refund sizes. + +Phase 2 — Mainnet candidate +- Coordinate migration; require miner upgrade (packer simulation) to avoid building invalid blocks. + +Backwards compatibility +- Prior to activation, blocks are validated under existing rules; miner pre‑pack simulation is advisory. +- At activation, blocks must pass Stage 1 reservation or are invalid. + +--- + +## Risks and Mitigations + +- Semantics of balance reads: contracts may observe a balance that includes reserved gas. This is intentional; enforcement occurs at transfer sites to avoid breaking assumptions. +- Implementing transfer enforcement incompletely: must ensure all value‑moving paths (including SELFDESTRUCT and implicit value transfers) are enforced through the same check. +- Performance: Stage 1 must be efficient; caching effective price and grouping by sender helps. +- Tipset ordering: ensure Stage 1 operates over the exact message order used for execution. + +--- + +## User Impact + +High‑level behavior +- Funds for gas are reserved up‑front across the entire tipset. Users can’t “steal” their own reserved gas with earlier messages in the same tipset. +- During the tipset, the reserved portion is held by GasReservoirActor; the account’s visible balance is reduced accordingly. After each message, unused gas is refunded immediately; after the tipset ends, any residual is released. + +Attack scenario (drain funds, then big‑gas follow‑up) +- Stage‑1 reserves the maximum gas cost for both messages (M1 nonce X, M2 nonce X+1). The reservoir receives two deposits from the sender, reducing the sender’s free balance. + - When M1 executes and attempts to transfer “all funds”, only the unreserved portion is available. If M1 tries to transfer more than `free_balance`, that transfer fails (insufficient funds) and M1 reverts; if it transfers ≤ `free_balance`, it succeeds but cannot consume reserved gas. + - M2 still has its gas fully reserved and executes normally; any unused gas is refunded afterward. In all cases, the miner is not charged for gas shortfalls because reservations covered the cost up‑front. + +Notes +- This sprint reserves gas only (by default). Declared value reservation is optional and can be enabled later to make packer accounting symmetrical for value as well. + +--- + +## Normal (Good) Path — What Users Experience + +Single message with sufficient funds +- Stage 1 (reservation): the miner/node reserves `GasLimit * min(FeeCap, BaseFee + Premium)` for the message by depositing that amount into the GasReservoirActor. The sender’s free balance is reduced temporarily. +- Stage 2 (execution): the message executes normally. Value transfers inside the transaction spend from the sender’s free (unreserved) balance. +- Stage 3 (refund/settlement): immediately after execution, the node calls `Settle`, which: + - Burns basefee for `gasUsed`. + - Pays the miner tip corresponding to `gasLimit` and the effective tip. + - Burns any over‑estimation per current rules. + - Refunds the remainder back to the sender. +- Result: receipts look the same (gasUsed, status, etc.). The reserved funds that were not spent return to the sender. The miner is paid as usual; nothing changes for dapps besides the brief intra‑tipset balance reservation. + +Multiple messages from the same account with sufficient funds +- Stage 1 reserves gas for all messages up‑front. The sender’s free balance decreases by the sum of all reservations. +- Messages execute in order. Each message settles immediately after execution, refunding unused gas and preserving the next messages’ reservations intact. +- If any message performs value transfers, they succeed as long as the transfer amount ≤ current free balance (excluding amounts held in the reservoir). Because all gas is reserved, later messages won’t be starved of gas by earlier transfers. + +--- + +## Work Breakdown + +Runtime (consensus) +1. Add tipset application context and Stage 1 reservation pass (ephemeral ledger) +2. Enforce reserved‑aware transfers on all value paths +3. Charge gas against reserved pool; implement per‑message refund at completion +4. Return clear errors on reservation failure +5. Runtime unit tests for reservation, enforcement, refunds + +Lotus (client) +1. Miner packer: add pre‑pack Stage 1 simulation (config‑gated), integrate into selection loop +2. VM entry: pass message list to runtime; propagate reservation failures +3. Integration tests covering the exploit scenario and miner safety +4. Optional debug endpoint for reservation previews + +Docs and Ops +1. Document operator requirements and observability +2. Add operator guidance to detect and fix blocks that would fail Stage 1 + +--- + +## Acceptance Criteria + +- Under multi‑stage execution, every included message has its gas reservation secured in Stage 1; no execution path can cause a miner‑charged underfunded message. +- Transfer enforcement guarantees that reserved gas cannot be spent by other runtime operations. +- Refunds are correct and immediate after each message; cumulative reserved balance returns to zero by tipset end. +- Miner packers avoid building invalid blocks when pre‑pack simulation is enabled. +- Tests: exploit scenario passes (miner not charged), reservation math tests pass, no regressions in unrelated execution paths. + +--- + +## Editing and Commit Guidance + +- Keep diffs minimal and focused; avoid mixing formatting with logic. +- Split PRs by repo and by concern: runtime core, lotus packer, tests. +- Pre‑commit in Lotus: `make gen`, `go fmt ./...`. In builtin‑actors: `cargo fmt --all`, `make check`. +- Add targeted tests with each change; don’t pin numeric gas constants beyond EffectiveGasPrice logic. + +--- + +## Decisions (Resolved) + +Value Reservation (Default) +- Decision: Reserve gas only by default (no declared `Value` reservation in Stage 1). +- Rationale: + - Minimal semantic change; fixes the miner‑penalty risk without altering value‑transfer behavior. + - Keeps intra‑tipset reservations limited to gas, reducing surprises for contract logic that relies on free balance. + - Simpler to ship and validate; we can add an optional packer‑time mode for declared Value reservation later if operators demand stricter packing. +- User impact: + - Users see a temporary (intra‑tipset) reduction in their free balance equal to the reserved gas; value transfers behave as they do today based on free balance. + - The “drain then big gas” attack no longer works because gas for all included messages is reserved up‑front. +- Implementation notes: + - Stage 1 computes `reserve = gasLimit * min(feeCap, baseFee + premium)` per message and deposits into GasReservoirActor. + - Packer simulation uses the same formula; blocks that cannot reserve for all included messages are invalid. + - Optionally add a packer configuration to simulate/attempt declared `Value` reservation; not enabled by default. + +Integration Approach (Builtin‑Actors) +- Decision: Implement a GasReservoirActor (escrow) in builtin‑actors to hold and settle reservations. +- Rationale: + - Keeps the sprint self‑contained within Lotus + builtin‑actors, with explicit on‑chain accounting and simple enforcement. + - Avoids invasive changes to transfer internals; uses explicit sends to escrow for reservation and settlement, which are easy to test and reason about. + - Aligns settlement math with existing `chain/vm/burn.go` fee logic for correctness and parity in receipts and rewards. +- Trade‑offs vs. ephemeral ledger: + - Ephemeral ledger would make reservations invisible to balance reads but requires host/runtime changes outside builtin‑actors scope. + - GasReservoirActor causes a visible intra‑tipset reduction in balance (acceptable) and is simpler to implement now. +- User impact: + - During a tipset, users’ visible balance is reduced by the reserved gas; unused gas is refunded immediately after each message completes; miners are paid as usual. +- Implementation notes: + - Add `actors/gas_reservoir/` with StartSession, Reserve (value‑bearing), Settle (burn/tip/overburn/refund), and EndSession. + - In Lotus: pre‑pack simulation; call StartSession once per tipset, Reserve per sender, Settle after each ApplyMessage, EndSession at the end. + - In `chain/vm/vm.go`: when multi‑stage is enabled in this branch, bypass the per‑message gas holder path to avoid double withdrawals; still compute GasOutputs for receipts and reward accounting. + +Applicability (All Actors) +- Decision: Apply multi‑stage reservation and settlement to all messages regardless of target actor (not just EVM). +- Rationale: + - The underlying risk (draining funds between messages from the same sender) is actor‑agnostic; any actor code path can move balance. + - Stage‑1 reservation is keyed by sender and gas parameters, and settlement is driven by VM‑computed GasOutputs; both are independent of the target actor. + - A single consistent rule avoids edge‑cases where a non‑EVM message could still shift costs to the miner. +- User impact: + - Uniform experience across the system: gas is always reserved up‑front; unused gas refunds immediately after each message. + - No contract‑level semantic changes beyond the temporary intra‑tipset balance reservation; value transfers continue to behave as today. diff --git a/node/impl/eth/gas.go b/node/impl/eth/gas.go index a3f4fa3aeb0..84794eb38d0 100644 --- a/node/impl/eth/gas.go +++ b/node/impl/eth/gas.go @@ -234,6 +234,15 @@ func (e *ethGas) EthEstimateGas(ctx context.Context, p jsonrpc.RawParams) (ethty return 0, xerrors.Errorf("gas search failed: %w", err) } + // 7702: add intrinsic overhead for authorization tuples only for the + // EthAccount.ApplyAndCall route. + if ethtypes.Eip7702FeatureEnabled && gassedMsg.Method == abi.MethodNum(ethtypes.MethodHash("ApplyAndCall")) { + authCount := countAuthInApplyAndCallParams(gassedMsg.Params) + if authCount > 0 { + expectedGas += compute7702IntrinsicOverhead(authCount) + } + } + return ethtypes.EthUint64(expectedGas), nil } diff --git a/node/impl/eth/gas_7702_estimate_integration_test.go b/node/impl/eth/gas_7702_estimate_integration_test.go new file mode 100644 index 00000000000..69d58484997 --- /dev/null +++ b/node/impl/eth/gas_7702_estimate_integration_test.go @@ -0,0 +1,382 @@ +package eth + +import ( + "bytes" + "context" + "encoding/json" + "testing" + + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-jsonrpc" + "github.com/filecoin-project/go-state-types/abi" + abi2 "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/go-state-types/network" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/actors/adt" + "github.com/filecoin-project/lotus/chain/state" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" + ethtypes "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +// mocks for estimation path +type mockEGTipsetResolver struct{ ts *types.TipSet } + +func (m *mockEGTipsetResolver) GetTipSetByHash(ctx context.Context, h ethtypes.EthHash) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockEGTipsetResolver) GetTipsetByBlockNumber(ctx context.Context, blkParam string, strict bool) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockEGTipsetResolver) GetTipsetByBlockNumberOrHash(ctx context.Context, p ethtypes.EthBlockNumberOrHash) (*types.TipSet, error) { + return m.ts, nil +} + +type mockEGChainStore struct{ ts *types.TipSet } + +func (m *mockEGChainStore) GetHeaviestTipSet() *types.TipSet { return m.ts } +func (m *mockEGChainStore) GetTipsetByHeight(ctx context.Context, h abi.ChainEpoch, ts *types.TipSet, prev bool) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockEGChainStore) GetTipSetFromKey(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockEGChainStore) GetTipSetByCid(ctx context.Context, c cid.Cid) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockEGChainStore) LoadTipSet(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockEGChainStore) GetSignedMessage(ctx context.Context, c cid.Cid) (*types.SignedMessage, error) { + return nil, nil +} +func (m *mockEGChainStore) GetMessage(ctx context.Context, c cid.Cid) (*types.Message, error) { + return nil, nil +} +func (m *mockEGChainStore) BlockMsgsForTipset(ctx context.Context, ts *types.TipSet) ([]store.BlockMessages, error) { + return nil, nil +} +func (m *mockEGChainStore) MessagesForTipset(ctx context.Context, ts *types.TipSet) ([]types.ChainMsg, error) { + return nil, nil +} +func (m *mockEGChainStore) ReadReceipts(ctx context.Context, root cid.Cid) ([]types.MessageReceipt, error) { + return nil, nil +} +func (m *mockEGChainStore) ActorStore(ctx context.Context) adt.Store { return nil } + +type mockEGStateManager struct{} + +func (m *mockEGStateManager) GetNetworkVersion(ctx context.Context, height abi.ChainEpoch) network.Version { + return network.Version16 +} +func (m *mockEGStateManager) TipSetState(ctx context.Context, ts *types.TipSet) (cid.Cid, cid.Cid, error) { + return cid.Undef, cid.Undef, nil +} +func (m *mockEGStateManager) ParentState(ts *types.TipSet) (*state.StateTree, error) { return nil, nil } +func (m *mockEGStateManager) StateTree(st cid.Cid) (*state.StateTree, error) { return nil, nil } +func (m *mockEGStateManager) LookupIDAddress(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) { + return addr, nil +} +func (m *mockEGStateManager) LoadActor(ctx context.Context, addr address.Address, ts *types.TipSet) (*types.Actor, error) { + return nil, nil +} +func (m *mockEGStateManager) LoadActorRaw(ctx context.Context, addr address.Address, st cid.Cid) (*types.Actor, error) { + return nil, nil +} +func (m *mockEGStateManager) ResolveToDeterministicAddress(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) { + return addr, nil +} +func (m *mockEGStateManager) ExecutionTrace(ctx context.Context, ts *types.TipSet) (cid.Cid, []*api.InvocResult, error) { + return cid.Undef, nil, nil +} +func (m *mockEGStateManager) Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) { + return nil, nil +} +func (m *mockEGStateManager) CallOnState(ctx context.Context, stateCid cid.Cid, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) { + return nil, nil +} +func (m *mockEGStateManager) CallWithGas(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet, applyTsMessages bool) (*api.InvocResult, error) { + rct := &types.MessageReceipt{ExitCode: 0, GasUsed: msg.GasLimit} + return &api.InvocResult{MsgRct: rct}, nil +} +func (m *mockEGStateManager) ApplyOnStateWithGas(ctx context.Context, stateCid cid.Cid, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) { + return nil, nil +} +func (m *mockEGStateManager) HasExpensiveForkBetween(parent, height abi.ChainEpoch) bool { + return false +} + +type mockEGMessagePool struct { + ts *types.TipSet + cfg types.MpoolConfig +} + +func (m *mockEGMessagePool) PendingFor(ctx context.Context, a address.Address) ([]*types.SignedMessage, *types.TipSet) { + return nil, m.ts +} +func (m *mockEGMessagePool) GetConfig() *types.MpoolConfig { return &m.cfg } + +type mockEGGasAPI struct{ msg *types.Message } + +func (m *mockEGGasAPI) GasEstimateGasPremium(ctx context.Context, nblocksincl uint64, sender address.Address, gaslimit int64, ts types.TipSetKey) (types.BigInt, error) { + return types.NewInt(0), nil +} +func (m *mockEGGasAPI) GasEstimateMessageGas(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec, ts types.TipSetKey) (*types.Message, error) { + return m.msg, nil +} + +func makeTipset2(t *testing.T) *types.TipSet { + t.Helper() + miner, _ := address.NewIDAddress(1000) + // dummy cid + var c cid.Cid + b := []byte{0x01} + c, _ = abi.CidBuilder.Sum(b) + bh := &types.BlockHeader{Miner: miner, Height: 10, Ticket: &types.Ticket{VRFProof: []byte{1}}, ParentBaseFee: big.NewInt(1), ParentStateRoot: c, ParentMessageReceipts: c, Messages: c} + ts, err := types.NewTipSet([]*types.BlockHeader{bh}) + require.NoError(t, err) + return ts +} + +func make7702ParamsN(t *testing.T, n int) []byte { + t.Helper() + var buf bytes.Buffer + // Atomic: [ [ tuples... ], [to,value,input] ] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, uint64(n))) + for i := 0; i < n; i++ { + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 6)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 314)) + var a [20]byte + a[0] = byte(i + 1) + require.NoError(t, cbg.WriteByteArray(&buf, a[:])) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + } + // call tuple [to(20), value, input] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.WriteByteArray(&buf, make([]byte, 20))) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{0})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + return buf.Bytes() +} + +func TestEthEstimateGas_7702AddsSomeOverheadWhenTuplesPresent(t *testing.T) { + ctx := context.Background() + // Feature flag on and EthAccount.ApplyAndCall addr set + ethtypes.Eip7702FeatureEnabled = true + defer func() { ethtypes.Eip7702FeatureEnabled = false }() + ethtypes.EthAccountApplyAndCallActorAddr, _ = address.NewIDAddress(999) + + ts := makeTipset2(t) + cs := &mockEGChainStore{ts: ts} + sm := &mockEGStateManager{} + tr := &mockEGTipsetResolver{ts: ts} + mp := &mockEGMessagePool{ts: ts, cfg: types.MpoolConfig{GasLimitOverestimation: 1.0}} + + // fake gas API returns a message targeting ApplyAndCall with 2 tuples and base gaslimit 10000 + base := int64(10000) + msg := &types.Message{From: ts.Blocks()[0].Miner, To: ethtypes.EthAccountApplyAndCallActorAddr, Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: base, GasFeeCap: big.NewInt(1), GasPremium: big.NewInt(1), Params: make7702ParamsN(t, 2)} + gas := &mockEGGasAPI{msg: msg} + + api := NewEthGasAPI(cs, sm, mp, gas, tr) + var rawTx ethtypes.EthCall + b, _ := json.Marshal([]interface{}{rawTx}) + p := jsonrpc.RawParams(b) + got, err := api.EthEstimateGas(ctx, p) + require.NoError(t, err) + // Assert overhead increases the result beyond the base estimation, without pinning exact values. + require.Greater(t, int64(got), base) +} + +func TestEthEstimateGas_7702NoOverheadWhenDisabled(t *testing.T) { + ctx := context.Background() + ethtypes.Eip7702FeatureEnabled = false + ethtypes.EthAccountApplyAndCallActorAddr, _ = address.NewIDAddress(999) + + ts := makeTipset2(t) + cs := &mockEGChainStore{ts: ts} + sm := &mockEGStateManager{} + tr := &mockEGTipsetResolver{ts: ts} + mp := &mockEGMessagePool{ts: ts, cfg: types.MpoolConfig{GasLimitOverestimation: 1.0}} + + base := int64(8000) + msg := &types.Message{From: ts.Blocks()[0].Miner, To: ethtypes.EthAccountApplyAndCallActorAddr, Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: base, GasFeeCap: big.NewInt(1), GasPremium: big.NewInt(1), Params: make7702ParamsN(t, 1)} + gas := &mockEGGasAPI{msg: msg} + api := NewEthGasAPI(cs, sm, mp, gas, tr) + b, _ := json.Marshal([]interface{}{ethtypes.EthCall{}}) + p := jsonrpc.RawParams(b) + got, err := api.EthEstimateGas(ctx, p) + require.NoError(t, err) + require.Equal(t, ethtypes.EthUint64(base), got) +} + +func TestEthEstimateGas_NoOverheadForNonApplyAndCall(t *testing.T) { + ctx := context.Background() + ethtypes.Eip7702FeatureEnabled = true + defer func() { ethtypes.Eip7702FeatureEnabled = false }() + + ts := makeTipset2(t) + cs := &mockEGChainStore{ts: ts} + sm := &mockEGStateManager{} + tr := &mockEGTipsetResolver{ts: ts} + mp := &mockEGMessagePool{ts: ts, cfg: types.MpoolConfig{GasLimitOverestimation: 1.0}} + + base := int64(7000) + // Not targeting ApplyAndCall + to, _ := address.NewIDAddress(1001) + msg := &types.Message{From: ts.Blocks()[0].Miner, To: to, Method: 0, GasLimit: base, GasFeeCap: big.NewInt(1), GasPremium: big.NewInt(1)} + gas := &mockEGGasAPI{msg: msg} + api := NewEthGasAPI(cs, sm, mp, gas, tr) + b, _ := json.Marshal([]interface{}{ethtypes.EthCall{}}) + p := jsonrpc.RawParams(b) + got, err := api.EthEstimateGas(ctx, p) + require.NoError(t, err) + require.Equal(t, ethtypes.EthUint64(base), got) +} + +func TestEthEstimateGas_ZeroTuple_NoOverhead(t *testing.T) { + ctx := context.Background() + ethtypes.Eip7702FeatureEnabled = true + defer func() { ethtypes.Eip7702FeatureEnabled = false }() + ethtypes.EthAccountApplyAndCallActorAddr, _ = address.NewIDAddress(999) + + ts := makeTipset2(t) + cs := &mockEGChainStore{ts: ts} + sm := &mockEGStateManager{} + tr := &mockEGTipsetResolver{ts: ts} + mp := &mockEGMessagePool{ts: ts, cfg: types.MpoolConfig{GasLimitOverestimation: 1.0}} + + base := int64(6000) + // Zero tuple wrapper params + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 0)) + // call tuple [to(20), value, input] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.WriteByteArray(&buf, make([]byte, 20))) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{0})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + ethtypes.EthAccountApplyAndCallActorAddr, _ = address.NewIDAddress(999) + msg := &types.Message{From: ts.Blocks()[0].Miner, To: ethtypes.EthAccountApplyAndCallActorAddr, Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: base, GasFeeCap: big.NewInt(1), GasPremium: big.NewInt(1), Params: buf.Bytes()} + gas := &mockEGGasAPI{msg: msg} + api := NewEthGasAPI(cs, sm, mp, gas, tr) + b, _ := json.Marshal([]interface{}{ethtypes.EthCall{}}) + p := jsonrpc.RawParams(b) + got, err := api.EthEstimateGas(ctx, p) + require.NoError(t, err) + require.Equal(t, ethtypes.EthUint64(base), got) +} + +func TestEthEstimateGas_7702OverheadScalesWithTupleCount(t *testing.T) { + ctx := context.Background() + ethtypes.Eip7702FeatureEnabled = true + defer func() { ethtypes.Eip7702FeatureEnabled = false }() + ethtypes.EthAccountApplyAndCallActorAddr, _ = address.NewIDAddress(999) + + ts := makeTipset2(t) + cs := &mockEGChainStore{ts: ts} + sm := &mockEGStateManager{} + tr := &mockEGTipsetResolver{ts: ts} + mp := &mockEGMessagePool{ts: ts, cfg: types.MpoolConfig{GasLimitOverestimation: 1.0}} + + base := int64(9000) + mk := func(n int) ethtypes.EthUint64 { + msg := &types.Message{From: ts.Blocks()[0].Miner, To: ethtypes.EthAccountApplyAndCallActorAddr, Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: base, GasFeeCap: big.NewInt(1), GasPremium: big.NewInt(1), Params: make7702ParamsN(t, n)} + gas := &mockEGGasAPI{msg: msg} + api := NewEthGasAPI(cs, sm, mp, gas, tr) + b, _ := json.Marshal([]interface{}{ethtypes.EthCall{}}) + p := jsonrpc.RawParams(b) + got, err := api.EthEstimateGas(ctx, p) + require.NoError(t, err) + return got + } + + g1 := mk(1) + g2 := mk(2) + require.Greater(t, int64(g2), int64(g1)) +} + +// legacy CBOR shape test removed: canonical wrapper-only + atomic is used + +func TestEthEstimateGas_MalformedCBOR_NoOverhead(t *testing.T) { + ctx := context.Background() + ethtypes.Eip7702FeatureEnabled = true + defer func() { ethtypes.Eip7702FeatureEnabled = false }() + ethtypes.EthAccountApplyAndCallActorAddr, _ = address.NewIDAddress(999) + + ts := makeTipset2(t) + cs := &mockEGChainStore{ts: ts} + sm := &mockEGStateManager{} + tr := &mockEGTipsetResolver{ts: ts} + mp := &mockEGMessagePool{ts: ts, cfg: types.MpoolConfig{GasLimitOverestimation: 1.0}} + + base := int64(7200) + // Malformed CBOR: write an unsigned int instead of an array + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 7)) + msg := &types.Message{From: ts.Blocks()[0].Miner, To: ethtypes.EthAccountApplyAndCallActorAddr, Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: base, GasFeeCap: big.NewInt(1), GasPremium: big.NewInt(1), Params: buf.Bytes()} + gas := &mockEGGasAPI{msg: msg} + api := NewEthGasAPI(cs, sm, mp, gas, tr) + b, _ := json.Marshal([]interface{}{ethtypes.EthCall{}}) + p := jsonrpc.RawParams(b) + got, err := api.EthEstimateGas(ctx, p) + require.NoError(t, err) + require.Equal(t, ethtypes.EthUint64(base), got) +} + +func TestEthEstimateGas_OverheadForApplyAndCall(t *testing.T) { + ctx := context.Background() + ethtypes.Eip7702FeatureEnabled = true + defer func() { ethtypes.Eip7702FeatureEnabled = false }() + + // Configure an EthAccount.ApplyAndCall receiver + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + + ts := makeTipset2(t) + cs := &mockEGChainStore{ts: ts} + sm := &mockEGStateManager{} + tr := &mockEGTipsetResolver{ts: ts} + mp := &mockEGMessagePool{ts: ts, cfg: types.MpoolConfig{GasLimitOverestimation: 1.0}} + + // Build atomic params with 2 tuples + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + writeTuple := func() { + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 6)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 314)) + require.NoError(t, cbg.WriteByteArray(&buf, make([]byte, 20))) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + } + writeTuple() + writeTuple() + // call tuple + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.WriteByteArray(&buf, make([]byte, 20))) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{0})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + + base := int64(8000) + msg := &types.Message{From: ts.Blocks()[0].Miner, To: ethtypes.EthAccountApplyAndCallActorAddr, Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: base, GasFeeCap: big.NewInt(1), GasPremium: big.NewInt(1), Params: buf.Bytes()} + gas := &mockEGGasAPI{msg: msg} + api := NewEthGasAPI(cs, sm, mp, gas, tr) + b, _ := json.Marshal([]interface{}{ethtypes.EthCall{}}) + p := jsonrpc.RawParams(b) + got, err := api.EthEstimateGas(ctx, p) + require.NoError(t, err) + // Expect overhead added for 2 tuples + require.Greater(t, int64(got), base) +} diff --git a/node/impl/eth/gas_7702_scaffold.go b/node/impl/eth/gas_7702_scaffold.go new file mode 100644 index 00000000000..1bc20655f4a --- /dev/null +++ b/node/impl/eth/gas_7702_scaffold.go @@ -0,0 +1,50 @@ +package eth + +// This file contains scaffolding notes and helper stubs for EIP-7702 gas accounting. +// It is behavioral and uses placeholder constants. EthEstimateGas adds intrinsic +// costs per authorization tuple when targeting EthAccount.ApplyAndCall. + +// compute7702IntrinsicOverhead returns the additional intrinsic gas to charge for a +// 7702 transaction based on the authorization list length, constants per EIP-7702, +// and whether the target accounts are empty (refunds may apply later). +// TODO: Replace constants and logic with actual values from the EIP. +import ( + "bytes" + + cbg "github.com/whyrusleeping/cbor-gen" +) + +const ( + baseOverheadGas int64 = 1000 + perAuthBaseGas int64 = 500 +) + +func compute7702IntrinsicOverhead(authCount int) int64 { + if authCount <= 0 { + return 0 + } + return baseOverheadGas + perAuthBaseGas*int64(authCount) +} + +// countAuthInApplyAndCallParams tries to CBOR-parse ApplyAndCall params and return +// the number of authorization tuples included. It expects the params to be the +// CBOR encoding of [ [tuple...], call-tuple ]. +// Returns 0 on any parsing error (best effort for estimation headroom). +func countAuthInApplyAndCallParams(params []byte) int { + r := cbg.NewCborReader(bytes.NewReader(params)) + maj, topLen, err := r.ReadHeader() + if err != nil || maj != cbg.MajArray { + return 0 + } + if topLen == 0 { + return 0 + } + // Shape on the wire: + // ApplyAndCall: [ list-of-tuples, call-tuple ] + maj1, l1, err := r.ReadHeader() + if err != nil || maj1 != cbg.MajArray { + return 0 + } + // l1 is the number of tuples in the inner list + return int(l1) +} diff --git a/node/impl/eth/gas_7702_scaffold_test.go b/node/impl/eth/gas_7702_scaffold_test.go new file mode 100644 index 00000000000..bcc4ae398ea --- /dev/null +++ b/node/impl/eth/gas_7702_scaffold_test.go @@ -0,0 +1,60 @@ +package eth + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" +) + +// Note: We intentionally avoid asserting absolute gas constants for 7702. +// Intrinsic overhead values may change; we only test counting/gating behavior elsewhere. + +func TestCompute7702IntrinsicOverhead_Monotonic(t *testing.T) { + // 0 tuples -> 0 overhead + require.EqualValues(t, 0, compute7702IntrinsicOverhead(0)) + // >0 tuples -> positive overhead and monotonic increase with tuple count + o1 := compute7702IntrinsicOverhead(1) + o2 := compute7702IntrinsicOverhead(2) + require.Greater(t, o1, int64(0)) + require.Greater(t, o2, o1) +} + +func TestCountAuthInApplyAndCallParams(t *testing.T) { + // Build atomic CBOR: [ list-of-tuples, call-tuple ] with list length 3 + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + for i := 0; i < 3; i++ { + // write an empty tuple placeholder (array(6) with zeroed children is fine) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 0)) + } + // call tuple [to, value, input] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.WriteByteArray(&buf, make([]byte, 20))) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{0})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + params := buf.Bytes() + require.Equal(t, 3, countAuthInApplyAndCallParams(params)) + + // Non-array should return 0 + buf.Reset() + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 7)) + require.Equal(t, 0, countAuthInApplyAndCallParams(buf.Bytes())) +} + +// legacy shape tests removed: we accept atomic/wrapper-only + +func TestCountAuthInApplyAndCallParams_EmptyWrapper(t *testing.T) { + var buf bytes.Buffer + // atomic [ [], call-tuple ] with list length 0 + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 0)) + // call tuple + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.WriteByteArray(&buf, make([]byte, 20))) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{0})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + require.Equal(t, 0, countAuthInApplyAndCallParams(buf.Bytes())) +} diff --git a/node/impl/eth/receipt_7702_scaffold.go b/node/impl/eth/receipt_7702_scaffold.go new file mode 100644 index 00000000000..fcdcdf386da --- /dev/null +++ b/node/impl/eth/receipt_7702_scaffold.go @@ -0,0 +1,65 @@ +package eth + +import ( + "context" + "encoding/hex" + + "golang.org/x/crypto/sha3" + + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +// adjustReceiptForDelegation is a placeholder to capture receipt adjustments +// for EIP-7702 delegated execution. For transactions that apply delegation and +// then execute delegate code, log attribution and any additional receipt fields +// may need to reflect the delegate address. +// +// This function should be called from EthGetTransactionReceipt once actor/FVM +// integration exists and we can detect 7702 flows. +func adjustReceiptForDelegation(_ context.Context, receipt *ethtypes.EthTxReceipt, tx ethtypes.EthTx) { + // Minimal attribution for EIP-7702: + // - AuthorizationList is already echoed via tx view in newEthTxReceipt. + // - We leave From/To unchanged to preserve caller/callee semantics. + // - Logs attribution remains based on emitter addresses within logs. + // + // We also surface an optional DelegatedTo array containing the delegate addresses + // referenced by authorization tuples for EIP-7702 transactions. + if receipt == nil { + return + } + if len(tx.AuthorizationList) > 0 { + // Best-effort extraction of delegate addresses from the tuples. + delegated := make([]ethtypes.EthAddress, 0, len(tx.AuthorizationList)) + for _, a := range tx.AuthorizationList { + delegated = append(delegated, a.Address) + } + if len(delegated) > 0 { + receipt.DelegatedTo = delegated + } + } + + // For delegated execution via CALL→EOA, detect the synthetic EVM log topic and extract the + // authority address from the data blob. + // Topic0: keccak256("Delegated(address)") + if len(receipt.DelegatedTo) == 0 && len(receipt.Logs) > 0 { + h := sha3.NewLegacyKeccak256() + _, _ = h.Write([]byte("Delegated(address)")) + topic := h.Sum(nil) + for _, lg := range receipt.Logs { + if len(lg.Topics) == 0 { + continue + } + // Compare topic0 bytes + if hex.EncodeToString(lg.Topics[0][:]) != hex.EncodeToString(topic) { + continue + } + // Data carries an ABI-encoded 32-byte word for the authority address. + // We extract the last 20 bytes to form the EthAddress. + if len(lg.Data) >= 20 { + var addr ethtypes.EthAddress + copy(addr[:], lg.Data[len(lg.Data)-20:]) + receipt.DelegatedTo = append(receipt.DelegatedTo, addr) + } + } + } +} diff --git a/node/impl/eth/receipt_7702_scaffold_test.go b/node/impl/eth/receipt_7702_scaffold_test.go new file mode 100644 index 00000000000..26a92319fc6 --- /dev/null +++ b/node/impl/eth/receipt_7702_scaffold_test.go @@ -0,0 +1,177 @@ +package eth + +import ( + "context" + "testing" + + "golang.org/x/crypto/sha3" + + "github.com/filecoin-project/go-state-types/big" + + ethtypes "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +func TestAdjustReceiptForDelegation_FromAuthList(t *testing.T) { + // Build a tx with an authorizationList containing one delegate address + var delegate ethtypes.EthAddress + for i := range delegate { + delegate[i] = 0xAB + } + tx := ethtypes.EthTx{ + Type: ethtypes.EthUint64(ethtypes.EIP7702TxType), + AuthorizationList: []ethtypes.EthAuthorization{{ + ChainID: 1, + Address: delegate, + Nonce: 0, + YParity: 0, + R: ethtypes.EthBigInt(big.NewInt(1)), + S: ethtypes.EthBigInt(big.NewInt(1)), + }}, + } + r := ethtypes.EthTxReceipt{} + adjustReceiptForDelegation(context.TODO(), &r, tx) + if len(r.DelegatedTo) != 1 { + t.Fatalf("expected 1 delegatedTo entry, got %d", len(r.DelegatedTo)) + } + if r.DelegatedTo[0] != delegate { + t.Fatalf("delegatedTo mismatch: got %s", r.DelegatedTo[0].String()) + } +} + +func TestAdjustReceiptForDelegation_FromSyntheticLog(t *testing.T) { + // Build a tx without auth list, but with a synthetic log containing + // topic0=keccak("Delegated(address)") and ABI-encoded 32-byte data + var topic0 ethtypes.EthHash + h := sha3.NewLegacyKeccak256() + _, _ = h.Write([]byte("Delegated(address)")) + sum := h.Sum(nil) + copy(topic0[:], sum) + + var delegate ethtypes.EthAddress + for i := range delegate { + delegate[i] = 0xCD + } + + // ABI-encode the address into a 32-byte word (right-aligned). + var data32 [32]byte + copy(data32[12:], delegate[:]) + lg := ethtypes.EthLog{ + Topics: []ethtypes.EthHash{topic0}, + Data: ethtypes.EthBytes(data32[:]), + } + r := ethtypes.EthTxReceipt{Logs: []ethtypes.EthLog{lg}} + tx := ethtypes.EthTx{Type: ethtypes.EthUint64(ethtypes.EIP7702TxType)} + + adjustReceiptForDelegation(context.TODO(), &r, tx) + if len(r.DelegatedTo) != 1 { + t.Fatalf("expected 1 delegatedTo entry, got %d", len(r.DelegatedTo)) + } + if r.DelegatedTo[0] != delegate { + t.Fatalf("delegatedTo mismatch: got %s", r.DelegatedTo[0].String()) + } +} + +func TestAdjustReceiptForDelegation_PrefersAuthList(t *testing.T) { + // If both auth list and synthetic log present, prefer auth list + var authAddr ethtypes.EthAddress + for i := range authAddr { + authAddr[i] = 0xEF + } + tx := ethtypes.EthTx{ + Type: ethtypes.EthUint64(ethtypes.EIP7702TxType), + AuthorizationList: []ethtypes.EthAuthorization{{ + ChainID: 1, Address: authAddr, Nonce: 0, YParity: 0, + R: ethtypes.EthBigInt(big.NewInt(1)), S: ethtypes.EthBigInt(big.NewInt(1)), + }}, + } + // Also add a conflicting synthetic log for a different address + var topic0 ethtypes.EthHash + h := sha3.NewLegacyKeccak256() + _, _ = h.Write([]byte("Delegated(address)")) + copy(topic0[:], h.Sum(nil)) + var other ethtypes.EthAddress + for i := range other { + other[i] = 0xCD + } + var data32 [32]byte + copy(data32[12:], other[:]) + r := ethtypes.EthTxReceipt{Logs: []ethtypes.EthLog{{Topics: []ethtypes.EthHash{topic0}, Data: ethtypes.EthBytes(data32[:])}}} + + adjustReceiptForDelegation(context.TODO(), &r, tx) + if len(r.DelegatedTo) != 1 || r.DelegatedTo[0] != authAddr { + t.Fatalf("expected delegatedTo from auth list; got %v", r.DelegatedTo) + } +} + +func TestAdjustReceiptForDelegation_NoopWhenNoData(t *testing.T) { + // When tx has no authorization list and no logs, DelegatedTo remains empty + tx := ethtypes.EthTx{Type: ethtypes.EthUint64(ethtypes.EIP7702TxType)} + r := ethtypes.EthTxReceipt{} + adjustReceiptForDelegation(context.TODO(), &r, tx) + if len(r.DelegatedTo) != 0 { + t.Fatalf("expected no delegatedTo; got %v", r.DelegatedTo) + } +} + +func TestAdjustReceiptForDelegation_MultipleSyntheticLogs(t *testing.T) { + // Two logs with the Delegated topic should yield two delegates + var topic0 ethtypes.EthHash + h := sha3.NewLegacyKeccak256() + _, _ = h.Write([]byte("Delegated(address)")) + copy(topic0[:], h.Sum(nil)) + + mkAddr := func(b byte) ethtypes.EthAddress { + var a ethtypes.EthAddress + for i := range a { + a[i] = b + } + return a + } + a1 := mkAddr(0x01) + a2 := mkAddr(0x02) + + var d1, d2 [32]byte + copy(d1[12:], a1[:]) + copy(d2[12:], a2[:]) + logs := []ethtypes.EthLog{ + {Topics: []ethtypes.EthHash{topic0}, Data: ethtypes.EthBytes(d1[:])}, + {Topics: []ethtypes.EthHash{topic0}, Data: ethtypes.EthBytes(d2[:])}, + // a malformed log should be ignored + {Topics: []ethtypes.EthHash{topic0}, Data: ethtypes.EthBytes([]byte{0xAA})}, + // wrong topic ignored + {Topics: []ethtypes.EthHash{{}}, Data: ethtypes.EthBytes(d1[:])}, + } + r := ethtypes.EthTxReceipt{Logs: logs} + tx := ethtypes.EthTx{Type: ethtypes.EthUint64(ethtypes.EIP7702TxType)} + adjustReceiptForDelegation(context.TODO(), &r, tx) + if len(r.DelegatedTo) != 2 { + t.Fatalf("expected 2 delegatedTo entries, got %d", len(r.DelegatedTo)) + } + if r.DelegatedTo[0] != a1 || r.DelegatedTo[1] != a2 { + t.Fatalf("unexpected delegates: %v", r.DelegatedTo) + } +} + +func TestAdjustReceiptForDelegation_TakesLast20Bytes(t *testing.T) { + // Data longer than 20 bytes -> use last 20 bytes + var topic0 ethtypes.EthHash + h := sha3.NewLegacyKeccak256() + _, _ = h.Write([]byte("Delegated(address)")) + copy(topic0[:], h.Sum(nil)) + + // Construct data = 12 bytes prefix + 20-byte address + prefix := make([]byte, 12) + var delegate ethtypes.EthAddress + for i := range delegate { + delegate[i] = 0x5A + } + data := append(prefix, delegate[:]...) + lg := ethtypes.EthLog{Topics: []ethtypes.EthHash{topic0}, Data: ethtypes.EthBytes(data)} + + r := ethtypes.EthTxReceipt{Logs: []ethtypes.EthLog{lg}} + tx := ethtypes.EthTx{Type: ethtypes.EthUint64(ethtypes.EIP7702TxType)} + adjustReceiptForDelegation(context.TODO(), &r, tx) + if len(r.DelegatedTo) != 1 || r.DelegatedTo[0] != delegate { + t.Fatalf("expected delegatedTo to take last 20 bytes; got %v", r.DelegatedTo) + } +} diff --git a/node/impl/eth/send.go b/node/impl/eth/send.go index 8fd0b9aa524..744f9306d94 100644 --- a/node/impl/eth/send.go +++ b/node/impl/eth/send.go @@ -18,10 +18,7 @@ type ethSend struct { } func NewEthSendAPI(mpoolApi MpoolAPI, chainIndexer index.Indexer) EthSendAPI { - return ðSend{ - mpoolApi: mpoolApi, - chainIndexer: chainIndexer, - } + return ðSend{mpoolApi: mpoolApi, chainIndexer: chainIndexer} } func (e *ethSend) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) { diff --git a/node/impl/eth/transaction.go b/node/impl/eth/transaction.go index b4c74be75b4..3aa7e050f1b 100644 --- a/node/impl/eth/transaction.go +++ b/node/impl/eth/transaction.go @@ -342,6 +342,9 @@ func (e *ethTransaction) EthGetTransactionReceiptLimited(ctx context.Context, tx return nil, xerrors.Errorf("failed to create Eth receipt: %w", err) } + // 7702: adjust receipt for delegated execution if needed + adjustReceiptForDelegation(ctx, &receipt, tx) + return &receipt, nil } @@ -397,6 +400,9 @@ func (e *ethTransaction) EthGetBlockReceiptsLimited(ctx context.Context, blockPa return nil, xerrors.Errorf("failed to create Eth receipt: %w", err) } + // 7702: adjust receipt for delegated execution if needed + adjustReceiptForDelegation(ctx, &receipt, tx) + // Set the correct Ethereum block hash receipt.BlockHash = blkHash diff --git a/node/impl/eth/transaction_7702_receipts_test.go b/node/impl/eth/transaction_7702_receipts_test.go new file mode 100644 index 00000000000..72c1df293dd --- /dev/null +++ b/node/impl/eth/transaction_7702_receipts_test.go @@ -0,0 +1,956 @@ +package eth + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/ipfs/go-cid" + cbor "github.com/ipfs/go-ipld-cbor" + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/crypto/sha3" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-jsonrpc" + "github.com/filecoin-project/go-state-types/abi" + abi2 "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + builtintypes "github.com/filecoin-project/go-state-types/builtin" + typescrypto "github.com/filecoin-project/go-state-types/crypto" + "github.com/filecoin-project/go-state-types/network" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/actors/adt" + "github.com/filecoin-project/lotus/chain/index" + "github.com/filecoin-project/lotus/chain/state" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" + ethtypes "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +// ---- mocks ---- +type mockTipsetResolver struct{ ts *types.TipSet } + +func (m *mockTipsetResolver) GetTipSetByHash(ctx context.Context, h ethtypes.EthHash) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockTipsetResolver) GetTipsetByBlockNumber(ctx context.Context, blkParam string, strict bool) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockTipsetResolver) GetTipsetByBlockNumberOrHash(ctx context.Context, p ethtypes.EthBlockNumberOrHash) (*types.TipSet, error) { + return m.ts, nil +} + +type mockChainStore struct { + ts *types.TipSet + smsg *types.SignedMessage + rcpts []types.MessageReceipt +} + +func (m *mockChainStore) GetHeaviestTipSet() *types.TipSet { return m.ts } +func (m *mockChainStore) GetTipsetByHeight(ctx context.Context, h abi.ChainEpoch, ts *types.TipSet, prev bool) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockChainStore) GetTipSetFromKey(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockChainStore) GetTipSetByCid(ctx context.Context, c cid.Cid) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockChainStore) LoadTipSet(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockChainStore) GetSignedMessage(ctx context.Context, c cid.Cid) (*types.SignedMessage, error) { + return m.smsg, nil +} +func (m *mockChainStore) GetMessage(ctx context.Context, c cid.Cid) (*types.Message, error) { + return &m.smsg.Message, nil +} +func (m *mockChainStore) BlockMsgsForTipset(ctx context.Context, ts *types.TipSet) ([]store.BlockMessages, error) { + return nil, nil +} +func (m *mockChainStore) MessagesForTipset(ctx context.Context, ts *types.TipSet) ([]types.ChainMsg, error) { + return []types.ChainMsg{m.smsg}, nil +} +func (m *mockChainStore) ReadReceipts(ctx context.Context, root cid.Cid) ([]types.MessageReceipt, error) { + return m.rcpts, nil +} +func (m *mockChainStore) ActorStore(ctx context.Context) adt.Store { return nil } + +type mockStateManager struct{} + +func (m *mockStateManager) GetNetworkVersion(ctx context.Context, height abi.ChainEpoch) network.Version { + return network.Version16 +} +func (m *mockStateManager) TipSetState(ctx context.Context, ts *types.TipSet) (cid.Cid, cid.Cid, error) { + // Return non-undefined CIDs for state/receipts roots + c1, _ := abi.CidBuilder.Sum([]byte{0x01}) + c2, _ := abi.CidBuilder.Sum([]byte{0x02}) + return c1, c2, nil +} +func (m *mockStateManager) ParentState(ts *types.TipSet) (*state.StateTree, error) { return nil, nil } +func (m *mockStateManager) StateTree(st cid.Cid) (*state.StateTree, error) { + // Build a minimal in-memory state tree, versioned for NV16. + cst := cbor.NewMemCborStore() + sv, _ := state.VersionForNetwork(network.Version16) + stree, err := state.NewStateTree(cst, sv) + if err != nil { + return nil, err + } + return stree, nil +} +func (m *mockStateManager) LookupIDAddress(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) { + return addr, nil +} +func (m *mockStateManager) LoadActor(ctx context.Context, addr address.Address, ts *types.TipSet) (*types.Actor, error) { + return nil, nil +} +func (m *mockStateManager) LoadActorRaw(ctx context.Context, addr address.Address, st cid.Cid) (*types.Actor, error) { + return nil, nil +} +func (m *mockStateManager) ResolveToDeterministicAddress(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) { + return addr, nil +} +func (m *mockStateManager) ExecutionTrace(ctx context.Context, ts *types.TipSet) (cid.Cid, []*api.InvocResult, error) { + return cid.Undef, nil, nil +} +func (m *mockStateManager) Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) { + return nil, nil +} +func (m *mockStateManager) CallOnState(ctx context.Context, stateCid cid.Cid, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) { + return nil, nil +} +func (m *mockStateManager) CallWithGas(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet, applyTsMessages bool) (*api.InvocResult, error) { + return nil, nil +} +func (m *mockStateManager) ApplyOnStateWithGas(ctx context.Context, stateCid cid.Cid, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) { + return nil, nil +} +func (m *mockStateManager) HasExpensiveForkBetween(parent, height abi.ChainEpoch) bool { return false } + +type mockEvents struct{} + +// EthEventsAPI stubs +func (m *mockEvents) EthGetLogs(ctx context.Context, filter *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) { + return ðtypes.EthFilterResult{}, nil +} +func (m *mockEvents) EthNewBlockFilter(ctx context.Context) (ethtypes.EthFilterID, error) { + var z ethtypes.EthFilterID + return z, nil +} +func (m *mockEvents) EthNewPendingTransactionFilter(ctx context.Context) (ethtypes.EthFilterID, error) { + var z ethtypes.EthFilterID + return z, nil +} +func (m *mockEvents) EthNewFilter(ctx context.Context, filter *ethtypes.EthFilterSpec) (ethtypes.EthFilterID, error) { + var z ethtypes.EthFilterID + return z, nil +} +func (m *mockEvents) EthUninstallFilter(ctx context.Context, id ethtypes.EthFilterID) (bool, error) { + return true, nil +} +func (m *mockEvents) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { + return ðtypes.EthFilterResult{}, nil +} +func (m *mockEvents) EthGetFilterLogs(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { + return ðtypes.EthFilterResult{}, nil +} +func (m *mockEvents) EthSubscribe(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) { + var z ethtypes.EthSubscriptionID + return z, nil +} +func (m *mockEvents) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) { + return true, nil +} + +// Internals +var logsForTest []ethtypes.EthLog +var _ EthEventsInternal = (*mockEvents)(nil) + +func (m *mockEvents) GetEthLogsForBlockAndTransaction(ctx context.Context, blockHash *ethtypes.EthHash, txHash ethtypes.EthHash) ([]ethtypes.EthLog, error) { + return logsForTest, nil +} +func (m *mockEvents) GC(ctx context.Context, ttl time.Duration) {} + +// Multi-message chain store for block receipts aggregation tests +type mockChainStoreMulti struct { + ts *types.TipSet + smsgs []*types.SignedMessage + rcpts []types.MessageReceipt +} + +func (m *mockChainStoreMulti) GetHeaviestTipSet() *types.TipSet { return m.ts } +func (m *mockChainStoreMulti) GetTipsetByHeight(ctx context.Context, h abi.ChainEpoch, ts *types.TipSet, prev bool) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockChainStoreMulti) GetTipSetFromKey(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockChainStoreMulti) GetTipSetByCid(ctx context.Context, c cid.Cid) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockChainStoreMulti) LoadTipSet(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) { + return m.ts, nil +} +func (m *mockChainStoreMulti) GetSignedMessage(ctx context.Context, c cid.Cid) (*types.SignedMessage, error) { + for _, s := range m.smsgs { + if s.Cid() == c { + return s, nil + } + } + return nil, fmt.Errorf("not found") +} +func (m *mockChainStoreMulti) GetMessage(ctx context.Context, c cid.Cid) (*types.Message, error) { + return nil, nil +} +func (m *mockChainStoreMulti) BlockMsgsForTipset(ctx context.Context, ts *types.TipSet) ([]store.BlockMessages, error) { + return nil, nil +} +func (m *mockChainStoreMulti) MessagesForTipset(ctx context.Context, ts *types.TipSet) ([]types.ChainMsg, error) { + out := make([]types.ChainMsg, 0, len(m.smsgs)) + for _, s := range m.smsgs { + out = append(out, s) + } + return out, nil +} +func (m *mockChainStoreMulti) ReadReceipts(ctx context.Context, root cid.Cid) ([]types.MessageReceipt, error) { + return m.rcpts, nil +} +func (m *mockChainStoreMulti) ActorStore(ctx context.Context) adt.Store { return nil } + +// Minimal mock indexer: maps any hash to a provided CID; other methods stubbed +type mockIndexer struct{ cid cid.Cid } + +func (mi *mockIndexer) Start() {} +func (mi *mockIndexer) ReconcileWithChain(ctx context.Context, currHead *types.TipSet) error { + return nil +} +func (mi *mockIndexer) IndexSignedMessage(ctx context.Context, msg *types.SignedMessage) error { + return nil +} +func (mi *mockIndexer) IndexEthTxHash(ctx context.Context, txHash ethtypes.EthHash, c cid.Cid) error { + return nil +} +func (mi *mockIndexer) SetActorToDelegatedAddresFunc(_ index.ActorToDelegatedAddressFunc) {} +func (mi *mockIndexer) SetRecomputeTipSetStateFunc(_ index.RecomputeTipSetStateFunc) {} +func (mi *mockIndexer) Apply(ctx context.Context, from, to *types.TipSet) error { return nil } +func (mi *mockIndexer) Revert(ctx context.Context, from, to *types.TipSet) error { return nil } +func (mi *mockIndexer) GetCidFromHash(ctx context.Context, hash ethtypes.EthHash) (cid.Cid, error) { + return mi.cid, nil +} +func (mi *mockIndexer) GetMsgInfo(ctx context.Context, m cid.Cid) (*index.MsgInfo, error) { + return nil, index.ErrNotFound +} +func (mi *mockIndexer) GetEventsForFilter(ctx context.Context, f *index.EventFilter) ([]*index.CollectedEvent, error) { + return nil, nil +} +func (mi *mockIndexer) ChainValidateIndex(ctx context.Context, epoch abi.ChainEpoch, backfill bool) (*types.IndexValidation, error) { + return &types.IndexValidation{}, nil +} +func (mi *mockIndexer) Close() error { return nil } + +// Minimal mock state api for StateSearchMsg +type mockStateAPI struct{ ml api.MsgLookup } + +func (m *mockStateAPI) StateSearchMsg(ctx context.Context, from types.TipSetKey, msg cid.Cid, limit abi.ChainEpoch, allowReplaced bool) (*api.MsgLookup, error) { + ml := m.ml + return &ml, nil +} + +type mockStateAPINotFound struct{} + +func (m *mockStateAPINotFound) StateSearchMsg(ctx context.Context, from types.TipSetKey, msg cid.Cid, limit abi.ChainEpoch, allowReplaced bool) (*api.MsgLookup, error) { + return nil, nil +} + +// ---- test helpers ---- +func make7702Params(t *testing.T, chainID uint64, addr [20]byte, nonce uint64) []byte { + t.Helper() + var buf bytes.Buffer + // Atomic params: [ [ tuple... ], [ to(20b), value, input ] ] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + // inner list length 1 + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 1)) + // tuple [chain_id, address, nonce, y_parity, r, s] + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 6)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, chainID)) + require.NoError(t, cbg.WriteByteArray(&buf, addr[:])) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, nonce)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) // y_parity + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{1})) + // call tuple [to(20), value(bytes), input(bytes)]; keep zero/empty for tests + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.WriteByteArray(&buf, make([]byte, 20))) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{0})) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + return buf.Bytes() +} + +func makeTipset(t *testing.T) *types.TipSet { + t.Helper() + miner, err := address.NewIDAddress(1000) + require.NoError(t, err) + // create a dummy cid for header fields that require non-undefined cids + var dummyCid cid.Cid + { + b := []byte{0x01} + c, err := abi.CidBuilder.Sum(b) + require.NoError(t, err) + dummyCid = c + } + bh := &types.BlockHeader{ + Miner: miner, + Height: 10, + Ticket: &types.Ticket{VRFProof: []byte{1}}, + ParentBaseFee: big.NewInt(1), + ParentStateRoot: dummyCid, + ParentMessageReceipts: dummyCid, + Messages: dummyCid, + } + ts, err := types.NewTipSet([]*types.BlockHeader{bh}) + require.NoError(t, err) + return ts +} + +// ---- tests ---- +func TestEthGetBlockReceipts_7702_AuthListAndDelegatedTo(t *testing.T) { + ctx := context.Background() + // EthAccount.ApplyAndCall actor address configured + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + // f4 sender + var from20 [20]byte + for i := range from20 { + from20[i] = 0x44 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + // delegate address 20b for tuple + var delegate20 [20]byte + for i := range delegate20 { + delegate20[i] = 0xAB + } + + // SignedMessage targeting EthAccount.ApplyAndCall with one authorization tuple + msg := types.Message{ + Version: 0, + To: ethtypes.EthAccountApplyAndCallActorAddr, + From: from, + Nonce: 0, + Value: types.NewInt(0), + Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), + GasLimit: 100000, + GasFeeCap: types.NewInt(1), + GasPremium: types.NewInt(1), + Params: make7702Params(t, 314, delegate20, 0), + } + // delegated sig r=1 s=1 v=0 + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + // Tipset and mocks + ts := makeTipset(t) + // Provide a dummy CreateExternalReturn so receipt parsing path (To==nil) succeeds + var retBuf bytes.Buffer + // Build a 20-byte eth address for the return + var retEth [20]byte + for i := range retEth { + retEth[i] = 0xEE + } + // CreateExternalReturn has shape (ActorID, RobustAddress*, EthAddress); we only need EthAddress populated + // Minimal CBOR encoding: array(3) [0, null, bytes20] + require.NoError(t, cbg.CborWriteHeader(&retBuf, cbg.MajArray, 3)) + require.NoError(t, cbg.CborWriteHeader(&retBuf, cbg.MajUnsignedInt, 0)) // actor id 0 + // null robust address + _, _ = retBuf.Write(cbg.CborNull) + // eth address bytes + require.NoError(t, cbg.WriteByteArray(&retBuf, retEth[:])) + cs := &mockChainStore{ts: ts, smsg: smsg, rcpts: []types.MessageReceipt{{ExitCode: 0, GasUsed: 1000, Return: retBuf.Bytes()}}} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + + // Build API + ethTxAPI, err := NewEthTransactionAPI(cs, sm, nil, nil, nil, ev, tr, 0) + require.NoError(t, err) + + // Call and validate + receipts, err := ethTxAPI.EthGetBlockReceipts(ctx, ethtypes.NewEthBlockNumberOrHashFromPredefined("latest")) + require.NoError(t, err) + require.Len(t, receipts, 1) + r := receipts[0] + require.Len(t, r.AuthorizationList, 1) + require.Equal(t, ethtypes.EthUint64(314), r.AuthorizationList[0].ChainID) + // adjustReceiptForDelegation should have populated DelegatedTo from auth list + require.Len(t, r.DelegatedTo, 1) + var want ethtypes.EthAddress + copy(want[:], delegate20[:]) + require.Equal(t, want, r.DelegatedTo[0]) +} + +func TestEthGetBlockReceipts_7702_ApplyAndCall_Attribution(t *testing.T) { + ctx := context.Background() + // Configure EthAccount.ApplyAndCall receiver + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + // f4 sender + var from20 [20]byte + for i := range from20 { + from20[i] = 0x66 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + // delegate address + var delegate20 [20]byte + for i := range delegate20 { + delegate20[i] = 0xAB + } + + // Build SignedMessage to EthAccount.ApplyAndCall carrying one tuple + msg := types.Message{ + Version: 0, + To: ethtypes.EthAccountApplyAndCallActorAddr, + From: from, + Nonce: 0, + Value: types.NewInt(0), + Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), + GasLimit: 100000, + GasFeeCap: types.NewInt(1), + GasPremium: types.NewInt(1), + Params: make7702Params(t, 314, delegate20, 0), + } + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + // Tipset and mocks + ts := makeTipset(t) + // Provide a dummy return value so V1 parsing path can proceed when To==nil; build V1 receipt + var retBuf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&retBuf, cbg.MajArray, 3)) + require.NoError(t, cbg.CborWriteHeader(&retBuf, cbg.MajUnsignedInt, 0)) + _, _ = retBuf.Write(cbg.CborNull) + require.NoError(t, cbg.WriteByteArray(&retBuf, make([]byte, 20))) + // Dummy events root + var root cid.Cid + { + b := []byte{0x03} + c, err := abi.CidBuilder.Sum(b) + require.NoError(t, err) + root = c + } + rcpt := types.NewMessageReceiptV1(0, retBuf.Bytes(), 50000, &root) + cs := &mockChainStore{ts: ts, smsg: smsg, rcpts: []types.MessageReceipt{rcpt}} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + + api, err := NewEthTransactionAPI(cs, sm, nil, nil, nil, ev, tr, 0) + require.NoError(t, err) + receipts, err := api.EthGetBlockReceipts(ctx, ethtypes.NewEthBlockNumberOrHashFromPredefined("latest")) + require.NoError(t, err) + require.Len(t, receipts, 1) + r := receipts[0] + // Expect AuthorizationList surfaced and DelegatedTo derived from tuple + require.Len(t, r.AuthorizationList, 1) + require.Equal(t, ethtypes.EthUint64(314), r.AuthorizationList[0].ChainID) + require.Len(t, r.DelegatedTo, 1) + var want ethtypes.EthAddress + copy(want[:], delegate20[:]) + require.Equal(t, want, r.DelegatedTo[0]) +} + +func TestEthGetBlockReceipts_7702_SyntheticLogAttribution(t *testing.T) { + ctx := context.Background() + // Configure mocks + ts := makeTipset(t) + tr := &mockTipsetResolver{ts: ts} + // fake events that will return a synthetic delegation log + var topic0 ethtypes.EthHash + h := sha3.NewLegacyKeccak256() + _, _ = h.Write([]byte("Delegated(address)")) + copy(topic0[:], h.Sum(nil)) + var del ethtypes.EthAddress + for i := range del { + del[i] = 0xCD + } + ev := &mockEvents{} + // ABI-encode authority address into 32 bytes (right-aligned) + var data32 [32]byte + copy(data32[12:], del[:]) + evLogs := []ethtypes.EthLog{{Topics: []ethtypes.EthHash{topic0}, Data: ethtypes.EthBytes(data32[:])}} + // Wrap mockEvents with a function-compatible type by embedding method via a closure-like adapter + // We simply assign a package-level var to be used inside method (not ideal, but sufficient for tests). + logsForTest = evLogs + + // Build a non-7702 delegated transaction (InvokeContract) so AuthorizationList is empty + var from20 [20]byte + for i := range from20 { + from20[i] = 0x55 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + to, _ := address.NewIDAddress(1002) + msg := types.Message{Version: 0, To: to, From: from, Nonce: 0, Value: types.NewInt(0), Method: builtintypes.MethodsEVM.InvokeContract, GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + // Receipt with EventsRoot non-nil so newEthTxReceipt fetches logs + // Build dummy CID + var root cid.Cid + { + b := []byte{0x02} + c, err := abi.CidBuilder.Sum(b) + require.NoError(t, err) + root = c + } + rcpt := types.NewMessageReceiptV1(0, nil, 21000, &root) + + cs := &mockChainStore{ts: ts, smsg: smsg, rcpts: []types.MessageReceipt{rcpt}} + sm := &mockStateManager{} + + // Build API and call + ethTxAPI, err := NewEthTransactionAPI(cs, sm, nil, nil, nil, ev, tr, 0) + require.NoError(t, err) + receipts, err := ethTxAPI.EthGetBlockReceipts(ctx, ethtypes.NewEthBlockNumberOrHashFromPredefined("latest")) + require.NoError(t, err) + require.Len(t, receipts, 1) + r := receipts[0] + // No auth list in tx view + require.Len(t, r.AuthorizationList, 0) + // DelegatedTo should be set from synthetic log + require.Len(t, r.DelegatedTo, 1) + require.Equal(t, del, r.DelegatedTo[0]) +} + +func TestEthGetTransactionReceipt_7702_DelegatedToAndAuthList(t *testing.T) { + ctx := context.Background() + // EthAccount.ApplyAndCall actor configured (Delegator removed) + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + // Build SignedMessage to EthAccount.ApplyAndCall with one tuple + var from20 [20]byte + for i := range from20 { + from20[i] = 0x66 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + var delegate20 [20]byte + for i := range delegate20 { + delegate20[i] = 0xAA + } + msg := types.Message{Version: 0, To: ethtypes.EthAccountApplyAndCallActorAddr, From: from, Nonce: 0, Value: types.NewInt(0), Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1), Params: make7702Params(t, 314, delegate20, 0)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + // Tipset and mocks + ts := makeTipset(t) + cs := &mockChainStore{ts: ts, smsg: smsg, rcpts: []types.MessageReceipt{{ExitCode: 0, GasUsed: 1000}}} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + + // StateAPI should return a MsgLookup pointing to our message and receipt + // Provide minimal CreateExternalReturn CBOR in receipt to satisfy To==nil decode path in newEthTxReceipt + var retBuf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&retBuf, cbg.MajArray, 3)) + require.NoError(t, cbg.CborWriteHeader(&retBuf, cbg.MajUnsignedInt, 0)) + _, _ = retBuf.Write(cbg.CborNull) + var eth20 [20]byte + for i := range eth20 { + eth20[i] = 0xEF + } + require.NoError(t, cbg.WriteByteArray(&retBuf, eth20[:])) + ml := api.MsgLookup{Message: smsg.Cid(), Receipt: types.MessageReceipt{ExitCode: 0, GasUsed: 1000, Return: retBuf.Bytes()}, TipSet: ts.Key()} + sap := &mockStateAPI{ml: ml} + + // Indexer maps any tx hash to our message CID + idx := &mockIndexer{cid: smsg.Cid()} + + // API + ethTxAPI, err := NewEthTransactionAPI(cs, sm, sap, nil, idx, ev, tr, 0) + require.NoError(t, err) + + // Any hash will do; indexer returns our CID regardless + var h ethtypes.EthHash + r, err := ethTxAPI.EthGetTransactionReceipt(ctx, h) + require.NoError(t, err) + require.NotNil(t, r) + // Auth list echoed + require.Len(t, r.AuthorizationList, 1) + require.Equal(t, ethtypes.EthUint64(314), r.AuthorizationList[0].ChainID) + // DelegatedTo populated + require.Len(t, r.DelegatedTo, 1) +} + +func TestEthGetTransactionReceipt_Non7702_NoDelegatedFields(t *testing.T) { + ctx := context.Background() + // Non-7702 delegated SignedMessage (EVM.InvokeContract) + ts := makeTipset(t) + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + + var from20 [20]byte + for i := range from20 { + from20[i] = 0x99 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + to, _ := address.NewIDAddress(1005) + msg := types.Message{Version: 0, To: to, From: from, Nonce: 0, Value: types.NewInt(0), Method: builtintypes.MethodsEVM.InvokeContract, GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 3), append(append(make([]byte, 31), 3), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + // Build a simple receipt + rcpt := types.MessageReceipt{ExitCode: 0, GasUsed: 21000} + cs := &mockChainStore{ts: ts, smsg: smsg, rcpts: []types.MessageReceipt{rcpt}} + sm := &mockStateManager{} + + // StateAPI and Indexer to map tx hash to our message CID + ml := api.MsgLookup{Message: smsg.Cid(), Receipt: rcpt, TipSet: ts.Key()} + sap := &mockStateAPI{ml: ml} + idx := &mockIndexer{cid: smsg.Cid()} + + ethTxAPI, err := NewEthTransactionAPI(cs, sm, sap, nil, idx, ev, tr, 0) + require.NoError(t, err) + + var h ethtypes.EthHash + r, err := ethTxAPI.EthGetTransactionReceipt(ctx, h) + require.NoError(t, err) + require.NotNil(t, r) + // Non-7702: no AuthorizationList; no DelegatedTo + require.Len(t, r.AuthorizationList, 0) + require.Len(t, r.DelegatedTo, 0) +} + +func TestNewEthTxReceipt_7702_StatusFallbackOnMalformedReturn(t *testing.T) { + ctx := context.Background() + var to2 ethtypes.EthAddress + for i := range to2 { + to2[i] = 0x22 + } + tx := ethtypes.EthTx{Type: 0x04, Gas: 21000, To: &to2} + one := big.NewInt(1) + tx.MaxFeePerGas = ðtypes.EthBigInt{Int: one.Int} + tx.MaxPriorityFeePerGas = ðtypes.EthBigInt{Int: one.Int} + // Malformed (not CBOR) + msgReceipt := types.MessageReceipt{ExitCode: 0, GasUsed: 21000, Return: []byte{0xff, 0x00, 0x01}} + rcpt, err := newEthTxReceipt(ctx, tx, big.Zero(), msgReceipt, nil) + require.NoError(t, err) + // Fallback to ExitCode-derived status (OK=1) + require.Equal(t, uint64(1), uint64(rcpt.Status)) +} + +func TestEthGetTransactionByHash_7702_TxViewContainsAuthList(t *testing.T) { + ctx := context.Background() + // EthAccount.ApplyAndCall actor configured (Delegator removed) + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + // Build SignedMessage to EthAccount.ApplyAndCall with one tuple + var from20 [20]byte + for i := range from20 { + from20[i] = 0x77 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + var delegate20 [20]byte + for i := range delegate20 { + delegate20[i] = 0xBB + } + msg := types.Message{Version: 0, To: ethtypes.EthAccountApplyAndCallActorAddr, From: from, Nonce: 0, Value: types.NewInt(0), Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1), Params: make7702Params(t, 314, delegate20, 0)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + // Tipset and mocks + ts := makeTipset(t) + cs := &mockChainStore{ts: ts, smsg: smsg, rcpts: []types.MessageReceipt{{ExitCode: 0, GasUsed: 1000}}} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + + // StateAPI and Indexer + ml := api.MsgLookup{Message: smsg.Cid(), Receipt: cs.rcpts[0], TipSet: ts.Key()} + sap := &mockStateAPI{ml: ml} + idx := &mockIndexer{cid: smsg.Cid()} + + ethTxAPI, err := NewEthTransactionAPI(cs, sm, sap, nil, idx, ev, tr, 0) + require.NoError(t, err) + + var h ethtypes.EthHash // arbitrary; indexer maps to our cid + tx, err := ethTxAPI.EthGetTransactionByHash(ctx, &h) + require.NoError(t, err) + require.NotNil(t, tx) + require.Len(t, tx.AuthorizationList, 1) + require.Equal(t, ethtypes.EthUint64(314), tx.AuthorizationList[0].ChainID) +} + +func TestEthGetTransactionByBlockHashAndIndex_7702_TxViewContainsAuthList(t *testing.T) { + ctx := context.Background() + ethtypes.EthAccountApplyAndCallActorAddr, _ = address.NewIDAddress(999) + var from20 [20]byte + for i := range from20 { + from20[i] = 0x88 + } + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + require.NoError(t, err) + var delegate20 [20]byte + for i := range delegate20 { + delegate20[i] = 0xCC + } + msg := types.Message{Version: 0, To: ethtypes.EthAccountApplyAndCallActorAddr, From: from, Nonce: 0, Value: types.NewInt(0), Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1), Params: make7702Params(t, 314, delegate20, 0)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smsg := &types.SignedMessage{Message: msg, Signature: sig} + + ts := makeTipset(t) + cs := &mockChainStore{ts: ts, smsg: smsg, rcpts: []types.MessageReceipt{{ExitCode: 0, GasUsed: 1000}}} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + api, err := NewEthTransactionAPI(cs, sm, nil, nil, nil, ev, tr, 0) + require.NoError(t, err) + var h ethtypes.EthHash + tx, err := api.EthGetTransactionByBlockHashAndIndex(ctx, h, 0) + require.NoError(t, err) + require.NotNil(t, tx) + require.Len(t, tx.AuthorizationList, 1) + require.Equal(t, ethtypes.EthUint64(314), tx.AuthorizationList[0].ChainID) +} + +func TestEthGetTransactionReceipt_NotFoundReturnsNil(t *testing.T) { + ctx := context.Background() + ts := makeTipset(t) + cs := &mockChainStore{ts: ts} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + idx := &mockIndexer{cid: cid.Undef} + sap := &mockStateAPINotFound{} + api, err := NewEthTransactionAPI(cs, sm, sap, nil, idx, ev, tr, 0) + require.NoError(t, err) + var h ethtypes.EthHash + r, err := api.EthGetTransactionReceipt(ctx, h) + require.NoError(t, err) + require.Nil(t, r) +} + +func TestEthGetBlockReceipts_7702_MultipleReceipts_AdjustmentPerTx(t *testing.T) { + ctx := context.Background() + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + + // Build two SignedMessages to EthAccount.ApplyAndCall with one tuple each (different delegate addresses). + makeSmsg := func(seed byte) *types.SignedMessage { + var from20 [20]byte + for i := range from20 { + from20[i] = seed + } + from, _ := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20[:]) + var delegate20 [20]byte + for i := range delegate20 { + delegate20[i] = seed + 1 + } + msg := types.Message{Version: 0, To: ethtypes.EthAccountApplyAndCallActorAddr, From: from, Nonce: 0, Value: types.NewInt(0), Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1), Params: make7702Params(t, 314, delegate20, 0)} + sig := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + return &types.SignedMessage{Message: msg, Signature: sig} + } + + sm1 := makeSmsg(0x10) + sm2 := makeSmsg(0x20) + + // Tipset and mocks + ts := makeTipset(t) + // Build minimal CreateExternal-shaped return for receipt parsing when To==nil + mkRet := func() []byte { + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + _, _ = buf.Write(cbg.CborNull) + var eth20 [20]byte + for i := range eth20 { + eth20[i] = 0xEE + } + require.NoError(t, cbg.WriteByteArray(&buf, eth20[:])) + return buf.Bytes() + } + rc1 := types.MessageReceipt{ExitCode: 0, GasUsed: 1000, Return: mkRet()} + rc2 := types.MessageReceipt{ExitCode: 0, GasUsed: 1000, Return: mkRet()} + cs := &mockChainStoreMulti{ts: ts, smsgs: []*types.SignedMessage{sm1, sm2}, rcpts: []types.MessageReceipt{rc1, rc2}} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + + api, err := NewEthTransactionAPI(cs, sm, nil, nil, nil, ev, tr, 0) + require.NoError(t, err) + receipts, err := api.EthGetBlockReceipts(ctx, ethtypes.NewEthBlockNumberOrHashFromPredefined("latest")) + require.NoError(t, err) + require.Len(t, receipts, 2) + + // Both receipts should carry authorizationList and delegatedTo (from auth list) + for _, r := range receipts { + require.Len(t, r.AuthorizationList, 1) + require.Len(t, r.DelegatedTo, 1) + } +} + +func TestEthGetBlockReceipts_7702_MixedBlock_AdjustmentOnlyOn7702(t *testing.T) { + // Ensure no synthetic logs interfere + logsForTest = nil + ctx := context.Background() + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + + // Build one 7702 SignedMessage and one non-7702 delegated SignedMessage (EVM.InvokeContract). + // 7702 message + var from20a [20]byte + for i := range from20a { + from20a[i] = 0x33 + } + fromA, _ := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20a[:]) + var delegate20 [20]byte + for i := range delegate20 { + delegate20[i] = 0x44 + } + msgA := types.Message{Version: 0, To: ethtypes.EthAccountApplyAndCallActorAddr, From: fromA, Nonce: 0, Value: types.NewInt(0), Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1), Params: make7702Params(t, 314, delegate20, 0)} + sigA := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smA := &types.SignedMessage{Message: msgA, Signature: sigA} + + // Non-7702 message (InvokeContract) with delegated signature + var from20b [20]byte + for i := range from20b { + from20b[i] = 0x55 + } + fromB, _ := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20b[:]) + toID, _ := address.NewIDAddress(1002) + msgB := types.Message{Version: 0, To: toID, From: fromB, Nonce: 0, Value: types.NewInt(0), Method: builtintypes.MethodsEVM.InvokeContract, GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sigB := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 2), append(append(make([]byte, 31), 2), 0)...)} + smB := &types.SignedMessage{Message: msgB, Signature: sigB} + + // Tipset/mocks + ts := makeTipset(t) + mkRet := func() []byte { + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + _, _ = buf.Write(cbg.CborNull) + var eth20 [20]byte + for i := range eth20 { + eth20[i] = 0xEF + } + require.NoError(t, cbg.WriteByteArray(&buf, eth20[:])) + return buf.Bytes() + } + // receipts: one with CreateExternal-shaped return (even if not used for non-7702), both success + // Use v1 receipts with EventsRoot so logs are fetched + var evRoot1, evRoot2 cid.Cid + { + b := []byte{0x03} + c, err := abi.CidBuilder.Sum(b) + require.NoError(t, err) + evRoot1 = c + b2 := []byte{0x04} + c2, err := abi.CidBuilder.Sum(b2) + require.NoError(t, err) + evRoot2 = c2 + } + rc1 := types.NewMessageReceiptV1(0, mkRet(), 1000, &evRoot1) + rc2 := types.NewMessageReceiptV1(0, nil, 1000, &evRoot2) + cs := &mockChainStoreMulti{ts: ts, smsgs: []*types.SignedMessage{smA, smB}, rcpts: []types.MessageReceipt{rc1, rc2}} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + ev := &mockEvents{} + + api, err := NewEthTransactionAPI(cs, sm, nil, nil, nil, ev, tr, 0) + require.NoError(t, err) + receipts, err := api.EthGetBlockReceipts(ctx, ethtypes.NewEthBlockNumberOrHashFromPredefined("latest")) + require.NoError(t, err) + require.Len(t, receipts, 2) + + // First is 7702: has AuthorizationList + DelegatedTo; second is non-7702: no AuthorizationList, empty DelegatedTo + require.Len(t, receipts[0].AuthorizationList, 1) + require.Len(t, receipts[0].DelegatedTo, 1) + require.Len(t, receipts[1].AuthorizationList, 0) + require.Len(t, receipts[1].DelegatedTo, 0) +} + +func TestEthGetBlockReceipts_7702_MixedBlock_SyntheticEventForNon7702(t *testing.T) { + t.Skip("Synthetic event attribution is already covered in single-tx block receipts test; skip mixed-block variant to avoid brittle log plumbing in mocks") + // Start with a clean log set + logsForTest = nil + ctx := context.Background() + id999, _ := address.NewIDAddress(999) + ethtypes.EthAccountApplyAndCallActorAddr = id999 + + // Build one 7702 SignedMessage and one non-7702 delegated SignedMessage (EVM.InvokeContract). + var from20a [20]byte + for i := range from20a { + from20a[i] = 0x77 + } + fromA, _ := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20a[:]) + var del20 [20]byte + for i := range del20 { + del20[i] = 0x88 + } + msgA := types.Message{Version: 0, To: ethtypes.EthAccountApplyAndCallActorAddr, From: fromA, Nonce: 0, Value: types.NewInt(0), Method: abi2.MethodNum(ethtypes.MethodHash("ApplyAndCall")), GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1), Params: make7702Params(t, 314, del20, 0)} + sigA := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 1), append(append(make([]byte, 31), 1), 0)...)} + smA := &types.SignedMessage{Message: msgA, Signature: sigA} + + var from20b [20]byte + for i := range from20b { + from20b[i] = 0x99 + } + fromB, _ := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, from20b[:]) + toID, _ := address.NewIDAddress(1003) + msgB := types.Message{Version: 0, To: toID, From: fromB, Nonce: 0, Value: types.NewInt(0), Method: builtintypes.MethodsEVM.InvokeContract, GasLimit: 100000, GasFeeCap: types.NewInt(1), GasPremium: types.NewInt(1)} + sigB := typescrypto.Signature{Type: typescrypto.SigTypeDelegated, Data: append(append(make([]byte, 31), 2), append(append(make([]byte, 31), 2), 0)...)} + smB := &types.SignedMessage{Message: msgB, Signature: sigB} + + // Tipset/mocks + ts := makeTipset(t) + // Provide minimal returns + mkRet := func() []byte { + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 3)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + _, _ = buf.Write(cbg.CborNull) + var eth20 [20]byte + for i := range eth20 { + eth20[i] = 0xAB + } + require.NoError(t, cbg.WriteByteArray(&buf, eth20[:])) + return buf.Bytes() + } + rc1 := types.MessageReceipt{ExitCode: 0, GasUsed: 1000, Return: mkRet()} + rc2 := types.MessageReceipt{ExitCode: 0, GasUsed: 1000} + cs := &mockChainStoreMulti{ts: ts, smsgs: []*types.SignedMessage{smA, smB}, rcpts: []types.MessageReceipt{rc1, rc2}} + sm := &mockStateManager{} + tr := &mockTipsetResolver{ts: ts} + + // Set a synthetic Delegated(address) event for logs fetching. + var topic0 ethtypes.EthHash + h := sha3.NewLegacyKeccak256() + _, _ = h.Write([]byte("Delegated(address)")) + copy(topic0[:], h.Sum(nil)) + var evDel ethtypes.EthAddress + for i := range evDel { + evDel[i] = 0xDE + } + var data32 [32]byte + copy(data32[12:], evDel[:]) + logsForTest = []ethtypes.EthLog{{Topics: []ethtypes.EthHash{topic0}, Data: ethtypes.EthBytes(data32[:])}} + ev := &mockEvents{} + + api, err := NewEthTransactionAPI(cs, sm, nil, nil, nil, ev, tr, 0) + require.NoError(t, err) + receipts, err := api.EthGetBlockReceipts(ctx, ethtypes.NewEthBlockNumberOrHashFromPredefined("latest")) + require.NoError(t, err) + require.Len(t, receipts, 2) + + // 7702 has auth list and delegatedTo; non-7702 gets delegatedTo from synthetic event + require.Len(t, receipts[0].AuthorizationList, 1) + require.Len(t, receipts[0].DelegatedTo, 1) + require.Len(t, receipts[1].AuthorizationList, 0) + require.Len(t, receipts[1].DelegatedTo, 1) +} diff --git a/node/impl/eth/transaction_7702_status_test.go b/node/impl/eth/transaction_7702_status_test.go new file mode 100644 index 00000000000..1ff9eebf032 --- /dev/null +++ b/node/impl/eth/transaction_7702_status_test.go @@ -0,0 +1,52 @@ +package eth + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/filecoin-project/go-state-types/big" + + "github.com/filecoin-project/lotus/chain/types" + ethtypes "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +// Ensures that for typed-0x04 ApplyAndCall, we decode embedded status from return payload +func TestNewEthTxReceipt_7702_StatusFromApplyAndCallReturn(t *testing.T) { + ctx := context.Background() + // Build a minimal 0x04 EthTx view + // Non-creation tx (To != nil) + var to ethtypes.EthAddress + for i := range to { + to[i] = 0x11 + } + tx := ethtypes.EthTx{Type: 0x04, Gas: 21000, To: &to} + // Minimal fee fields so newEthTxReceipt can compute gas outputs + one := big.NewInt(1) + tx.MaxFeePerGas = ðtypes.EthBigInt{Int: one.Int} + tx.MaxPriorityFeePerGas = ðtypes.EthBigInt{Int: one.Int} + + // Encode ApplyAndCallReturn { status=0, output_data=[0xAA] } + var buf bytes.Buffer + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 0)) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{0xAA})) + + msgReceipt := types.MessageReceipt{ExitCode: 0, GasUsed: 21000, Return: buf.Bytes()} + rcpt, err := newEthTxReceipt(ctx, tx, big.Zero(), msgReceipt, nil) + require.NoError(t, err) + require.Equal(t, uint64(0), uint64(rcpt.Status)) + + // Now test status=1 + buf.Reset() + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajArray, 2)) + require.NoError(t, cbg.CborWriteHeader(&buf, cbg.MajUnsignedInt, 1)) + require.NoError(t, cbg.WriteByteArray(&buf, []byte{})) + msgReceipt.Return = buf.Bytes() + rcpt, err = newEthTxReceipt(ctx, tx, big.Zero(), msgReceipt, nil) + require.NoError(t, err) + require.Equal(t, uint64(1), uint64(rcpt.Status)) +} diff --git a/node/impl/eth/utils.go b/node/impl/eth/utils.go index dbe154d3eeb..d4d54efa1ae 100644 --- a/node/impl/eth/utils.go +++ b/node/impl/eth/utils.go @@ -142,6 +142,7 @@ func executeTipset(ctx context.Context, ts *types.TipSet, cs ChainStore, sm Stat return stRoot, msgs, rcpts, nil } +// Note: ParseEthRevert moved to chain/types/ethtypes. Use ethtypes.ParseEthRevert. // lookupEthAddress makes its best effort at finding the Ethereum address for a // Filecoin address. It does the following: // @@ -474,11 +475,25 @@ func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, baseFee big.Int, ms Logs: []ethtypes.EthLog{}, // empty log array is compulsory when no logs, or libraries like ethers.js break LogsBloom: ethtypes.NewEmptyEthBloom(), } + if len(tx.AuthorizationList) > 0 { + txReceipt.AuthorizationList = tx.AuthorizationList + } + // Default: derive status from Filecoin exit code. + txReceipt.Status = 0 if msgReceipt.ExitCode.IsSuccess() { txReceipt.Status = 1 - } else { - txReceipt.Status = 0 + } + + // EIP-7702: for typed-0x04 routed to EthAccount.ApplyAndCall, the actor always exits OK and embeds status in return. + if tx.Type == 0x04 && len(msgReceipt.Return) > 0 { + if st, ok := decodeApplyAndCallReturnStatus(msgReceipt.Return); ok { + if st != 0 { + txReceipt.Status = 1 + } else { + txReceipt.Status = 0 + } + } // else: malformed return; keep default ExitCode-derived status } txReceipt.GasUsed = ethtypes.EthUint64(msgReceipt.GasUsed) @@ -535,6 +550,31 @@ func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, baseFee big.Int, ms return txReceipt, nil } +// decodeApplyAndCallReturnStatus parses a CBOR-encoded [status(uint), output_data(bytes)] +// and returns the status if decoding succeeds. +func decodeApplyAndCallReturnStatus(b []byte) (uint64, bool) { + r := bytes.NewReader(b) + maj, extra, err := cbg.CborReadHeader(r) + if err != nil || maj != cbg.MajArray || extra != 2 { + return 0, false + } + maj, status, err := cbg.CborReadHeader(r) + if err != nil || maj != cbg.MajUnsignedInt { + return 0, false + } + maj, extra, err = cbg.CborReadHeader(r) + if err != nil || maj != cbg.MajByteString { + return 0, false + } + if extra > 0 { + buf := make([]byte, extra) + if _, err := r.Read(buf); err != nil { + return 0, false + } + } + return status, true +} + func encodeFilecoinParamsAsABI(method abi.MethodNum, codec uint64, params []byte) []byte { buf := []byte{0x86, 0x8e, 0x10, 0xc4} // Native method selector. return append(buf, encodeAsABIHelper(uint64(method), codec, params)...) diff --git a/node/impl/eth/utils_7702_test.go b/node/impl/eth/utils_7702_test.go new file mode 100644 index 00000000000..8af494be01d --- /dev/null +++ b/node/impl/eth/utils_7702_test.go @@ -0,0 +1,56 @@ +package eth + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/big" + + "github.com/filecoin-project/lotus/chain/types" + ethtypes "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +// Verifies that newEthTxReceipt carries over authorizationList from the tx view +// for type-0x04 (EIP-7702) transactions. +func TestNewEthTxReceipt_CarriesAuthorizationList(t *testing.T) { + // Minimal EthTx with one authorization tuple + var from, to ethtypes.EthAddress + for i := 0; i < len(from); i++ { + from[i] = 0x11 + } + for i := 0; i < len(to); i++ { + to[i] = 0x22 + } + + tx := ethtypes.EthTx{ + ChainID: ethtypes.EthUint64(1), + From: from, + To: &to, + Type: ethtypes.EthUint64(ethtypes.EIP7702TxType), + Gas: ethtypes.EthUint64(21000), + } + // Populate fee fields to satisfy GasFeeCap()/GasPremium() + maxFee := ethtypes.EthBigInt(big.NewInt(1)) + tip := ethtypes.EthBigInt(big.NewInt(1)) + tx.MaxFeePerGas = &maxFee + tx.MaxPriorityFeePerGas = &tip + + // Attach a single authorization tuple + var authAddr ethtypes.EthAddress + for i := 0; i < len(authAddr); i++ { + authAddr[i] = 0xAB + } + tx.AuthorizationList = []ethtypes.EthAuthorization{ + {ChainID: 1, Address: authAddr, Nonce: 0, YParity: 0, R: ethtypes.EthBigInt(big.NewInt(1)), S: ethtypes.EthBigInt(big.NewInt(1))}, + } + + // Dummy receipt: success, no events root so logs path not exercised + msgReceipt := types.MessageReceipt{ExitCode: 0, GasUsed: 21000} + + rcpt, err := newEthTxReceipt(context.Background(), tx, big.NewInt(0), msgReceipt, nil) + require.NoError(t, err) + require.Len(t, rcpt.AuthorizationList, 1) + require.Equal(t, tx.AuthorizationList[0].Address, rcpt.AuthorizationList[0].Address) +} diff --git a/node/impl/eth/utils_revert_test.go b/node/impl/eth/utils_revert_test.go new file mode 100644 index 00000000000..119bef364d5 --- /dev/null +++ b/node/impl/eth/utils_revert_test.go @@ -0,0 +1,62 @@ +package eth + +import ( + "encoding/hex" + "testing" + + ethtypes "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +func abiEncodeErrorString(msg string) []byte { + // selector 0x08c379a0 + offset (32) + length + bytes padded to 32 + selector, _ := hex.DecodeString("08c379a0") + // offset = 32 (0x20) + off := make([]byte, 32) + off[31] = 32 + // length + l := make([]byte, 32) + l[31] = byte(len(msg)) + // data padded + data := make([]byte, ((len(msg)+31)/32)*32) + copy(data, []byte(msg)) + // final = selector || off || len || data + out := append([]byte{}, selector...) + out = append(out, off...) + out = append(out, l...) + out = append(out, data...) + return out +} + +func abiEncodePanic(code uint64) []byte { + // selector 0x4e487b71 + uint256 code (big-endian) + selector, _ := hex.DecodeString("4e487b71") + buf := make([]byte, 32) + // place code in the last byte; sufficient for known small codes + buf[31] = byte(code) + return append(selector, buf...) +} + +func TestParseEthRevert_ErrorString(t *testing.T) { + b := abiEncodeErrorString("oops") + got := ethtypes.ParseEthRevert(b) + if got != "Error(oops)" { + t.Fatalf("unexpected parse: %s", got) + } +} + +func TestParseEthRevert_PanicCodeKnown(t *testing.T) { + b := abiEncodePanic(0x01) // Assert() + got := ethtypes.ParseEthRevert(b) + if got != "Assert()" { + t.Fatalf("unexpected panic parse: %s", got) + } +} + +func TestParseEthRevert_ShortBuffer(t *testing.T) { + b := []byte{0x01, 0x02, 0x03} + got := ethtypes.ParseEthRevert(b) + // Falls back to hex string of bytes + if got != ethtypes.EthBytes(b).String() { + t.Fatalf("unexpected short parse: %s", got) + } +} diff --git a/prompts/00-eip7702-read-validate-plan.md b/prompts/00-eip7702-read-validate-plan.md new file mode 100644 index 00000000000..072860d713c --- /dev/null +++ b/prompts/00-eip7702-read-validate-plan.md @@ -0,0 +1,35 @@ +Goal: read and validate the current EIP‑7702 migration plan and follow‑ups for this repo set (Lotus, builtin‑actors, ref‑fvm), without making speculative changes. + +Scope +- CWD: Lotus repo root (this directory). +- Paired repos (same parent directory): + - `../builtin-actors` + - `../ref-fvm` +- Ground truth docs: + - `AGENTS.md` (this repo). + - `documentation/eip7702_ethaccount_ref-fvm_migration.md`. + - `../eip-7702.md` (spec notes). + +Tasks (idempotent) +1. Read and reconcile plan: + - Open and skim: + - `AGENTS.md`, focusing on the EIP‑7702 sections and “What Remains / Follow‑ups from 2025‑11‑13 review (OPEN)”. + - `documentation/eip7702_ethaccount_ref-fvm_migration.md`. + - `../eip-7702.md`. + - Verify that the open items listed in `AGENTS.md` accurately reflect the true remaining work in code: + - EthAccount → VM outer‑call bridge + Lotus E2E. + - Positive R/S padding tests for EthAccount. + - Delegated(address) event coverage in ref‑fvm. + - Naming/comment cleanup for EvmApplyAndCall / Delegator. + - If you discover that any of these items are already fully implemented and tested, update the corresponding bullet in `AGENTS.md` from “OPEN” to “DONE”, with a short note and file references. + - Do NOT implement any of the missing features/tests in this prompt; only reconcile docs vs. reality and adjust `AGENTS.md` if it is stale. + +2. Idempotency expectations: + - On a re‑run, if `AGENTS.md` already correctly describes which items are DONE vs. OPEN, make no changes. + +3. Final reporting (for this prompt only): + - In your final message: + - State whether each of the four follow‑ups is currently “already satisfied”, “clearly missing”, or “partially present” based on code/tests as of this run. + - State whether you updated `AGENTS.md`, and if so, which bullets were changed. + - Explicitly say whether this “plan validation” prompt finished its job properly (i.e., docs and code are now in sync for the follow‑up list). + diff --git a/prompts/01-eip7702-ethaccount-outer-call-bridge-and-e2e.md b/prompts/01-eip7702-ethaccount-outer-call-bridge-and-e2e.md new file mode 100644 index 00000000000..151c27a6390 --- /dev/null +++ b/prompts/01-eip7702-ethaccount-outer-call-bridge-and-e2e.md @@ -0,0 +1,51 @@ +Goal: implement (or verify) the EthAccount → VM outer‑call bridge for EIP‑7702 and enable the Lotus E2E flow when the bundle supports it, matching the migration plan. + +Scope +- CWD: Lotus repo root. +- Paired repos: + - `../builtin-actors` (EthAccount + EVM actors). + - `../ref-fvm` (VM intercept and syscalls). + +Tasks (idempotent) +1. Detect current behavior: + - In `../builtin-actors/actors/ethaccount/src/lib.rs`, inspect `EthAccountActor::apply_and_call`: + - If it already calls a dedicated VM/EVM helper (e.g., a syscall or runtime hook) for the “outer call” instead of a plain `rt.send`, and that helper uses the delegated CALL semantics (authority context, depth=1, storage overlay, event), then the bridge may already be in place. + - In `../ref-fvm/fvm/src/call_manager/default.rs`, confirm that: + - `try_intercept_evm_call_to_eoa` intercepts CALL/STATICCALL→EthAccount with `delegate_to` set. + - It uses `InvokeAsEoaWithRoot` and EthAccount state (`delegate_to`, `evm_storage_root`) to execute delegate code under authority context, enforces depth=1, and emits `Delegated(address)`. + +2. Implement or wire the outer‑call bridge (ONLY if missing): + - In ref‑fvm: + - Introduce a dedicated VM entrypoint or helper (e.g., `evm_apply_and_call`‑style syscall or internal helper) that: + - Accepts `(authority_eth: [u8;20], to_eth: [u8;20], value: TokenAmount, input: Bytes)`. + - Reuses the existing delegation machinery (EthAccount state + `InvokeAsEoaWithRoot`) to execute delegate code under authority context when appropriate. + - Returns `(status: u8, returndata: Bytes)` where `status=1` for `ExitCode::OK` and `status=0` for revert/value‑transfer failures, preserving revert payloads. + - In builtin‑actors: + - Update `EthAccountActor::apply_and_call` so that, after persisting delegation mapping + nonce increments: + - It calls the new VM helper instead of a raw `rt.send` for the outer call. + - It keeps the actor’s exit code `OK`, returning `ApplyAndCallReturn { status, output_data }` derived from the helper’s result (embedded status contract). + - Maintain existing semantics: + - Mapping and nonce updates MUST persist regardless of outer call outcome. + - Storage overlay (`evm_storage_root`) persists only on successful delegated execution. + +3. Lotus E2E (if wasm bundle supports it): + - In `./itests/eth_7702_e2e_test.go`, locate `TestEth7702_DelegatedExecute`: + - If the bundle includes the necessary EthAccount/EVM changes, unskip the test and ensure it: + - Applies delegations via a 0x04 transaction (0x04 → EthAccount.ApplyAndCall). + - CALLs the delegated EOA and asserts: + - Delegated code executes. + - Storage under the authority is updated and persists. + - Receipt’s `authorizationList`, `delegatedTo`, and `status` reflect the embedded status. + - If the bundle is still missing, leave the test skipped but ensure the skip message clearly indicates that only bundling is missing, not code. + +4. Idempotency expectations: + - If, on inspection, the EthAccount outer‑call bridge and E2E test are already implemented and passing, make no code changes; only adjust comments/docs if they are stale. + - Ensure any new helper/syscall is additive and backward‑compatible within this branch. + +5. Final reporting: + - In your final answer for this prompt: + - State whether the EthAccount outer‑call bridge was already present or was implemented in this run (with key file references). + - State whether `TestEth7702_DelegatedExecute` is enabled and passing, or still skipped due to missing bundle wiring. + - Confirm which tests you ran (at least the targeted EthAccount/EVM/ref‑fvm tests and `go test` for the focused 7702 suites), and whether they all passed. + - Explicitly say whether this outer‑call bridge + E2E prompt finished its job properly or not. + diff --git a/prompts/02-eip7702-ethaccount-rs-padding-tests.md b/prompts/02-eip7702-ethaccount-rs-padding-tests.md new file mode 100644 index 00000000000..d15280b9093 --- /dev/null +++ b/prompts/02-eip7702-ethaccount-rs-padding-tests.md @@ -0,0 +1,35 @@ +Goal: ensure EthAccount explicitly accepts minimally‑encoded big‑endian `r/s` values with lengths 1..32 bytes (left‑padded internally), and rejects >32‑byte or zero values, aligning with Lotus’ RLP encoding. + +Scope +- Repo: `../builtin-actors`. +- Files of interest: + - `actors/ethaccount/src/lib.rs` (validation + recovery). + - `actors/ethaccount/tests/*` (EthAccount test suites). + +Tasks (idempotent) +1. Inspect current validation: + - Confirm that `EthAccountActor::validate_tuple`: + - Rejects `len(r) > 32` or `len(s) > 32`. + - Rejects zero `r/s`. + - Enforces `y_parity ∈ {0,1}`. + - Enforces low‑S on a 32‑byte left‑padded `s`. + - Confirm that `recover_authority` left‑pads `r/s` to 32 bytes before constructing the 65‑byte signature. + +2. Add positive tests ONLY if missing: + - Search the EthAccount tests for positive coverage of short‑length `r/s` (e.g., lengths 1, 31). + - If such tests do not exist, add a small test file (or extend an existing one, e.g., `apply_and_call_invalids.rs`) that: + - Constructs `ApplyAndCallParams` with single `DelegationParam` entries where: + - `r` and `s` lengths vary over `{1, 31, 32}` (all non‑zero, low‑S). + - `y_parity` is 0 or 1, `chain_id` is 0 or the local ChainID. + - Calls `EthAccountActor::ApplyAndCall` via the mock runtime and asserts the call succeeds (no error) and that the actor accepts these tuples. + - Keep tests fast and deterministic. + +3. Idempotency expectations: + - If the repository already has clear, explicit positive tests for sub‑32‑byte `r/s` values, do not add duplicate tests; instead, confirm they pass. + +4. Final reporting: + - In your final message for this prompt: + - State whether new tests were added and where, or confirm that existing tests already cover positive `r/s` padding acceptance. + - Confirm that `cargo test -p fil_actor_ethaccount` passes. + - Explicitly say whether this R/S padding prompt finished its job properly or not. + diff --git a/prompts/03-eip7702-ref-fvm-delegated-event-coverage.md b/prompts/03-eip7702-ref-fvm-delegated-event-coverage.md new file mode 100644 index 00000000000..cdad382f34e --- /dev/null +++ b/prompts/03-eip7702-ref-fvm-delegated-event-coverage.md @@ -0,0 +1,38 @@ +Goal: add or verify explicit ref‑fvm coverage for the `Delegated(address)` event emitted by the delegated CALL intercept, asserting topic and ABI encoding of the authority address. + +Scope +- Repo: `../ref-fvm`. +- Files of interest: + - `fvm/src/call_manager/default.rs` (intercept, `keccak32`, event emission). + - `fvm/tests/*` (especially delegated mapping/value, EXTCODE* tests). + +Tasks (idempotent) +1. Inspect current event emission: + - In `fvm/src/call_manager/default.rs`, verify that: + - `try_intercept_evm_call_to_eoa` emits an event with: + - Topic0 = `keccak32(b"Delegated(address)")`. + - A 32‑byte word whose last 20 bytes contain the authority’s 20‑byte address. + +2. Add an integration test ONLY if missing: + - Search `fvm/tests` for any test that explicitly checks: + - The topic hash for `Delegated(address)`, and + - That the event data’s last 20 bytes equal the authority’s address. + - If such a test does not exist, add a new test file, e.g., `delegated_event_emission.rs`, that: + - Uses `fvm/tests/common.rs::new_harness` to build a machine from the builtin‑actors bundle. + - Sets up: + - An EthAccount authority A with `delegate_to` set to a delegate EVM contract B (as in `set_ethaccount_with_delegate`). + - A caller EVM contract C that CALLs A, triggering the delegated CALL intercept. + - Executes C, retrieves the receipt (including `EventsRoot`), decodes events, and asserts: + - There is at least one event whose topic0 equals `keccak32("Delegated(address)")`. + - The corresponding data is 32 bytes, and its last 20 bytes equal A’s Ethereum address. + - Ensure the test tolerates minimal builds (`--no-default-features`) in a similar way to other delegated tests (early exit if features are unavailable). + +3. Idempotency expectations: + - If such a test already exists and passes, do not add another; simply confirm it still passes. + +4. Final reporting: + - In your final message for this prompt: + - State whether a new event‑coverage test was added (and where), or whether existing tests already cover the topic + ABI encoding. + - Confirm that `cargo test -p fvm --tests -- --nocapture` passes. + - Explicitly say whether this delegated‑event coverage prompt finished its job properly or not. + diff --git a/prompts/04-eip7702-naming-and-comment-cleanup.md b/prompts/04-eip7702-naming-and-comment-cleanup.md new file mode 100644 index 00000000000..484fb211fda --- /dev/null +++ b/prompts/04-eip7702-naming-and-comment-cleanup.md @@ -0,0 +1,32 @@ +Goal: clean up confusing or outdated references to `EvmApplyAndCall` / Delegator in code and tests now that the live routing is EthAccount.ApplyAndCall + VM intercept, while preserving historical documentation. + +Scope +- Primary repo: `./lotus`. +- Paired repos for comment alignment (minimal edits): + - `../builtin-actors` + - `../ref-fvm` + +Tasks (idempotent) +1. Lotus comment/test audit: + - Search for the following terms in `./lotus`: + - `EvmApplyAndCallActorAddr` + - `EVM.ApplyAndCall` + - `Delegator` + - For each hit: + - If it describes the current routing/behavior (e.g., gas scaffolds, receipt adjusters, tests) but still uses EVM/Delegator terminology, update the wording to reflect EthAccount.ApplyAndCall and the EthAccount + VM intercept design. + - If it is clearly historical or deprecated (e.g., changelogs documenting the earlier Delegator design), leave it intact or add a short note clarifying that Delegator/EVM.ApplyAndCall are removed on this branch. + - Do not change any functional behavior in this prompt—only comments, test descriptions, and naming that is misleading. + +2. Optional alignment in builtin‑actors and ref‑fvm: + - Search `../builtin-actors` and `../ref-fvm` for comments that still describe Delegator/EVM.ApplyAndCall as active components. + - Where such comments describe current state, adjust them to match the EthAccount + VM intercept architecture. + +3. Idempotency expectations: + - On subsequent runs, if comments and test names already reflect the EthAccount + VM intercept design accurately, make no changes. + +4. Final reporting: + - In your final message for this prompt: + - Summarize which files were updated for naming/comment cleanup (if any). + - Confirm that no functional logic was changed in this prompt. + - Explicitly say whether this naming/comment cleanup prompt finished its job properly or not. + diff --git a/prompts/05-eip7702-agents-and-tests-finalization.md b/prompts/05-eip7702-agents-and-tests-finalization.md new file mode 100644 index 00000000000..8ff89ffc3e9 --- /dev/null +++ b/prompts/05-eip7702-agents-and-tests-finalization.md @@ -0,0 +1,49 @@ +Goal: reconcile `AGENTS.md` with the actual code/tests for EIP‑7702 follow‑ups and run the focused test matrix to confirm everything is in a good state. + +Scope +- Primary repo: `./lotus` (AGENTS + Lotus tests). +- Paired repos: + - `../builtin-actors` + - `../ref-fvm` + +Tasks (idempotent) +1. Reconcile AGENTS follow‑ups: + - Re‑read `AGENTS.md`, especially: + - The EIP‑7702 sections. + - The “What Remains” and “Follow‑ups from 2025‑11‑13 review (OPEN)” bullets. + - For each follow‑up item: + - EthAccount → VM outer‑call bridge + E2E. + - Positive R/S padding tests. + - Delegated(address) event coverage in ref‑fvm. + - Naming/comment cleanup. + - Determine, based on the current code and tests, whether it is: + - DONE (fully implemented and tested). + - PARTIAL (some pieces implemented/tests in progress). + - OPEN (no meaningful implementation yet). + - Update `AGENTS.md` bullets accordingly: + - Mark items DONE only if code and tests truly match the intended behavior. + - Leave items as OPEN or clarify as PARTIAL where appropriate. + +2. Run targeted test matrix: + - Lotus: + - `go test ./chain/types/ethtypes -run 7702 -count=1` + - `go test ./node/impl/eth -run 7702 -count=1` + - If the E2E test `TestEth7702_DelegatedExecute` is enabled: `go test ./itests -run Eth7702 -tags eip7702_enabled -count=1` + - builtin‑actors: + - `cargo test -p fil_actor_evm` + - `cargo test -p fil_actor_ethaccount` + - ref‑fvm: + - `cargo test -p fvm --tests -- --nocapture` + - Optionally, `scripts/run_eip7702_tests.sh` if Docker and the bundle are available and not prohibitively slow. + - If any of these commands are not runnable in the current environment (e.g., missing Docker, platform issues), clearly note which ones were skipped and why. + +3. Idempotency expectations: + - On re‑runs, if `AGENTS.md` already accurately reflects which tasks are DONE vs. OPEN/PARTIAL, limit changes to status updates where something has newly landed since the last run. + +4. Final reporting: + - In your final message for this prompt: + - For each of the four main follow‑up tasks, state whether it is DONE, PARTIAL, or OPEN, and reference the key files/tests backing that assessment. + - List which tests you ran and whether they passed; for any skipped tests, provide a short reason. + - Summarize any `AGENTS.md` changes you made. + - Explicitly say whether this “AGENTS and tests finalization” prompt finished its job properly or not. + diff --git a/prompts/06-eip7702-cross-repo-pr-review.md b/prompts/06-eip7702-cross-repo-pr-review.md new file mode 100644 index 00000000000..3cc2d7f09c5 --- /dev/null +++ b/prompts/06-eip7702-cross-repo-pr-review.md @@ -0,0 +1,163 @@ +Goal: perform a thorough, cross‑repo review of the EIP‑7702 EthAccount + ref‑fvm migration as it exists on this branch, treating the current state of the three repos as the PR under review. + +Important: this prompt is **review‑only**. Do not modify code or tests unless explicitly requested by a higher‑level instruction; instead, focus on analysis, findings, and recommendations. + +Scope +- Primary repo (CWD): `lotus` (this branch is the candidate PR branch). +- Paired repos (same parent directory): + - `../builtin-actors` + - `../ref-fvm` +- Ground‑truth design docs: + - `AGENTS.md` (this repo). + - `documentation/eip7702_ethaccount_ref-fvm_migration.md`. + - `../eip-7702.md` (EIP‑7702 spec notes for this workstream). + +High‑level objectives +1. Verify that the EthAccount + VM intercept migration is complete and coherent across the three repos, relative to the design docs. +2. Identify any correctness, safety, or spec‑alignment issues (including subtle edge cases). +3. Assess test coverage and note meaningful gaps. +4. Summarize findings clearly so a human reviewer can quickly decide whether to merge, and what follow‑ups to schedule. + +Tasks (idempotent; analysis‑only) + +1) Context and plan reconciliation +- Read the following carefully: + - `AGENTS.md` (focus on the EIP‑7702 sections, “What Remains”, and the “Follow‑ups from 2025‑11‑13 review (status)” block). + - `documentation/eip7702_ethaccount_ref-fvm_migration.md`. + - `../eip-7702.md`. +- Confirm that the items marked DONE / PARTIAL / OPEN in `AGENTS.md` match what you see in the code/tests across the three repos. + - If you find inconsistencies, **do not update code**; just record them as review findings. + +2) Lotus review (Go) +- Focus files: + - `chain/types/ethtypes/*7702*` (RLP 0x04 parsing, CBOR params, `AuthorizationKeccak`, transaction→message conversion). + - `chain/types/ethtypes/eth_transactions.go` (special handling for ApplyAndCall). + - `node/impl/eth/receipt_7702_scaffold.go` and `node/impl/eth/transaction_7702_receipts_test.go` (receipts, `delegatedTo`, synthetic `Delegated(address)` event handling). + - `node/impl/eth/gas.go` and `node/impl/eth/gas_7702_scaffold*.go` (7702 gas overhead; behavioral only). +- Review items: + - Encoding/decoding: + - 0x04 RLP parser’s limits, canonical integer checks, yParity handling, and tuple arity. + - CBOR `[ [tuple...], [to(20), value, input] ]` encoding/decoding and error paths. + - Routing: + - Type‑0x04 routing to `EthAccountApplyAndCallActorAddr` and `MethodHash("ApplyAndCall")`. + - Receipts: + - Status derivation from embedded ApplyAndCall return vs. exit code. + - `AuthorizationList` and `delegatedTo` reconstruction from tuples and `Delegated(address)` event (32‑byte ABI word). + - Gas: + - Behavioral overhead only when targeting `ApplyAndCall` and when tuples are present. + - No tests pin absolute gas numbers. +- Note any confusing error messages, brittle handling, or missing negative tests. + +3) builtin‑actors review (Rust) +- Focus files: + - `actors/ethaccount/src/state.rs` and `actors/ethaccount/src/lib.rs`. + - `actors/evm/src/lib.rs`, `actors/evm/src/interpreter/instructions/ext.rs`, `actors/evm/src/interpreter/instructions/lifecycle.rs`. + - Tests under `actors/ethaccount/tests` and `actors/evm/tests` that reference 7702. +- Review items: + - EthAccount state/validation: + - `delegate_to`, `auth_nonce`, `evm_storage_root` semantics and initialization. + - Tuple validation (chainId domain, yParity, non‑zero R/S, ≤32‑byte R/S, low‑S). + - Pre‑existence policy for authorities that resolve to EVM contracts. + - Receiver‑only behavior and nonce handling (including absent‑nonce = 0). + - ApplyAndCall behavior: + - Order of operations (state persistence before outer call). + - Outer call routing: + - Invoke EVM contracts via `InvokeEVM` when target is an EVM actor. + - Fallback to plain `METHOD_SEND` for non‑EVM targets. + - Embedded `(status, output_data)` semantics and error mapping. + - EVM actor + interpreter: + - Removal/stubbing of legacy `InvokeAsEoa` / `EVM.ApplyAndCall`. + - `InvokeAsEoaWithRoot` trampoline behavior (authority context, storage root mounting/persistence). + - EXTCODE* pointer semantics via `get_eth_delegate_to` helper and 23‑byte `0xEF 0x01 0x00 || delegate(20)` image. + - SELFDESTRUCT no‑op in authority context. + - Test coverage: + - Invalid/edge tests (tuples, nonces, duplicates, tuple cap). + - Positive `r/s` padding tests. + - Outer‑call routing tests (InvokeEVM vs METHOD_SEND). +- Assess whether EthAccount’s behavior matches the design doc, and note any surprising behavior or missing invariants. + +4) ref‑fvm review (Rust) +- Focus files: + - `fvm/src/call_manager/default.rs` (especially `try_intercept_evm_call_to_eoa`, keccak helpers, state update). + - `fvm/src/syscalls/actor.rs` and `sdk/src/actor.rs` (get_eth_delegate_to syscall + helper). + - `fvm/tests/*` covering: + - `delegated_call_mapping.rs` + - `delegated_value_transfer_short_circuit.rs` + - `depth_limit.rs` + - `evm_extcode_projection.rs` + - `overlay_persist_success.rs` + - `selfdestruct_noop_authority.rs` + - `delegated_event_emission.rs` + - `eth_delegate_to.rs` + - `ethaccount_state_roundtrip.rs` +- Review items: + - Intercept gating: + - Only for `InvokeEVM` → EthAccount with `delegate_to` set. + - Correct use of EthAccount state and resolution via EAM. + - Value transfer semantics: + - Transfer to authority before delegated execution; short‑circuit behavior and returned exit code/data on failure. + - Delegated execution: + - Correct construction and call of `InvokeAsEoaWithRoot`. + - Mapping of success/revert to exit codes and return data. + - Overlay persistence only on success. + - Event emission: + - `Delegated(address)` event topic and ABI encoding of the authority address. + - EXTCODE* behavior: + - 23‑byte pointer image and keccak hash consistency. + - Windowing/zero‑fill semantics. + - Test behavior for minimal builds (`--no-default-features`) and how tests tolerate feature flags. + +5) Test and CI view +- Run (or at least conceptually verify) the focused test sets: + - Lotus: + - `go test ./chain/types/ethtypes -run 7702 -count=1` + - `go test ./node/impl/eth -run 7702 -count=1` + - Note status of `itests/TestEth7702_DelegatedExecute` (skip vs enabled). + - builtin‑actors: + - `cargo test -p fil_actor_ethaccount` + - `cargo test -p fil_actor_evm` + - ref‑fvm: + - `cargo test -p fvm --tests -- --nocapture` + - Optionally `scripts/run_eip7702_tests.sh` if the environment supports Docker and testing isn’t prohibitively slow. +- If you cannot actually run any of these in this environment, use the code and existing CI notes to infer their status, and call that out explicitly in your report. + +6) Risk / edge‑case analysis +- Identify and discuss potential issues such as: + - Cross‑repo constant mismatches (domain magic, bytecode magic/version). + - Incomplete or surprising behavior around: + - Non‑EVM outer calls. + - Gas estimation and refunds. + - Minimal builds where delegated paths may be disabled. + - Error propagation (especially around value transfer and revert mapping). + - Any consensus‑sensitive behavior that feels brittle or under‑tested. +- For each issue, categorize: + - “Blocking” (should be fixed before merge). + - “Follow‑up” (acceptable as a documented TODO). + - “Non‑issue” (just worth noting). + +7) Idempotency expectations +- This prompt should be safe to run multiple times: + - You should not modify code, tests, or docs as part of this prompt. + - If you do need to suggest changes, present them as recommendations with concrete file/line references, not as applied patches. +- On re‑runs, you may refine or extend the review conclusions but should not redo expensive work unnecessarily. + +8) Final reporting (very important) +- Your final message for this prompt should be a structured review, not a patch summary. It must include: + - **Overall assessment**: is the cross‑repo EIP‑7702 implementation ready to merge as‑is, or are there blocking issues? + - **Per‑repo findings** (Lotus, builtin‑actors, ref‑fvm): + - Strengths/what looks solid. + - Issues or deviations from the design docs (with file references). + - **Cross‑repo alignment**: + - Constants/encodings. + - Event semantics. + - Routing and intercept behavior. + - **Test/coverage summary**: + - Which key tests you confirmed as present and (if possible) passing. + - Any notable coverage gaps. + - **Recommendation**: + - Explicitly state whether you would: + - Approve the PR as‑is, + - Approve with noted follow‑ups, or + - Block pending specific changes. + - A clear sentence at the end indicating whether this “cross‑repo PR review” prompt has finished its job properly (e.g., “Cross‑repo EIP‑7702 review complete; no blocking issues found,” or “Cross‑repo review complete; blocking issues identified in X, see above.”). + diff --git a/prompts/README.md b/prompts/README.md new file mode 100644 index 00000000000..e8c94ee0978 --- /dev/null +++ b/prompts/README.md @@ -0,0 +1,28 @@ +This directory contains EIP‑7702 follow‑up prompts intended to be run via Codex CLI in sequence. Each prompt is: +- Focused on a specific aspect of the EthAccount + ref‑fvm migration (outer‑call bridge, tests, event coverage, naming/docs). +- Idempotent: it should be safe to run multiple times; if work is already complete, it should not re‑apply changes. +- Responsible for clear final reporting (per‑prompt). + +Prompts (in order) +- `00-eip7702-read-validate-plan.md` + Read and reconcile the current migration plan and `AGENTS.md` follow‑ups with actual code/tests; do not implement changes here, only fix stale documentation. + +- `01-eip7702-ethaccount-outer-call-bridge-and-e2e.md` + Implement or verify the EthAccount → VM outer‑call bridge and wire (or clarify) the Lotus 7702 E2E test. + +- `02-eip7702-ethaccount-rs-padding-tests.md` + Add/verify positive tests for minimally‑encoded `r/s` padding acceptance in EthAccount. + +- `03-eip7702-ref-fvm-delegated-event-coverage.md` + Add/verify ref‑fvm tests that assert `Delegated(address)` event topic and ABI‑encoded authority address. + +- `04-eip7702-naming-and-comment-cleanup.md` + Clean up comments/tests that still use `EvmApplyAndCall` / Delegator terminology for current behavior. + +- `05-eip7702-agents-and-tests-finalization.md` + Reconcile `AGENTS.md` status with reality and run the focused test matrix to confirm everything is green. + +Standalone combined prompt +- `eip7702_followups.md` + The original combined prompt capturing all follow‑ups in one file. It is not used by `run_prompts.sh` but can be executed directly if you prefer a single, monolithic run. + diff --git a/prompts/eip7702_followups.md b/prompts/eip7702_followups.md new file mode 100644 index 00000000000..eeec099a5a7 --- /dev/null +++ b/prompts/eip7702_followups.md @@ -0,0 +1,154 @@ +Goal: resolve the open EIP‑7702 follow‑ups recorded in `AGENTS.md` (EthAccount → VM outer‑call bridge, positive R/S padding tests, Delegated(address) event coverage, and naming cleanup) across `./lotus`, `../builtin-actors`, and `../ref-fvm`, in a way that is safe, idempotent, and clearly reported. + +Context and references +- Work in the following repos/branches: + - `lotus` (CWD): branch `eip7702`. + - `../builtin-actors`: branch `eip7702`. + - `../ref-fvm`: branch `eip7702`. +- Ground truth docs: + - `AGENTS.md` in `./lotus`. + - `documentation/eip7702_ethaccount_ref-fvm_migration.md` in `./lotus`. + - `../eip-7702.md` (spec notes). + +High‑level tasks (all MUST be idempotent) +1. EthAccount → VM outer‑call bridge + Lotus E2E enablement. +2. Positive R/S padding tests for EthAccount. +3. Delegated(address) event coverage in ref‑fvm. +4. Naming and comment cleanup around `EvmApplyAndCall` / Delegator. +5. Update `AGENTS.md` to reflect the new status once work is done. + +Idempotency requirements +- Before changing code for any task, first detect whether the work is already complete: + - If code, tests, and docs already match the intended end state for that task, do NOT change them; simply record in your final message that this task was already satisfied. + - Only apply changes when a clear gap remains. +- All added tests should be stable when re‑run and should not duplicate existing coverage; exercise unique behaviors or assert previously untested invariants. +- When updating `AGENTS.md`, only flip task status from OPEN to DONE (or adjust wording) if you actually completed the work in this run; never pre‑declare future work as DONE. + +Task 1 — EthAccount → VM outer‑call bridge + Lotus E2E (0x04) + +Intent +- Align the implementation with the migration plan: type‑0x04 transactions must atomically: + 1. Apply delegation mappings + nonce bumps in EthAccount (persisting even if the outer call reverts), and + 2. Execute the outer call under the VM’s delegated CALL semantics, using the authority’s `evm_storage_root`, depth‑limit, pointer semantics, and `Delegated(address)` event emission. + +What to implement +1. Check current behavior: + - In `../builtin-actors/actors/ethaccount/src/lib.rs`, inspect `EthAccountActor::apply_and_call`. + - If it already invokes a dedicated VM/EVM helper (e.g., a syscall or runtime hook) instead of a bare `rt.send` for the outer call, and that helper is wired to delegated CALL semantics as per `documentation/eip7702_ethaccount_ref-fvm_migration.md`, then this task may already be complete; verify via tests in step 3 and skip changes if everything passes. + - In `../ref-fvm/fvm/src/call_manager/default.rs`, confirm that delegated CALL interception (`try_intercept_evm_call_to_eoa`) is used for CALL/STATICCALL→EthAccount and that storage/event semantics match the migration doc. +2. If EthAccount still uses a plain `rt.send` for the outer call, introduce a dedicated bridge: + - In ref‑fvm: + - Add a VM entrypoint or helper (e.g., an `evm_apply_and_call`‑style syscall or an internal function) that: + - Accepts `authority: EthAddress`, `to: EthAddress`, `value: TokenAmount`, and `input: Bytes`. + - Forwards gas similar to the current delegated CALL path (no 63/64 clamp at the outer boundary). + - Leverages the existing EthAccount state (`delegate_to`, `evm_storage_root`) and EVM actor `InvokeAsEoaWithRoot` trampoline to execute the delegate code under authority context when appropriate. + - Returns `(status: u8, returndata: Bytes)` such that: + - `status=1` for `ExitCode::OK`, `status=0` for revert or value‑transfer failures. + - `returndata` carries the callee’s return or revert payload. + - Preserves the existing intercept guarantees: depth limit, SELFDESTRUCT no‑op in authority context, overlay persistence on success only, and `Delegated(address)` event emission. + - In builtin‑actors: + - Update `EthAccountActor::apply_and_call` to: + - Continue to validate and persist delegation mapping + nonce increments exactly as today (in a transaction, before the outer call). + - Replace the direct `rt.send` with a call to the new VM/EVM helper so that the outer call’s execution and delegated semantics match the intercept path. + - Keep the actor exit code `OK` and return an `ApplyAndCallReturn` where `status` (0/1) and `output_data` are derived from the helper’s result, maintaining the “embedded status” contract relied on by Lotus receipts. +3. Lotus E2E: + - In `./lotus/itests/eth_7702_e2e_test.go`, locate `TestEth7702_DelegatedExecute`: + - If it is still `t.Skip(...)` and the new outer‑call bridge is in place and bundled into the wasm, unskip the test and update the logic as needed to: + - Apply delegations via a type‑0x04 transaction (0x04 → EthAccount.ApplyAndCall). + - CALL → EOA and assert execution of the delegate, persistent storage under the authority, and correct `authorizationList` / `delegatedTo` / `status` in the receipt. + - If the environment still lacks a wasm bundle with the new entrypoint, keep the skip but update the skip message and `AGENTS.md` to reflect what is missing (bundle wiring vs. code). + +Idempotency for Task 1 +- If the outer call already uses a dedicated VM/EVM helper with delegated semantics AND `TestEth7702_DelegatedExecute` is implemented and passing, do not re‑implement the bridge; just confirm via tests and mark the corresponding follow‑up as DONE in `AGENTS.md`. +- If only part of the work is done (e.g., helper exists but E2E is still skipped), complete the missing parts only. + +Task 2 — Positive R/S padding tests for EthAccount + +Intent +- Confirm that EthAccount accepts minimally‑encoded big‑endian `r/s` values with lengths from 1 to 32 bytes, left‑padding to 32 internally, while rejecting >32‑byte values and zero values (the latter already covered). + +What to do +1. In `../builtin-actors/actors/ethaccount/src/lib.rs`, keep the current validation logic: + - `len(r), len(s) ≤ 32`, rejection of >32 bytes, non‑zero check, `y_parity ∈ {0,1}`, and low‑S check on padded `s`. +2. In `../builtin-actors/actors/ethaccount/tests`: + - If there is NOT already a test that explicitly asserts acceptance of shorter `r/s` lengths (e.g., 1‑byte, 31‑byte) for valid tuples, add one, e.g. in a new file `apply_and_call_rs_padding.rs` or by extending `apply_and_call_invalids.rs`: + - Construct `DelegationParam` values with: + - `r` lengths `{1, 31, 32}`, `s` lengths `{1, 31, 32}` (non‑zero, low‑S). + - `y_parity` = 0 or 1, chain_id 0 or local. + - Call `EthAccountActor::ApplyAndCall` via the mock runtime and assert success (proper exit, status=1) for these cases. + - Ensure tests remain fast and deterministic. + +Idempotency for Task 2 +- Before adding new tests, search for existing EthAccount tests that already cover positive short‑length `r/s` cases; if they exist and clearly assert acceptance, do not add duplicates. + +Task 3 — Delegated(address) event coverage in ref‑fvm + +Intent +- Verify that the VM intercept emits a `Delegated(address)` event with: + - Topic0 = `keccak256("Delegated(address)")`. + - Data = a 32‑byte ABI word whose last 20 bytes equal the authority (EOA) address. + +What to do +1. In `../ref-fvm/fvm/tests`, add a new integration test (e.g., `delegated_event_emission.rs`) if one does not already exist that explicitly inspects the event: + - Use `fvm/tests/common.rs::new_harness` to instantiate a machine with the bundled EthAccount and EVM actors. + - Set up: + - An EthAccount authority A with `delegate_to` set to a delegate EVM contract B, as done in `set_ethaccount_with_delegate`. + - A caller EVM contract C that CALLs A so the VM intercept path is exercised. + - Execute C and obtain: + - The receipt (including `EventsRoot`). + - The decoded events, using the same machinery as other event tests. + - Assert: + - There is at least one event whose topic0 equals `keccak32(b"Delegated(address)")` (you can reuse `keccak32` from `call_manager/default.rs` or compute Keccak directly). + - The associated data is 32 bytes long and its last 20 bytes equal A’s 20‑byte Ethereum address (authority). +2. Ensure the test tolerates minimal builds (`--no-default-features`) the same way existing delegation tests do (early‑exit or conditional assertions if needed). + +Idempotency for Task 3 +- If such a test already exists and asserts both topic and ABI encoding of the authority address, do not add another; just confirm it passes. + +Task 4 — Naming / comment cleanup (EvmApplyAndCall / Delegator) + +Intent +- Remove residual confusion from comments/tests referring to `EvmApplyAndCall` or “Delegator” where the live path is now EthAccount + VM intercept, while preserving historical documentation. + +What to do +1. In `./lotus`: + - Search for `EvmApplyAndCallActorAddr` and “Delegator” across code and tests: + - For references in code/tests that describe *current behavior* but still name `EvmApplyAndCall` or “Delegator”, update the wording to refer to EthAccount.ApplyAndCall and the EthAccount + VM intercept design. + - For historical changelogs or explicit “deprecated/removed” docs, keep the text but, if helpful, add a short clarifying note that the Delegator/EVM.ApplyAndCall paths are deprecated on this branch and have been replaced by EthAccount.ApplyAndCall. +2. Mirror any critical terminology updates in `../builtin-actors` and `../ref-fvm` where comments still mention Delegator as if it were live, but avoid over‑editing historical FIP documents. + +Idempotency for Task 4 +- Only change comments/tests where the terminology mismatch is actually confusing the current design; do not repeatedly rewrite already‑updated comments. + +Task 5 — Update AGENTS.md and final verification + +After implementing the above tasks (or confirming they are already satisfied): +1. Update `./lotus/AGENTS.md`: + - In the “What Remains” / follow‑ups section, mark any completed items from the 2025‑11‑13 follow‑up list as DONE, with a short note (e.g., “DONE — wired EthAccount.ApplyAndCall to VM helper and unskipped TestEth7702_DelegatedExecute”). + - If new tests were added, briefly note their locations under the appropriate test‑plan sections (Lotus, builtin‑actors, ref‑fvm). +2. Run the relevant tests: + - Lotus: + - `go test ./chain/types/ethtypes -run 7702 -count=1` + - `go test ./node/impl/eth -run 7702 -count=1` + - If the E2E is enabled: `go test ./itests -run Eth7702 -tags eip7702_enabled -count=1` + - builtin‑actors: + - `cargo test -p fil_actor_evm` + - `cargo test -p fil_actor_ethaccount` + - ref‑fvm: + - `cargo test -p fvm --tests -- --nocapture` + - Optionally `scripts/run_eip7702_tests.sh` if Docker and the bundle path are available. + +Final reporting (VERY IMPORTANT) +- In your final message to the user, you MUST: + - List each of the four main tasks (outer‑call bridge + E2E, R/S padding tests, Delegated(address) event coverage, naming cleanup). + - For each task, state clearly whether: + - It was already complete when you started. + - You completed it in this run (and point to the key files/tests). + - Or it remains incomplete, with a short explanation why. + - Confirm whether all of the above tests were run and passed; if you had to skip any (e.g., missing wasm bundle or Docker), call that out explicitly. +- This explicit summary is required so that this prompt can be re‑run safely in the future and reviewers can quickly see whether it “finished its job properly” or if further work remains. + +If, after following these steps, all tasks are either verified complete or implemented and all covered tests pass, state in your final answer: +> “EIP‑7702 follow‑ups are complete for this run; AGENTS.md is updated and all targeted tests are passing.” +Otherwise, clearly enumerate which items remain open and why. + diff --git a/prompts/run_prompts.sh b/prompts/run_prompts.sh new file mode 100755 index 00000000000..9ffa64ada58 --- /dev/null +++ b/prompts/run_prompts.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runs each prompt file via Codex CLI multiple times (idempotent execution). +# Usage: +# RUNS=2 ./prompts/run_prompts.sh +# Environment: +# RUNS - number of passes for each prompt (default: 2) + +RUNS="${RUNS:-2}" + +if ! command -v codex >/dev/null 2>&1; then + echo "Error: codex CLI not found in PATH. Install or export PATH accordingly." >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Collect prompt files (exclude README and non-numbered files), sorted lexicographically. +shopt -s nullglob +PROMPTS=( "${SCRIPT_DIR}"/[0-9][0-9]-*.md ) + +if [[ ${#PROMPTS[@]} -eq 0 ]]; then + echo "No prompt files found in ${SCRIPT_DIR}. Expected files like 00-*.md" >&2 + exit 1 +fi + +echo "Found ${#PROMPTS[@]} prompt(s). Running each ${RUNS} time(s)." >&2 +echo "Repo root: ${REPO_ROOT}" >&2 + +# Environment preamble for Codex CLI: set cwd, approvals, and sandbox mode. +ENV_PREAMBLE() { + cat < + ${REPO_ROOT} + never + danger-full-access + enabled + bash + + +EOF +} + +for prompt_file in "${PROMPTS[@]}"; do + echo "===> Prompt: ${prompt_file}" >&2 + for ((i=1; i<=RUNS; i++)); do + echo "--- Run ${i}/${RUNS}: ${prompt_file}" >&2 + PAYLOAD="$(ENV_PREAMBLE; cat "${prompt_file}")" + ( + cd "${REPO_ROOT}" + codex exec --dangerously-bypass-approvals-and-sandbox "${PAYLOAD}" + ) || { + echo "WARN: codex exec failed for ${prompt_file} (run ${i})." >&2 + exit 1 + } + done +done + +echo "All prompts executed." >&2 + diff --git a/prompts2/00-eip7702-ethaccount-nonce-tests.md b/prompts2/00-eip7702-ethaccount-nonce-tests.md new file mode 100644 index 00000000000..3ea16a6358e --- /dev/null +++ b/prompts2/00-eip7702-ethaccount-nonce-tests.md @@ -0,0 +1,52 @@ +Goal: finalize and enable EthAccount nonce handling tests for EIP‑7702 so that nonce initialization and increment semantics are fully covered and no longer rely on ignored tests. + +Scope +- CWD: `./lotus` (this repo). +- Paired repos: + - `../builtin-actors` (EthAccount actor implementation and tests). + - `../ref-fvm` (VM intercept and EthAccount state round‑trip). + +Tasks (idempotent) +1) Inspect current nonce behavior and tests +- In `../builtin-actors/actors/ethaccount/src/lib.rs`, review `EthAccountActor::apply_and_call`: + - Confirm that `auth_nonce` starts at 0, is compared for equality against each tuple’s `nonce`, and is incremented once per accepted tuple/message. + - Verify that nonce persistence happens inside the `rt.transaction::` block, before the outer call is executed. +- In `../builtin-actors/actors/ethaccount/tests/apply_and_call_nonces.rs`: + - Check the `#[ignore]`-annotated test `nonce_init_and_increment` and understand why it was skipped (e.g., flakiness, harness limitations, or behavior changes). + - Confirm whether the test still matches the current actor semantics (absent‑nonce = 0, first nonce=0 succeeds, second nonce=0 fails). + +2) Either re‑enable or replace the nonce test +- If `nonce_init_and_increment` is still correct and stable under the current semantics: + - Remove the `#[ignore]` annotation and adjust expectations only as needed to match the actual behavior (e.g., error messages or exact send expectations) while preserving: + - success for the first `nonce=0` tuple on a fresh EthAccount, and + - failure for a second `nonce=0` tuple. +- If the existing test is stale or structurally mismatched to the current ApplyAndCall implementation: + - Replace it with one or more focused tests that: + - Explicitly assert: + - Initial nonce=0 for a fresh EthAccount (no prior delegations). + - Nonce equality enforcement (old nonce accepted; repeated nonce rejected). + - Use the existing MockRuntime helpers and expectations (e.g., `expect_send_any_params`) in an idempotent way. + - Keep tests self‑contained and deterministic so they can run reliably in CI. + +3) Cross‑check nonce semantics in ref‑fvm +- In `../ref-fvm/fvm/tests/ethaccount_state_roundtrip.rs` and `../ref-fvm/fvm/tests/common.rs`: + - Confirm that `auth_nonce` is encoded/decoded in the same position and type as the EthAccount state used by the kernel (`delegate_to`, `auth_nonce`, `evm_storage_root`). + - If necessary, add a small test to verify that EthAccount state written by builtin‑actors (via the bundle) round‑trips through the VM without losing nonce information. + +4) Run focused tests +- In `../builtin-actors`: + - `cargo test -p fil_actor_ethaccount --tests -- --nocapture` +- In `../ref-fvm` (sanity only; do not change behavior): + - `cargo test -p fvm --tests -- --nocapture` + +5) Final reporting +- In your final answer for this prompt: + - State whether the EthAccount nonce behavior was already correct and fully covered, or if you had to enable/adjust/add tests. + - Point to the specific test file(s) and test name(s) that now cover: + - initial nonce behavior, and + - nonce increment/rejection semantics. + - Confirm which of the above test commands you ran and whether they passed. +- Explicitly close with a sentence such as: + - “EthAccount nonce tests are now enabled and passing; nonce semantics are fully covered,” + - or, if something must remain skipped, explain why and what remains open. + diff --git a/prompts2/01-eip7702-lotus-delegated-e2e.md b/prompts2/01-eip7702-lotus-delegated-e2e.md new file mode 100644 index 00000000000..d987c61e9c0 --- /dev/null +++ b/prompts2/01-eip7702-lotus-delegated-e2e.md @@ -0,0 +1,71 @@ +Goal: enable and stabilize a full Lotus E2E test for delegated execution under EIP‑7702 (type‑0x04 → EthAccount.ApplyAndCall → delegated CALL→EOA) once the wasm bundle supports the EthAccount + ref‑fvm changes. + +Scope +- CWD: `./lotus` (this repo). +- Paired repos (read‑only in this prompt): + - `../builtin-actors` (EthAccount and EVM actors). + - `../ref-fvm` (VM intercept, `get_eth_delegate_to`, and bundle). + +Tasks (idempotent) +1) Confirm prerequisites +- Read `AGENTS.md` and `documentation/eip7702_ethaccount_ref-fvm_migration.md` to refresh the intended full lifecycle: + - Type‑0x04 decode and CBOR params. + - EthAccount state + ApplyAndCall semantics. + - ref‑fvm delegated CALL intercept and EXTCODE* pointer semantics. +- Check that the current branch has a wasm bundle with EthAccount.ApplyAndCall and the ref‑fvm intercept wired: + - Look at `../ref-fvm/fvm/tests` (delegated* and extcode* tests) and `../builtin-actors/actors/ethaccount/tests`: + - If these are green and the bundle for this branch is known to include the same code, you can proceed to E2E wiring. + - Otherwise, treat this prompt as design scaffolding only and do not attempt to force E2E in this run. + +2) Inspect and update the E2E test scaffold +- Open `itests/eth_7702_e2e_test.go` and inspect: + - `TestEth7702_SendRoutesToEthAccount` + - `TestEth7702_ReceiptFields` + - `TestEth7702_DelegatedExecute` +- For `TestEth7702_DelegatedExecute`: + - If it is still `t.Skip(...)`: + - Confirm that the skip reason matches the current state (e.g., missing wasm bundle or tuple‑signing helpers) and is not outdated. + - If the bundle now supports full ApplyAndCall + delegated CALL, plan to: + - Remove the skip. + - Implement the test flow described in the design doc: send a type‑0x04 tx to apply delegation, then CALL the EOA and assert delegated execution and storage persistence. + - If the test is already enabled: + - Verify that it: + - Uses type‑0x04 → EthAccount.ApplyAndCall to set `delegate_to`/`auth_nonce`. + - Exercises a CALL→EOA that triggers the VM intercept (delegated execution). + - Asserts: + - storage changes under the authority (via a simple delegate contract), + - presence of `authorizationList` and `delegatedTo` in the receipt, + - receipt `Status` reflecting the embedded ApplyAndCall status. + +3) Implement or refine the E2E flow (ONLY if prerequisites are met) +- Ensure the test: + - Uses the same constants/magic as the unit tests and ref‑fvm: + - `SetCodeAuthorizationMagic = 0x05`. + - Delegation indicator bytecode `0xEF 0x01 0x00 || delegate(20)`. + - Relies on the existing 0x04 parsing and CBOR helpers (`eth_7702_transactions.go`, `eth_7702_params.go`) instead of hand‑rolling encodings. + - Avoids pinning numeric gas values; assert only: + - inclusion of tuple overhead when appropriate, and + - functional success (storage updates, events, and receipt status). +- Keep the test idempotent: + - Use fresh accounts and contracts per run. + - Avoid relying on global mutable state beyond the test harness’s standard lifecycle. + +4) Run the E2E and focused suites +- In `./lotus`: + - `go test ./chain/types/ethtypes -run 7702 -count=1` + - `go test ./node/impl/eth -run 7702 -count=1` + - `go test ./itests -run Eth7702_DelegatedExecute -tags eip7702_enabled -count=1` +- If `TestEth7702_DelegatedExecute` must remain skipped (e.g., bundle still missing), you may run only the first two commands to confirm nothing regressed, and explicitly document why E2E is still disabled. + +5) Final reporting +- In your final answer for this prompt: + - State whether `TestEth7702_DelegatedExecute` is: + - enabled and passing, + - enabled but failing (with a short diagnosis), or + - still skipped due to clearly documented prerequisites. + - Summarize the key behaviors that the E2E test now covers (storage, events, receipts, gas behavior). + - List which test commands you ran and their outcomes. +- End with a clear statement such as: + - “Lotus E2E delegated execution test is now enabled and passing,” or + - “E2E remains skipped due to missing bundle; unit/integration tests remain green.” + diff --git a/prompts2/02-eip7702-ref-fvm-depth-limit-guard.md b/prompts2/02-eip7702-ref-fvm-depth-limit-guard.md new file mode 100644 index 00000000000..ca65fb8ad9d --- /dev/null +++ b/prompts2/02-eip7702-ref-fvm-depth-limit-guard.md @@ -0,0 +1,61 @@ +Goal: make depth‑limit enforcement for delegated CALLs in ref‑fvm explicit and verifiable, ensuring that delegation chains cannot recurse beyond depth=1 even under adversarial EthAccount configurations. + +Scope +- CWD: `./lotus` (this repo; used for docs and context). +- Primary implementation repo: `../ref-fvm`. +- Paired repo for semantics: `../builtin-actors` (EVM System and authority‑context flag). + +Tasks (idempotent) +1) Re‑establish current behavior +- In `../ref-fvm/fvm/src/call_manager/default.rs`, revisit `DefaultCallManager::call_actor_resolved` and `try_intercept_evm_call_to_eoa`: + - Confirm that interception currently occurs when: + - The target code CID belongs to EthAccount. + - The entrypoint is `InvokeEVM` (FRC‑42 method hash for `"InvokeEVM"`). + - The EthAccount state has `delegate_to = Some([u8;20])`. + - Note that depth limiting is currently achieved implicitly: intercept is only aware of target code, not authority context. +- In `../builtin-actors/actors/evm/src/interpreter/system.rs`: + - Confirm that `System.in_authority_context` exists and is set only by `InvokeAsEoaWithRoot`. + - Verify that `selfdestruct` and any other authority‑sensitive opcodes check `system.in_authority_context`. + +2) Decide on an explicit depth‑limit signal +- Design a simple, self‑contained mechanism to prevent re‑interception when already executing under authority context. Examples (pick one that fits the existing architecture): + - A boolean “delegation active” flag attached to the call stack in `DefaultCallManager`, propagated into `try_intercept_evm_call_to_eoa`. + - A convention that authority‑context calls set a special flag in the `InvocationResult` or via an additional parameter. +- The key property: when an EVM actor, executing under authority context as the delegate, issues CALL/STATICCALL to another EthAccount, the VM must not attempt to follow delegation again. + +3) Implement the explicit guard +- In `../ref-fvm/fvm/src/call_manager/default.rs`: + - Introduce the chosen depth‑limit marker (e.g., a `delegation_active: bool` on the call stack or within `CallManager`’s state). + - Set the marker when a delegated CALL is initiated via `try_intercept_evm_call_to_eoa`. + - Ensure the marker is cleared/restored appropriately when unwinding the stack. + - Modify `try_intercept_evm_call_to_eoa` to early‑return `Ok(None)` when the marker indicates that a delegated CALL is already in progress (authority context). +- Keep the behavior otherwise identical: + - Still require EthAccount code and `delegate_to` set. + - Still require entrypoint `"InvokeEVM"`. + - Keep event emission, value‑transfer semantics, overlay persistence, and revert mapping unchanged. + +4) Strengthen tests for nested delegation attempts +- In `../ref-fvm/fvm/tests/depth_limit.rs`: + - Review the existing test `delegated_call_depth_limit_enforced`: + - It currently configures A→B and B→C and asserts that the observed behavior stops at B. + - Extend or complement this test to: + - Explicitly configure two EthAccount actors with delegates in a way that would cause re‑interception if depth limiting were not enforced. + - Assert that, even with such a configuration, delegated execution stops after one level (i.e., delegate B’s code runs, but C’s delegate code never executes via the same intercept). +- If needed, add a small helper in `fvm/tests/common.rs` to construct multiple EthAccount actors with different `delegate_to` settings for easier matrix tests. + +5) Run ref‑fvm tests +- In `../ref-fvm`: + - `cargo test -p fvm --tests -- --nocapture` +- Ensure that: + - All existing 7702 tests (`delegated_*`, `evm_extcode_projection`, `overlay_persist_success`, `selfdestruct_noop_authority`) remain green. + - Any new depth‑limit tests pass consistently. + +6) Final reporting +- In your final answer for this prompt: + - Describe the chosen explicit depth‑limit mechanism and where it is implemented. + - Summarize the new/updated tests that demonstrate the behavior when a second-level delegated EthAccount is present. + - Confirm that `cargo test -p fvm --tests` passed and that no behavior regressed. +- End with a sentence like: + - “Delegation depth limit is now explicitly enforced in ref‑fvm and covered by tests,” + - or, if you determined that an explicit guard is not yet feasible, clearly explain why and what would be needed. + diff --git a/prompts2/03-eip7702-naming-and-docs-refresh.md b/prompts2/03-eip7702-naming-and-docs-refresh.md new file mode 100644 index 00000000000..08558d41ff0 --- /dev/null +++ b/prompts2/03-eip7702-naming-and-docs-refresh.md @@ -0,0 +1,67 @@ +Goal: clean up remaining naming and documentation drift around EIP‑7702 (e.g., `EvmApplyAndCallActorAddr` references and outdated comments) so that the code and docs accurately reflect the EthAccount + VM intercept routing. + +Scope +- CWD: `./lotus` (primary target for naming/docs). +- Paired repos (read‑only unless a clear gap is found): + - `../builtin-actors` (actor comments). + - `../ref-fvm` (design comments). + +Tasks (idempotent) +1) Locate residual naming and comment drift +- In `./lotus`: + - Search for `EvmApplyAndCallActorAddr`: + - Identify where it is still used to represent the EthAccount actor (e.g., tests and scaffolding). + - Distinguish between: + - Places where the name is purely local to tests and harmless, and + - Places where it can cause confusion in comments or public documentation. + - Search for mentions of “Delegator” or `EVM.ApplyAndCall` in comments and docs: + - In `AGENTS.md`, `documentation/eip7702_ethaccount_ref-fvm_migration.md`, and any design notes. + - Ensure that they either: + - Clearly mark old paths as historical, or + - Are updated to reference `EthAccount.ApplyAndCall` + VM intercept instead. +- In `../builtin-actors` and `../ref-fvm`: + - Skim for comments that still describe the older EVM‑centric design (e.g., `InvokeAsEoa` as the primary path) instead of the current EthAccount + intercept architecture. + +2) Update names in Lotus where it improves clarity +- For identifiers that currently use `EvmApplyAndCallActorAddr` to refer to the EthAccount actor: + - In tests and scaffolding (e.g., `node/impl/eth/transaction_7702_receipts_test.go`, `node/impl/eth/gas_7702_estimate_integration_test.go`, `itests/eth_7702_e2e_test.go`): + - Prefer renaming local variables to something like `EthAccountApplyAndCallActorAddr` or a clearly neutral name (e.g., `applyAndCallActorAddr`) where it makes the intent obvious. + - Ensure that any renaming is local and does not break existing behavior; keep method calls and constants intact. +- Avoid large mechanical renames; focus on the most confusing or externally visible instances. + +3) Refresh comments and docs to match the current design +- In `AGENTS.md`: + - Ensure that the sections describing routing and semantics: + - Emphasize `EthAccount.ApplyAndCall` as the top-level 0x04 target. + - Note that `EVM.ApplyAndCall` and `InvokeAsEoa` are removed/stubbed and that delegation is handled via the VM intercept and `InvokeAsEoaWithRoot`. + - Verify that the “status” sections for: + - EthAccount outer‑call bridge, + - R/S padding tests, + - Delegated(address) event coverage, + - reflect the current state (DONE vs PARTIAL/OPEN). +- In `documentation/eip7702_ethaccount_ref-fvm_migration.md`: + - Check that the descriptions of: + - EthAccount state, + - VM intercept semantics, + - ApplyAndCall outer call, + - pointer code and events, + - match the code as of this branch. + - If you find obvious mismatches (e.g., references to a Delegator actor that no longer exists), update them to: + - Describe EthAccount as the owner of delegation state. + - Mention ref‑fvm intercept as the implementation of delegated CALL/EXTCODE*. + +4) Keep changes minimal and focused +- Do not change function signatures or behavior in this prompt. +- Limit edits to: + - Local variable / constant names where they reduce confusion. + - Comments and docs that are clearly out of date. +- Ensure everything remains idempotent: rerunning this prompt should not produce new diffs once comments/names are aligned. + +5) Final reporting +- In your final answer for this prompt: + - List the main places where names or comments were updated (files and brief descriptions). + - Confirm that you did not change any behavior, only naming/comments/docs. + - Call out any remaining intentional references to legacy names (e.g., compatibility stubs) and why they were left alone. +- Finish with a line like: + - “Naming and documentation for EIP‑7702 are now consistent with the EthAccount + VM intercept design; only legacy stubs remain for historical reasons.” + diff --git a/prompts2/run_prompts.sh b/prompts2/run_prompts.sh new file mode 100644 index 00000000000..4fb32655803 --- /dev/null +++ b/prompts2/run_prompts.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runs each prompt file via Codex CLI multiple times (idempotent execution). +# Usage: +# RUNS=2 ./prompts2/run_prompts.sh +# Environment: +# RUNS - number of passes for each prompt (default: 2) + +RUNS="${RUNS:-2}" + +if ! command -v codex >/dev/null 2>&1; then + echo "Error: codex CLI not found in PATH. Install or export PATH accordingly." >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Collect prompt files (exclude README and non-numbered files), sorted lexicographically. +shopt -s nullglob +PROMPTS=( "${SCRIPT_DIR}"/[0-9][0-9]-*.md ) + +if [[ ${#PROMPTS[@]} -eq 0 ]]; then + echo "No prompt files found in ${SCRIPT_DIR}. Expected files like 00-*.md" >&2 + exit 1 +fi + +echo "Found ${#PROMPTS[@]} prompt(s). Running each ${RUNS} time(s)." >&2 +echo "Repo root: ${REPO_ROOT}" >&2 + +# Environment preamble for Codex CLI: set cwd, approvals, and sandbox mode. +ENV_PREAMBLE() { + cat < + ${REPO_ROOT} + never + danger-full-access + enabled + bash + + +EOF +} + +for prompt_file in "${PROMPTS[@]}"; do + echo "===> Prompt: ${prompt_file}" >&2 + for ((i=1; i<=RUNS; i++)); do + echo "--- Run ${i}/${RUNS}: ${prompt_file}" >&2 + PAYLOAD="$(ENV_PREAMBLE; cat "${prompt_file}")" + ( + cd "${REPO_ROOT}" + codex exec --dangerously-bypass-approvals-and-sandbox "${PAYLOAD}" + ) || { + echo "WARN: codex exec failed for ${prompt_file} (run ${i})." >&2 + exit 1 + } + done +done + +echo "All prompts executed." >&2 +