Skip to content

EVM integration — 32 commits across Phases 0–5 (draft for review)#443

Draft
JSanchezFDZ wants to merge 156 commits into
Raptor3um:developfrom
JSanchezFDZ:feat/evm-integration
Draft

EVM integration — 32 commits across Phases 0–5 (draft for review)#443
JSanchezFDZ wants to merge 156 commits into
Raptor3um:developfrom
JSanchezFDZ:feat/evm-integration

Conversation

@JSanchezFDZ

Copy link
Copy Markdown

Summary

Native EVM integration. 32 commits across 5 phases: account
abstraction layer, full evmone-backed execution, ConnectBlock /
DisconnectTip wiring, reorg journal, EIP-1559 fee accounting,
parallel pre-flight, complete eth_*/net_*/web3_* JSON-RPC
namespace (22 methods + default 8545 listener), four RTM-native
precompiles (ChainLocks, Masternode Registry, Smart Asset ERC-20,
LLMQ Oracle), and C++ EIP-1559 signing primitives byte-for-byte
identical to Python eth-account on canonical test vectors.

Marked DRAFT — this is for architecture review, not merge.
Phases 6 (testnet + paid audits) and 7 (mainnet activation) live
in the plan after the review feedback lands.

Why review now

  • Validate the architecture before going deeper (Phase 6 paid
    audits are expensive).
  • Resolve 10 open questions still owed by the core team
    (Q-A1..Q-A10 in docs/evm/QUESTIONS.md) — these unblock
    Phase 5.4 (wallet HD), Phase 6 (testnet), Phase 7 (mainnet).
  • Get directional feedback on the four precompiles, the
    consensus carve-outs in tx_check/validation/chainparams,
    the receipt persistence, and the D2 header-field deferral.

Documentation entry point

Start with docs/evm/README.md. It indexes:

  • STATUS.md — phase-by-phase map with commit refs and test surface
  • RPC.md — all 25 RPC methods with examples
  • PRECOMPILES.md — Solidity interfaces, selectors, addresses, gas costs
  • FUP.md — follow-up register (mainnet-blocking vs. before-testnet vs. post-launch)
  • QUESTIONS.md — Q-A1..Q-A10 still open with the core team
  • BUILD.md — build / run / test guide + full file inventory
  • PLAN.md + PROPOSAL-FOR-CORE-TEAM.md — historical reference (the original plan and the formal acceptance with D1–D7 decisions)

What ships in this PR

Phase Status Highlights
0 evmone spike evm_executeReadOnly smoke RPC, smoke test, depends/ wiring
1 AAL TX types 11/12/13, opcodes 0xbd/0xbe/0xbf, activation gate via UpdateManager
2 Full EVM execution CEvmStateDB/CEvmStateCache + evmc::Host with nested CALL/CREATE + ApplyEvm{Call,Deploy,Spend}Tx + EIP-1559 ProcessEvm*Tx + ConnectBlock/DisconnectTip wiring + reorg journal + parallel pre-flight
3 JSON-RPC namespace 22 eth_*/net_*/web3_* methods, eth_sendRawTransaction with EIP-1559+legacy decode + secp256k1 sender recovery, receipt persistence with eth↔rtm cross-index, eth_getLogs with filter, default 8545 listener
4 RTM-native precompiles ChainLocks (0x…0a03), Masternode Registry (0x…0a04), Smart Asset ERC-20 (0xA55E70…hash160), LLMQ Oracle (0x…0a02)
5 Signing primitives ✅ partial C++ EIP-1559 signing byte-identical to eth-account; HD wallet integration + Qt UI deferred (need Q-A4)

Test surface: 11 EVM unit-test suites, all green every commit.
Non-EVM tests unaffected. End-to-end live-validated against a
regtest daemon for every RPC method. Per-suite case/assertion
counts in STATUS.md.

Consensus-affecting changes (the diff that matters)

These are intentionally narrow carve-outs for the new EVM tx types;
they are necessary (an EVM tx has no UTXO inputs/outputs by
design) and are gated by tx-type checks so non-EVM transactions
behave identically.

  • src/consensus/tx_check.cppallowEmptyTxInOut extended to
    TRANSACTION_EVM_DEPLOY / _CALL / _SPEND (matches the
    existing pattern for TRANSACTION_QUORUM_COMMITMENT).
  • src/validation.cppbad-txns-type allow-list extended; relay-
    fee + mempool-min-fee skipped for EVM tx types (fee lives on the
    EVM side); ConnectBlock dispatches EVM-typed txs through
    ProcessEvmTransactionsInBlock behind Updates().IsEvmActive();
    ConnectTip generates + persists receipts; DisconnectTip applies
    • erases the reorg journal.
  • src/chainparams.cppUpdate(EUpdate::EVM, …, heightActivated=0) registered for REGTEST. Mainnet/testnet
    activation heights are coordinated hard-fork decisions (see
    Q-A5).

The CBlockHeader is NOT modified in this PR. The D2 header
fields (stateRoot, receiptsRoot, transactionsRoot,
chainLocksCommit) land alongside the mainnet activation hard fork
— tracked as FUP-2.1 and Q-A6.

What's deferred (and why)

  • stateRoot / receiptsRoot / transactionsRoot /
    chainLocksCommit header fields (D2).
    Coordinated hard-fork;
    needs Q-A6 resolution. While deferred, EIP-1559 baseFee
    stays at 0 and coinbase-tip-output verification is unenforced
    (FUP-2.1FUP-2.3). All mainnet-blocking.
  • Wallet-keystore HD integration. Awaits Q-A4 (m/44'/60'
    Ethereum-standard vs m/44'/10226'/0'/1/i RTM-subtree). Signing
    primitives are ready; this is just the "select-key-from-wallet"
    wiring.
  • Qt UI for deploy/call dialogs. Separate session — Qt + MOC.
  • Smart Asset ↔ EVM bidirectional balance mirror (D4-revised
    "T-mirror" gate). Highest-risk integration piece still ahead.
    The ERC-20 precompile's transfer/approve surface stays
    unimplemented until the mirror lands. Read surface
    (name/symbol/decimals/totalSupply) is live.
  • Async requestSignature on the LLMQ Oracle. Needs the gas-
    escrow + timeout pattern (Q-A8). Sync verifySignature is
    live.

Full register in docs/evm/FUP.md.

Open questions for the core team

10 questions in docs/evm/QUESTIONS.md. The four that unblock immediate
next steps:

  • Q-A4 — HD path for EVM keys (m/44'/60' vs RTM coin_type).
    Unblocks Phase 5.4.
  • Q-A5 — Activation height per network (testnet + mainnet).
    Unblocks Phase 6.
  • Q-A6 — D2 header field layout + activation mechanism.
    Unblocks mainnet (FUP-2.1..FUP-2.3).
  • Q-A7 — Mempool gas-price floor for non-UTXO-fee EVM txs.
    DoS-defence for mainnet.

The other six are either no-rush (Q-A1 clarification, Q-A2/3
asset-fee semantics, Q-A9 MN IPv6, Q-A10 bug-bounty multisig)
or design-tier (Q-A8 async LLMQ).

How to validate

  1. Read docs/evm/STATUS.md for the high-level shape.
  2. Check out the branch + build per docs/evm/BUILD.md (Docker
    instructions included; full build is ~10 minutes from clean).
  3. Run all 11 EVM unit-test suites — all should pass.
  4. Boot a regtest daemon (EVM is force-active at height 0); poke
    at the RPC namespace per docs/evm/RPC.md examples.
  5. Optionally compare evm_signTransaction output against Python
    eth-account for the canonical 0x4646…4646 key — bytes
    should match exactly.

Test plan

  • All 11 EVM unit-test suites green
  • Non-EVM unit-test suites unaffected (spot-checked: assets,
    bls, bip32, transaction)
  • End-to-end live regtest validation for every RPC method
  • EIP-1559 signing byte-parity with eth-account on canonical
    test vectors
  • Pending — Phase 6: 10000+ Ethereum tests suite vectors
    (T1)
  • Pending — Phase 6: state-divergence harness across two
    regtest nodes (T2) — requires FUP-2.1 header fields first
  • Pending — Phase 6: reorg fuzzing property-based tests
    (T3)
  • Pending — Phase 6: T-mirror harness once FUP-4.2 lands
  • Pending — Phase 6: paid auditorías (Trail of Bits,
    ChainSecurity, OpenZeppelin)

Branch layout

The PR is 32 commits, organised per-phase with substantive commit
bodies (the implementation notes for each phase live in the commit
messages — not just in the docs). Phases / sub-phases are visible
in git log --oneline:

8926f8785 docs(evm): comprehensive reference + status documentation
fdd0846b6 feat(evm): Phase 5 — server-side EIP-1559 signing primitives + 3 RPC methods
26a6933b9 feat(evm): Phase 4.1 + 4.2 + 4.4 — Masternodes / Smart Assets ERC-20 / LLMQ Oracle precompiles
0454a95a9 feat(evm): Phase 4.0 + 4.3 — precompile framework + ChainLocks precompile
45a3f445e feat(evm): Phase 3.7 — second HTTP listener on the Ethereum default port
2f6cd4982 feat(evm): Phase 3.6d — eth_getLogs (filter by block range / address / topics)
57d3bbec4 feat(evm): Phase 3.6 — receipt persistence + eth_getTransactionReceipt
a6b635f4a feat(evm): Phase 3.5 — eth_sendRawTransaction (RLP + EIP-1559 + sender recovery)
06b244a6e feat(evm): Phase 3.4 — Ethereum/Web3 metadata methods
285df40a5 feat(evm): Phase 3.3 — eth_getBlockByNumber / eth_getBlockByHash + counts
db5ded10d feat(evm): Phase 3.2 — eth_call + eth_estimateGas
24b3f1e67 feat(evm): Phase 3.1 — eth_* JSON-RPC namespace (read-only subset)
26328f77e feat(evm): Phase 2.5 — worker pool (D1, MVP)
c33d4f2d7 feat(evm): Phase 2.6-2 — wire reorg journaling into Connect/Disconnect
9fcac2814 feat(evm): Phase 2.6-1 — reorg journal data structures
90fb43f08 feat(evm): Phase 2.4e-2 — ConnectBlock wiring + pevmstatedb lifecycle
53891ae97 feat(evm): Phase 2.4e-1 — block-level EVM tx iterator
fb8dd3ec4 feat(evm): Phase 2.4 — ProcessEvm*Tx (EIP-1559 fee accounting)
4a3e1d384 feat(evm): Phase 2.3d — snapshot/revert + nested CALL/CREATE
1eb006846 feat(evm): Phase 2.3c — ApplyEvmSpendTx (EVM → UTXO move)
24b2a93aa feat(evm): Phase 2.3b — ApplyEvmDeployTx + real Keccak-256 + CREATE address
ae8e6a292 feat(evm): Phase 2.3a — ApplyEvmCallTx (single-call execution path)
dad0d3069 feat(evm): Phase 2.2 — evmc::Host implementation over CEvmStateCache
a48d22313 feat(evm): Phase 2.1 — state storage primitives (account, db, cache)
b853e7e8f feat(evm): Phase 1.5 — validation surface completion + ops helper
a4f9b17b7 feat(evm): Phase 1.4 — activation gate wired to UpdateManager
0386419b4 docs(evm): core team acceptance + D4 revised + D7 added
f34bcdf41 test(evm): add evmtx serialization tests + SCRIPT_ENABLE_EVM_OPCODES gating
f434a9470 build: fix NATPMP_LIBS detection in configure.ac
80c7e262c feat(evm): Phase 1.1 scaffolding — tx types, opcodes, validation stubs
51d38a437 build: add .gitattributes to enforce LF on build-critical files
c3909e50d feat(evm): Phase 0 spike — validate evmone integration

If you'd prefer this split across multiple PRs (e.g., one per
phase), happy to refactor. The single-PR shape keeps the
architectural narrative coherent, but it's a tradeoff against
review-chunk size.

Validates the technical premise of the RTM-EVM integration plan (see
docs/evm/PLAN.md, Camino C): link the evmone EVM implementation into
raptoreumd via the standard EVMC C++ interface, execute arbitrary
bytecode against an empty world state, and produce correct EVM semantics
on Ethereum Cancun revision (per design decision D6).

Phase 0 has ZERO consensus impact. The smoke entry point and RPC are
behind no activation flag and are NOT reachable from validation code
paths. This commit only adds plumbing.

Deliverables in this commit:

  Source code (new):
  - src/evm/smoke.{h,cpp}            — EvmSmokeExecute(bytecode, calldata)
                                       via evmc::VM + evmc::MockedHost,
                                       fixed to EVMC_CANCUN.
  - src/test/evm_smoke_tests.cpp     — 3 Boost cases: empty contract,
                                       ADD+RETURN, KECCAK256("") matches
                                       canonical constant.
  - src/rpc/ethereum.cpp             — evm_executeReadOnly RPC for
                                       manual end-to-end testing.

  Build system:
  - depends/packages/evmone.mk       — pinned to v0.12.0 (manual install
                                       documented; depends/ integration
                                       blocked by Hunter+HTTPS issue,
                                       see Phase 1 cleanup TODOs).
  - depends/packages/packages.mk     — note explaining evmone exclusion.
  - src/Makefile.am                  — evm/smoke.{h,cpp}, ethereum.cpp,
                                       EVMONE_LIBS=-levmone-standalone
                                       (bundles ethash).
  - src/Makefile.test.include        — evm_smoke_tests.cpp + libevmone.
  - src/rpc/register.h               — RegisterEthereumRPCCommands.

  Documentation:
  - docs/evm/BRANCH-GUIDE.md                        — branch orientation.
  - TODOS.md                         — prioritized backlog (P0 blockers,
                                       Phase 0/1/2 deliverables, review
                                       findings, design decisions D1-D6).
  - docs/evm/PLAN.md                 — full 8-phase, 26-month design.
  - docs/evm/PROPOSAL-FOR-CORE-TEAM.md — technical validation request
                                       (640 lines, English).
  - docs/evm/PHASE-0-HANDOFF.md      — validated build procedure with
                                       all 4 workarounds discovered
                                       during execution.
  - docs/evm/README.md               — docs index.

Validation (executed 2026-05-11):
  ✓ Build: raptoreumd 339 MB, test_raptoreum 489 MB.
  ✓ Smoke tests 3/3:
      empty_contract_succeeds
      add_then_return_yields_nine
      keccak256_empty_matches_known_constant
  ✓ RPC end-to-end 3/3:
      empty: status_code=0, gas_used=0, return_data=""
      ADD+RETURN: status_code=0, gas_used=24, return_data=…0009
      KECCAK256(""): return_data=c5d2460186f7233c927e7db2dcc703c0e500b653…85a470
                    (matches canonical Keccak-256 constant)

Issues discovered (documented in PHASE-0-HANDOFF.md, scheduled for Phase 1.0):
  1. CRLF line endings break shell scripts cloned on Windows.
     Fix: add top-level .gitattributes.
  2. evmone v0.12.0 + Hunter package manager fail because depends-built
     cmake lacks HTTPS support. Workaround: manual build with system
     cmake. Fix: patch cmake.mk or vendor evmc/intx/ethash separately.
  3. --disable-wallet / --without-natpmp not honored: source files use
     wallet/natpmp types unconditionally. Fix: add #ifdef guards.
  4. NATPMP_LIBS empty after configure despite libnatpmp present.
     Workaround: NATPMP_LIBS=-lnatpmp env. Fix: investigate configure.ac
     detection logic.

Out of scope for Phase 0 (TODOs for Phase 1+):
  - Ethereum tests/ suite at 100% (T1) — strongly recommended in
    Phase 1.0 as final integration confirmation.
  - Guix manifest update (CQ3).
  - Proper depends/ packaging of evmone (CQ3 follow-up).

P0 blockers still open (must resolve before Phase 1 consensus code):
  - Foundation legal entity (CQ4) — required for audits and bug bounty.
  - Strategic alignment with public README's multi-language VM
    commitment vs. this branch's EVM-specific design.

Design decisions D1-D6 (from /plan-eng-review) are documented and frozen
in docs/evm/PLAN.md and TODOS.md, awaiting Raptoreum core team ratification
via docs/evm/PROPOSAL-FOR-CORE-TEAM.md.
Resolves Phase 0 Issue 1 (see docs/evm/PHASE-0-HANDOFF.md): Windows
clones with core.autocrlf=true convert shell scripts to CRLF, breaking
Linux builds because /usr/bin/env interprets "bash\r" as the binary
name.

This file forces LF on shell scripts, Makefiles, m4 files, configure
scripts, autotools artifacts, Python, and other build-critical text.
C++ source is declared text but allowed native line endings (clang-format
handles both; not build-critical).

Preserves the existing `src/clientversion.cpp export-subst` rule which
is required by git-archive for version-string substitution in releases.

After this lands, fresh clones on Windows will produce LF-only build
scripts automatically. Existing Windows clones need a one-time fix:

  git rm --cached -r .
  git reset --hard

(or the per-file equivalent if cached changes need to be preserved).
Introduces the consensus-level surface for EVM transactions without
implementing execution. The network learns to recognize EVM transactions
structurally, but all of them are REJECTED with "evm-not-activated"
until Phase 2 lands the UPDATE_EVM activation params and the actual
EVM state trie + execution pipeline.

This is the "andamio" referenced in docs/evm/PLAN.md § "Fase 1 — AAL".

What's added:

  Transaction types (src/primitives/transaction.h):
  - TRANSACTION_EVM_DEPLOY = 11   (deploy a new contract)
  - TRANSACTION_EVM_CALL   = 12   (invoke a contract)
  - TRANSACTION_EVM_SPEND  = 13   (move RTM from EVM acct to UTXO)

  Script opcodes (src/script/script.h, script.cpp):
  - OP_EVMCREATE = 0xbd            (accompanies EVM_DEPLOY tx)
  - OP_EVMCALL   = 0xbe            (accompanies EVM_CALL tx)
  - OP_EVMSPEND  = 0xbf            (accompanies EVM_SPEND tx)
  - MAX_OPCODE bumped to OP_EVMSPEND
  - GetOpName() returns proper names for log output

  Payload structures (src/evm/evmtx.h):
  - struct CEvmDeployTx  with EIP-1559 fee fields
  - struct CEvmCallTx    with EIP-1559 fee fields
  - struct CEvmSpendTx   with output script for the UTXO destination
  - SERIALIZE_METHODS for each (round-trippable via vExtraPayload)
  - EVM_TX_PAYLOAD_VERSION constant (v1)
  - MAX_EVM_CONTRACT_CODE_SIZE (24576 = EIP-170 limit)
  - MAX_EVM_CALLDATA_SIZE     (24576 for now; revisit Phase 2)

  Validation entry points (src/evm/evmtx.cpp):
  - CheckEvmDeployTx / CheckEvmCallTx / CheckEvmSpendTx
  - Structural checks: payload deserialization, version, gasLimit > 0,
    EIP-1559 fee invariant (priority <= max), size limits.
  - IsEvmActive() hard-coded to false until UPDATE_EVM lands in Phase 2.
  - Reject codes: evm-not-activated, bad-evm-{tx-version,tx-gaslimit,
    tx-fee,deploy-payload,deploy-code,deploy-codesize,call-payload,
    call-datasize,spend-payload,spend-amount,spend-script}

  Dispatcher wiring (src/evo/specialtx.cpp):
  - CheckSpecialTx() routes the three new types to the Check* functions.
  - ProcessSpecialTx() and UndoSpecialTx() accept the types as no-ops
    (no state changes applied in Phase 1; comment notes Phase 2 will
    do the real work).

  Build (src/Makefile.am):
  - evm/evmtx.h added to BITCOIN_CORE_H
  - evm/evmtx.cpp added to libraptoreum_server_a_SOURCES

What's NOT in this commit:

  - Activation params (UPDATE_EVM in src/update/update.h + chainparams).
    Phase 2.6 deliverable. Until then IsEvmActive() returns false and
    every EVM tx is rejected with "evm-not-activated" — safe default.

  - Opcode interpretation in src/script/interpreter.cpp. The opcodes
    are declared but EvalScript() doesn't have cases for them, so they
    behave as OP_INVALIDOPCODE in scripts. Phase 2 wires real semantics
    behind SCRIPT_VERIFY_EVM (also to be added).

  - Header field changes (D2: stateRoot, receiptsRoot, transactionsRoot,
    chainLocksCommit). These are hard-fork-only and bundled together in
    a separate Phase 2 commit at activation time.

  - EVM execution. Phase 2 introduces CEvmStateCache, EvmStateDB, the
    evmc::Host implementation, ConnectBlock integration, and gas
    accounting.

Validation (executed 2026-05-11 in feat/evm-integration build):
  ✓ Build compiles cleanly with -j32 (~5 min incremental).
  ✓ Phase 0 smoke tests still pass: 3/3 cases, 39 assertions total.
  ✓ Symbols present in binary (verified via nm):
      evm::CheckEvmDeployTx, evm::EvmSmokeExecute
  ✓ Opcode names appear in GetOpName() output (no log-side regressions).
Resolves Phase 0 Issue 4 (see docs/evm/PHASE-0-HANDOFF.md): NATPMP_LIBS
was empty in the generated Makefile even when libnatpmp and natpmp.h were
present on the host, producing "undefined reference to initnatpmp" at
link time.

Three real bugs in the existing block:

1. AC_CHECK_LIB's "action-if-found" set NATPMP_LIBS but never set
   have_natpmp=yes, so the finalization block could never see the
   library as detected.

2. The finalization block had its conditional inverted:
       if test "$have_natpmp" = "yes"; then
         ... NAT-PMP NOT built ...
   When the library was found (have_natpmp=yes), it disabled NAT-PMP.
   When it was not found (have_natpmp=no or empty), it enabled NAT-PMP.
   The block "worked" by accident because have_natpmp was never "yes",
   so the else-branch always ran.

3. A typo on the else-branch test:
       if test "$use_natpmp" != "no"l; then
   The extra "l" made the comparison string literally "no"l, which
   always matched != correctly by accident, masking the typo.

The fix aligns the natpmp block with the structurally-correct upnp
block immediately above. After this patch, configure correctly
populates NATPMP_LIBS = -lnatpmp when libnatpmp is present.
…gating

Phase 1.2: round-trip serialization tests for the three EVM payload
structs (CEvmDeployTx, CEvmCallTx, CEvmSpendTx), including the
vExtraPayload path used by special transactions. 10 cases / 65
assertions, all passing.

Phase 1.3: add SCRIPT_ENABLE_EVM_OPCODES script-verify flag (bit 16) and
wire OP_EVMCREATE / OP_EVMCALL / OP_EVMSPEND into EvalScript():

  - When the flag is NOT set, the opcodes return SCRIPT_ERR_BAD_OPCODE
    (the safe default — matches Phase 1 IsEvmActive() returning false).
  - When the flag IS set, the opcodes are no-ops (stack unchanged).

The opcodes are strict markers — they signal that the surrounding
transaction is an EVM tx of the corresponding type. The actual EVM
dispatch lives at the transaction-type level in src/evo/specialtx.cpp,
not in script-level evaluation. Phase 2 will add context-aware checks
that verify the opcode appears in the right script position relative
to a TRANSACTION_EVM_* nType.

The new gating tests confirm both halves of the behavior:
  evm_opcodes_rejected_without_flag    — SCRIPT_VERIFY_NONE → BAD_OPCODE
  evm_opcodes_accepted_with_flag       — SCRIPT_ENABLE_EVM_OPCODES → ok

Total EVM test surface after this commit:
  evm_smoke_tests:  3 cases / 39 assertions  (Phase 0 — evmone exec)
  evm_evmtx_tests: 12 cases / 65 assertions  (Phase 1 — types/opcodes)
  TOTAL:           15 cases / 104 assertions
The Raptoreum core team accepted the EVM integration proposal on
2026-05-11 with non-negotiable constraints around risk isolation and
cryptographic surface. This commit reflects those constraints across
all design documents.

Constraints from the core team:

  1. Existing Smart Assets (tx types 8/9/10) MUST remain isolated from
     EVM. Owners and services built on the current asset layer must
     never be exposed to Solidity bugs.

  2. A new EVM-native asset class is needed for DeFi composability,
     separate from the existing Smart Assets.

  3. Optional opt-in wrap/unwrap is acceptable for Smart Asset owners
     who explicitly want EVM exposure. Default must be no exposure.

  4. All future "AI thing" / oracle integrations must live within the
     dual-consensus (PoW + LLMQ) box. No exotic signature schemes.

  5. Serialization changes are expected and acknowledged.

D4 is revised: the original bidirectional-mirror design is dropped in
favor of three asset classes:

  Class 1: Smart Assets (existing, tx 8/9/10)         UTXO-only, no EVM
  Class 2: EVM Assets (new, Phase 4, tx 14/15/16)     EVM-native ERC-20
  Class 3: Wrapped Smart Assets (Phase 5+, tx 17/18)  opt-in bridge

The T-mirror fuzz test is removed (no mirror to fuzz). D4 risk
classification drops from HIGH to LOW. Backward compatibility for
existing Smart Assets becomes 100%.

D7 is new: cryptographic surface restriction. No new signature schemes.
secp256k1 for txs, LLMQ BLS threshold for oracles/ChainLocks. zk-SNARKs,
new BLS variants, and alternative curves are out of scope for this
integration; they need a separate hard fork if ever proposed.

Tx types 14, 15, 16, 17, 18 are reserved (not yet implemented) — see
TODOS.md § "Reserved transaction types and opcodes" for the full table.

Three new questions surface from the acceptance and need follow-up
with the core team (see TODOS.md § "Open questions"):

  Q-A1  What is "the AI thing" specifically? Validates that the LLMQ
        oracle precompile (0x...0a02) covers the intended use case.
  Q-A2  Wrap/unwrap (tx 17/18): in v1 scope or strictly v2?
  Q-A3  EVM Asset economics — reuse Smart Assets fee model (~5 RTM
        per creation) or separate?

P0 blocker "Strategic alignment with public README" is resolved by the
acceptance. The remaining P0 is CQ4 (Foundation legal entity) — still
required for Phase 6 audit contracts and bug bounty escrow.

Files updated:
  docs/evm/BRANCH-GUIDE.md                              D1-D7 table (D4 revised, D7 new)
  TODOS.md                               full revision: acceptance section,
                                         tx type reservations, serialization
                                         summary, revised D4/D7, failure modes
  docs/evm/PLAN.md                       D4 revised, D7 added
  docs/evm/PROPOSAL-FOR-CORE-TEAM.md     ACCEPTED stamp, revised D4 section,
                                         new D7 section

No code changes. Phase 1.1 scaffolding (tx types 11/12/13, opcodes
0xbd-0xbf) stands unchanged — those are still correct under the revised
D4. The reserved tx types 14-18 will be wired in their respective phases.
…ved tx types

Replaces the Phase 1.1 hard-coded IsEvmActive()==false stub with a real
call into the UpdateManager. Also reserves tx types 14-18 in the
TRANSACTION_* enum per the revised D4 (core team acceptance 2026-05-11).

Behaviorally this is still a no-op for any chain — EUpdate::EVM is
declared in the enum but NOT yet registered in chainparams.cpp for any
network. Updates().IsActive(EUpdate::EVM, ...) returns false because
GetUpdate() returns nullptr → State() returns Unknown → IsActive
returns false. EVM transactions remain rejected with "evm-not-activated".

The wiring matters because once chainparams.cpp commits to an activation
height per network (Phase 6+), the gate flips automatically without
needing a code change in src/evm/evmtx.cpp. The activation will use the
existing UpdateManager's hard-fork mode (Update(..., heightActivated=X)
bypasses BIP9 voting and activates unconditionally at the chosen
height), satisfying D2's hard-fork-not-soft-fork constraint.

Changes:

  src/primitives/transaction.h
    Reserves 5 new tx type IDs (consensus-stable, do not change):
      TRANSACTION_NEW_EVM_ASSET    = 14
      TRANSACTION_UPDATE_EVM_ASSET = 15
      TRANSACTION_MINT_EVM_ASSET   = 16
      TRANSACTION_WRAP_ASSET       = 17
      TRANSACTION_UNWRAP_ASSET     = 18
    No dispatcher cases added — sending one of these still triggers the
    default-case BAD_TX_TYPE rejection in CheckSpecialTx() until Phase 4
    (EVM Assets) and Phase 5+ (wrap/unwrap) implement them.

  src/update/update.h, src/update/update.cpp
    Adds EUpdate::EVM = 3 (before MAX_VERSION_BITS_DEPLOYMENTS).
    Adds UpdateManager::IsEvmActive(blockIndex) convenience wrapper
    paralleling the existing IsAssetsActive().

  src/evm/evmtx.cpp
    Replaces the Phase 1.1 hard-coded stub:
      bool IsEvmActive(...) { return false; }
    With:
      bool IsEvmActive(const CBlockIndex* pindexPrev) {
        return Updates().IsEvmActive(pindexPrev);
      }
    Includes chainparams.h and update/update.h.

  src/test/evm_evmtx_tests.cpp
    Adds reserved_evm_asset_tx_types: pins the values 14-18 so any
    future renumbering produces a loud test failure rather than a
    silent consensus collision.

Test results (commit cd7c7d24a tests still pass):
  evm_smoke_tests:   3 cases /  39 assertions
  evm_evmtx_tests:  13 cases /  70 assertions  (+1 reserved test, +5 asserts)
  TOTAL:            16 cases / 109 assertions

Out of scope for Phase 1.4 (deferred to later phases):

  - Registering Update(EUpdate::EVM, ..., heightActivated=X) in any
    network in chainparams.cpp. This is a consensus-relevant
    commitment that requires the activation height to be agreed with
    the core team and operators (Phase 6+).
  - Validation logic for the reserved tx types 14-18 (Phase 4 for EVM
    Assets, Phase 5+ for wrap/unwrap).
  - functional test test/functional/feature_evm_activation.py — needs a
    real activation height in regtest to be meaningful.
Closes out Phase 1 cleanup. Four discrete pieces:

  1.5a — Fee handling for EVM tx types (11-18) in tx_verify.cpp

  Adds defense-in-depth activation-gate cases in checkSpecialTxFee()
  for the EVM tx types (11/12/13) and the reserved EVM-asset types
  (14-18, per revised D4). Each case calls Updates().IsEvmActive() and
  returns false pre-activation. Identical effect to CheckSpecialTx()
  today, but ensures any future refactor that bypasses CheckSpecialTx
  cannot accept an EVM tx pre-fork via the fee path.

  Full EIP-1559 fee accounting (gasLimit * maxFeePerGas) is deferred
  to Phase 2 alongside the actual EVM execution. A TODO marker in
  the cases documents what the Phase 2 body will look like.

  1.5b — Functional test test/functional/feature_evm_activation.py

  Pre-activation regression suite using the standard Python test
  framework:
    - evm_executeReadOnly RPC works pre-activation (3 sub-cases:
      empty contract, ADD+RETURN(9), KECCAK256("") = canonical).
    - getblockchaininfo returns cleanly with the new tx-type enums
      compiled in (regression check).

  Full pre-activation tx-rejection coverage (sendrawtransaction with
  hand-crafted EVM tx → "evm-not-activated" reject) is deferred until
  test_framework/messages.py grows Python wrappers for CEvmDeployTx
  / CEvmCallTx / CEvmSpendTx. The C++ unit tests in
  src/test/evm_evmtx_tests.cpp cover this case at the dispatcher
  level for now.

  1.5c — contrib/devtools/build-evmone.sh

  One-command helper that automates the ~6-step manual evmone install
  procedure documented in docs/evm/PHASE-0-HANDOFF.md § "Issue 2".
  Clones evmone v0.12.0 with submodules, builds with system cmake
  (NOT the depends-built cmake, which lacks HTTPS for Hunter), and
  installs libs + evmc headers into the depends prefix.

  This is a temporary solution. The "proper" depends/ packaging
  (vendor evmc + intx + ethash as separate packages, patch evmone to
  disable Hunter) is deferred to Phase 2 / Guix integration, where
  reproducible-builds become a hard requirement for audits.

  1.5d — Issue 3 (ENABLE_WALLET guards) DEFERRED with rationale

  TODOS.md documents why Issue 3 is not fixed in this phase:
  ~150+ guard insertions across 5+ files, high regression risk on
  wallet-enabled builds (which is every real configuration), and the
  issue pre-dates the EVM work — it's not introduced by this branch.
  The workaround (always pass --enable-wallet --with-incompatible-bdb)
  is documented in PHASE-0-HANDOFF.md and the helper script above.

Test results unchanged:
  evm_smoke_tests:   3 cases /  39 assertions
  evm_evmtx_tests:  13 cases /  70 assertions
  TOTAL:            16 cases / 109 assertions  — all passing

Functional test requires the standard Python test-framework env
(dash_hash, zmq, etc.) which is not set up inside this dev container;
the test runs in CI once that env is configured.

Phase 1 is now complete pending CQ4 (Foundation legal). Next phase is
Phase 2 — EVM execution pipeline (state trie, ConnectBlock integration,
worker pool per D1, header field additions per D2/D3). That's months
of work for a senior team, not a single session.
First slice of the Phase 2 execution layer. Introduces the persistent
data structures and the in-memory cache that EVM execution will sit on
top of. ZERO consensus integration in this commit — these are stand-
alone data structures behind no activation gate and not yet wired into
ConnectBlock / DisconnectBlock. They will be consumed by the evmc::Host
implementation in Phase 2.2.

What lands:

  src/evm/account.{h,cpp}     CEvmAccount

    Ethereum-shape account record: (nonce, balance, codeHash,
    storageRoot). 32-byte big-endian wire format for the 256-bit
    fields so on-disk layout matches what an Ethereum geth/erigon
    node would write. Pins the two canonical constants:

      EmptyCodeHash    = keccak256("")
                       = c5d2460186f7233c927e7db2dcc703c0e500b653...
      EmptyStorageRoot = keccak256(rlp(empty trie))
                       = 56e81f171bcc55a6ff8345e692c0f86e5b48e01b...

    These are the codeHash/storageRoot of every EOA and every account
    that has never deployed code / written storage. They're pinned so
    a typo in either constant becomes a loud test failure rather than
    a silent consensus-level divergence with mainline Ethereum tooling.

  src/evm/state_db.{h,cpp}    CEvmStateDB

    Thin LevelDB wrapper following the CAssetsDB pattern. Three key
    spaces in $datadir/evmstate/:

      'A' + address(20)               -> CEvmAccount
      'C' + codeHash(32)              -> bytecode bytes
      'S' + address(20) + slot(32)    -> uint256 (storage value)

    Logs ('L') and receipts ('R') will be added when execution lands
    (Phase 2.2+).

  src/evm/state_cache.{h,cpp} CEvmStateCache

    Write-through dirty layer. Reads cascade dirty -> DB. Writes stay
    in dirty until Flush(); Discard() abandons them. Honours SSTORE
    semantics on Flush: zero-valued storage entries are erased rather
    than written so the on-disk form stays compact (matches the gas-
    refund behavior we'll model in execution).

    Account deletion is tracked separately from account write so that
    SetAccount after DeleteAccount supersedes the deletion (matches
    the semantic of SELFDESTRUCT followed by re-CREATE2 to the same
    address in a single block).

    Thread safety: NOT thread-safe by design. Each EVM execution gets
    its own cache during the worker-pool phase (per design decision
    D1 — workers run in parallel on disjoint snapshots, merge to the
    shared parent under cs_main).

  src/test/evm_state_tests.cpp  14 cases / 70 assertions

    Boost test suite covering:
      - CEvmAccount serialization round-trip
      - Canonical constants pinned against the Ethereum yellow paper
      - In-memory CEvmStateDB Read/Write/Erase/Has for all 3 spaces
      - Cache read-through to DB without marking dirty
      - Cache dirty-write semantics
      - Flush persists dirty to DB
      - Discard abandons dirty without DB writes
      - Cache delete shadows the DB entry pre-Flush
      - SetAccount after DeleteAccount supersedes
      - SSTORE-zero clears the slot on Flush

Build wiring:
  src/Makefile.am               adds new headers + sources
  src/Makefile.test.include     adds evm_state_tests.cpp

Test totals after this commit:
  evm_smoke_tests    3 cases /  39 assertions
  evm_evmtx_tests   13 cases /  70 assertions
  evm_state_tests   14 cases /  70 assertions
  TOTAL             30 cases / 179 assertions

What's deliberately out of scope:

  - Merkle-Patricia-Trie root computation. Phase 2.1 keeps state as
    a flat key-value store; the trie root for the block header
    (stateRoot per decision D2) is computed in Phase 2.2 alongside
    the evmc::Host that drives EVM execution.
  - Snapshot / revert support for nested EVM CALL/CREATE. Phase 2.1
    cache flushes are all-or-nothing per block. Snapshot semantics
    land with execution in Phase 2.2.
  - Logs and receipts persistence. Added when execution starts emitting
    them (Phase 2.2).
  - Account address derivation for CREATE / CREATE2. Lives with the
    execution model.
  - cs_main integration. The cache is intentionally standalone and
    not yet hooked into ConnectBlock / DisconnectBlock. That wiring
    is the gating step of Phase 2.3.
Wires evmone to the Phase 2.1 state primitives via a full evmc::Host
implementation. The host translates between evmc's wire types
(evmc::address, evmc::bytes32, evmc::uint256be) and Bitcoin Core's
uint160 / uint256, dispatches storage / balance / code lookups to
CEvmStateCache, and exposes execution context (chain ID, block
number, base fee, coinbase, origin) for the corresponding EVM
opcodes.

What lands:

  src/evm/host.{h,cpp}        CEvmHost (full evmc::Host override)

    All 14 evmc::HostInterface virtual methods implemented:

      account_exists                routes to CEvmStateCache
      get_storage                   routes to CEvmStateCache (zero on miss)
      set_storage                   routes to CEvmStateCache, returns
                                    EIP-2200-style status (ASSIGNED /
                                    ADDED / MODIFIED / DELETED)
      get_balance                   reads CEvmAccount.balance
      get_code_size / _hash / copy_code   reads code via CEvmAccount.codeHash
      selfdestruct                  records intent (caller processes
                                    beneficiary transfer post-exec)
      call                          stub returning EVMC_REVERT — nested
                                    CALL/CREATE land in Phase 2.3 with
                                    snapshot/revert support
      get_tx_context                serializes ExecutionContext into
                                    evmc_tx_context (chain ID, block
                                    number/time/gas-limit/base-fee,
                                    coinbase, origin, prev block hash)
      get_block_hash                dispatches to a caller-supplied
                                    BlockHashFn callback (keeps the
                                    EVM layer testable without depending
                                    on chain.cpp)
      emit_log                      captures LOG0..LOG4 in an in-memory
                                    vector for the caller to drain into
                                    the surrounding tx receipt
      access_account / access_storage    EIP-2929 cold-then-warm tracking
      get_transient_storage /
        set_transient_storage       EIP-1153 (Cancun); lives only within
                                    the host instance, cleared per outer
                                    tx as required by EIP-1153 isolation

    Type conversion helpers (static):
      ToUint160 / FromUint160       evmc::address (20-byte) <-> uint160
      ToUint256 / FromUint256       evmc::bytes32 (32-byte) <-> uint256

    Inspection helpers (post-execution):
      Logs()              vector<Log> of LOG0..LOG4 emissions
      Selfdestructs()     set of contract addresses that issued SELFDESTRUCT

  src/evm/host.h ExecutionContext  block + tx context for the host

    Fields: chainId, blockHeight, blockTimestamp, blockGasLimit,
    baseFee (uint256), coinbase (uint160), txOrigin (uint160),
    txGasPrice (uint256), prevBlockHash (uint256).

  src/test/evm_host_tests.cpp     9 cases / 129 assertions

    Real-bytecode execution against the host backed by an in-memory
    CEvmStateCache:

      - SSTORE 0x42 at slot 1 → SLOAD → RETURN. Verify the cache holds
        the value at the expected (address, slot) key after execution.
      - EIP-1153 TSTORE → TLOAD round-trip; verify the value is
        readable within the execution and NOT bleeding into persistent
        storage afterward.
      - BALANCE opcode reads from pre-populated CEvmAccount via the
        cache.
      - emit_log captures address + topics + data with correct sizing.
      - EIP-2929 access_account / access_storage return COLD then WARM
        on subsequent calls; distinct slots stay cold independently.
      - call() stub returns EVMC_REVERT (Phase 2.3 will replace).
      - get_tx_context serializes chain ID with correct big-endian
        layout (chain 7373 -> bytes[30..31] = {0x1C, 0xCD}).
      - selfdestruct records the address; idempotent.

Test totals after this commit:
  evm_smoke_tests    3 cases /  39 assertions
  evm_evmtx_tests   13 cases /  70 assertions
  evm_state_tests   14 cases /  70 assertions
  evm_host_tests     9 cases / 129 assertions
  TOTAL             39 cases / 308 assertions

Out of scope for Phase 2.2:

  - Nested call / create (CALL / CALLCODE / DELEGATECALL / STATICCALL /
    CREATE / CREATE2). The host returns EVMC_REVERT for any call().
    Phase 2.3 implements these with proper snapshot/revert semantics
    so a reverted nested call rolls back state changes made within it.
  - SELFDESTRUCT beneficiary balance transfer. The address is recorded;
    the caller-side post-exec processor lands in Phase 2.3.
  - EIP-2200/3529 storage-status RESTORED variants. Requires tracking
    the value at outer-tx start; lands with snapshot support (Phase 2.3).
  - BLOCKHASH lookup against a real chain. The host accepts a
    BlockHashFn callback that the ConnectBlock integration (Phase 2.4)
    will supply.
First slice of the Phase 2.3 "execute a single EVM transaction" layer.
Takes a CEvmCallTx (validated by CheckEvmCallTx in Phase 1.1), wires
up a CEvmHost over the supplied CEvmStateCache, builds an evmc_message
from the payload, and dispatches it through evmone at Cancun revision
(per design decision D6).

This commit deliberately implements ONLY the CALL path. Deploy
(CREATE / address derivation / code installation) is Phase 2.3b.
Spend (EVM account -> UTXO) is Phase 2.3c. Nested CALL inside the
executing contract is still stubbed to EVMC_REVERT by CEvmHost::call
and lands in Phase 2.3d alongside snapshot/revert support.

What lands:

  src/evm/apply.{h,cpp}    ApplyEvmCallTx + ApplyResult

    Inputs:
      - CEvmCallTx payload (sender, recipient, value, calldata,
        gasLimit, EIP-1559 fee params, nonce)
      - CEvmStateCache for storage / balance / code
      - ExecutionContext for block + tx context

    Internals:
      - Derives 20-byte EVM addresses from the payload's uint256
        senderHash / toAddress (low 20 bytes per Ethereum convention).
      - Reads the recipient's deployed code from the cache, if any.
        A call to a code-less account is legal — evmone returns
        success with no output.
      - Builds the evmc_message and runs evmone.

    Output (ApplyResult):
      - statusCode (EVMC_SUCCESS / EVMC_REVERT / EVMC_OUT_OF_GAS / ...)
      - gasUsed = gasLimit - r.gas_left
      - returnData (RETURN output)
      - logs (LOG0..LOG4 emissions, captured by CEvmHost)
      - selfdestructs (set of addresses that issued SELFDESTRUCT)

    The caller (Phase 2.4 ConnectBlock integration) decides what to
    do with the result: on EVMC_SUCCESS, Flush() the cache and record
    side effects in the block receipt set; on revert / OOG / any
    failure, Discard() the cache.

  src/test/evm_apply_tests.cpp    4 cases / 23 assertions

    Real-bytecode end-to-end tests:
      1. Call to a codeless account returns EVMC_SUCCESS with empty
         output (the EOA-to-EOA degenerate case).
      2. Call into a deployed SSTORE contract persists storage to the
         cache, and post-Flush the value is readable from the DB layer.
      3. Call into a contract that REVERTs returns EVMC_REVERT, and
         Discard()-ing the cache rolls back the dirty SSTORE that
         evmone wrote before the REVERT (the caller is responsible
         for the discard — evmone applies state mutations eagerly
         and lets the host decide on rollback).
      4. LOG0 emission inside the contract surfaces correctly in
         ApplyResult.logs with address + data + zero topics.

    The tests bypass ApplyEvmDeployTx (not yet written) by directly
    populating the cache with a CEvmAccount + WriteCode for the test
    contract. The codeHash used is a Bitcoin Core SHA256-based hash
    rather than real Ethereum Keccak-256 — that's deliberate for
    Phase 2.3a since CEvmStateCache::SetCode is content-addressed by
    whatever 32-byte hash the caller supplies, and we don't yet have
    a wrapped Keccak-256 helper outside evmone itself. Phase 2.3b
    fixes this by deriving real Keccak-256 codeHashes during deploy.

Test totals after this commit:
  evm_smoke_tests    3 cases /  39 assertions
  evm_evmtx_tests   13 cases /  70 assertions
  evm_state_tests   14 cases /  70 assertions
  evm_host_tests     9 cases / 129 assertions
  evm_apply_tests    4 cases /  23 assertions
  TOTAL             43 cases / 331 assertions

Out of scope for Phase 2.3a (deferred):

  - Gas accounting: sender balance check, debit-upfront-for-gas,
    refund-unused-at-end, priority-fee-to-coinbase, base-fee-burn.
    Phase 2.4 wires this with ConnectBlock so all the bookkeeping
    happens under the same lock as the rest of block validation.
  - Nonce increment on the sender. Same reason — needs to be
    atomic with the gas accounting.
  - Sender balance debit for the `value` field. Phase 2.4.
  - Nested calls. CEvmHost::call still returns EVMC_REVERT.
    Phase 2.3d wires nested execution with snapshot/revert so a
    reverted inner call doesn't pollute the outer state.
  - Contract deployment (CREATE). Phase 2.3b.
  - EVM account -> UTXO move (TRANSACTION_EVM_SPEND). Phase 2.3c.
  - State-trie root computation for the stateRoot block-header
    field (D2). Phase 2.4.
…ddress

Adds the CREATE / contract-deployment path. Contracts deployed via a
CEvmDeployTx now land at the exact same address they would on Ethereum
mainnet for the same (sender, nonce) pair, with their runtime code
keyed by real Keccak-256 (not FIPS SHA3-256, not a Bitcoin-style hash).

This means Solidity contracts compiled by solc and deployed through
our tx type are portable to and from Ethereum tooling: bytecode is
verifiable, addresses are predictable, codeHashes match EXTCODEHASH
output on a geth node. Cross-chain tooling (Etherscan-style explorers,
block-indexers, etc.) that depends on these primitives will Just Work.

What lands:

  src/evm/hashing.{h,cpp}    Keccak256 + ContractAddressFromCreate

    Keccak256(data) — wraps ethash_keccak256 from the bundled
    libevmone-standalone. Returns a uint256 whose m_data layout
    matches the on-wire big-endian form (matches our evmc::bytes32
    conversion convention throughout the EVM module).

    ContractAddressFromCreate(sender, nonce) — derives the contract
    address per the Ethereum yellow paper:

      address = last 20 bytes of keccak256( rlp([sender, nonce]) )

    Includes a minimal RLP encoder for the (bytes20, uint64) pair
    case (sufficient for CREATE derivation; a general-purpose RLP
    encoder for eth_sendRawTransaction lives elsewhere in Phase 3).

    The ethash headers were copied into the depends prefix from the
    Hunter cellar — same workaround as evmc headers in Phase 0.
    Proper depends/ packaging of ethash lands in the Phase 1.0 Issue 2
    cleanup.

  src/evm/apply.{h,cpp}    ApplyEvmDeployTx + ApplyResult.deployedAddress

    Decodes a CEvmDeployTx (validated upstream by CheckEvmDeployTx in
    Phase 1.1), derives the contract address from (sender, nonce),
    enforces the EIP-161 / A11 collision check (refuse to overwrite
    an address that already has code or non-zero nonce), and
    dispatches the init bytecode through evmone with EVMC_CREATE
    kind. The init code's RETURN output is captured as the runtime
    code, keccak256'd, and written into the cache.

    On success the cache contains:
      - Code at codeHash = keccak256(runtime_code).
      - CEvmAccount(nonce=1, balance=0, codeHash, EmptyStorageRoot)
        at the derived address.

    On EVMC_FAILURE / collision, the existing account is preserved
    intact.

    Sender-side bookkeeping (nonce increment, balance debit for gas)
    is still Phase 2.4 work — same scope rule as ApplyEvmCallTx.

  src/test/evm_hashing_tests.cpp    6 cases / 14 assertions

    Pinned against the universally-cited Ethereum test vectors:

      - keccak256("")    = c5d2460186f7233c927e7db2dcc703c0e500b653...
      - keccak256("abc") = 4e03657aea45a94fc7d47ba826c8d667c0d1e6e3...

      ContractAddressFromCreate test vectors (sender =
      0x6ac7ea33f8831ea9dcc53393aaa88b25a785dbf0):
        nonce 0 -> 0xcd234a471b72ba2f1ccf0a70fcaba648a5eecd8d
        nonce 1 -> 0x343c43a37d37dff08ae8c4a11544c718abb4fcf8
        nonce 2 -> 0xf778b86fa74e846c4f0a1fbd1335fe81c00a0c91
        nonce 3 -> 0xfffd933a0bc612844eaf0c6fe3e5b8e9b6c1d19c

    These are the same vectors that geth, erigon and nethermind pin
    in their own test suites. Matching byte-for-byte is the
    correctness gate for "Ethereum compatibility".

  src/test/evm_apply_tests.cpp    3 new deploy cases

    1. Deploy a minimal contract (init code returns a 6-byte runtime
       that SSTOREs slot 1 with 0x42 then STOPs). Verify the contract
       lands at ContractAddressFromCreate(sender, nonce), its
       CEvmAccount has codeHash=Keccak256(runtime), and the runtime
       is stored under that hash.

    2. Deploy then CALL: the same contract from test 1, called via
       ApplyEvmCallTx using the address returned in
       ApplyResult.deployedAddress. Verify storage slot 1 holds
       0x42 after the call — proves the deploy/call cycle is
       wired correctly end-to-end.

    3. Collision refusal: place an existing account with a non-empty
       codeHash at the address (sender, nonce=5) would derive, then
       attempt the deploy with that exact (sender, nonce). Verify
       EVMC_FAILURE, full gasLimit consumed, and the pre-existing
       account unchanged.

  The existing apply tests' codeHash helper was migrated from the
  Bitcoin Core HashWriter placeholder to evm::Keccak256, so those
  tests now also serve as integration coverage of the new hashing
  module.

Test totals after this commit:
  evm_smoke_tests        3 cases /  39 assertions
  evm_evmtx_tests       13 cases /  70 assertions
  evm_state_tests       14 cases /  70 assertions
  evm_host_tests         9 cases / 129 assertions
  evm_apply_tests        7 cases /  43 assertions (+3 deploy tests, +20 asserts)
  evm_hashing_tests      6 cases /  14 assertions
  TOTAL                 52 cases / 438 assertions

Out of scope for Phase 2.3b (deferred):

  - CREATE2 derivation (salt + init code hash). Easy to add once we
    expose CREATE2 at the tx-payload level; for Phase 2.3 only
    classic CREATE is needed via TRANSACTION_EVM_DEPLOY (tx type 11).
  - Sender balance debit + nonce increment as part of the deploy.
    Still Phase 2.4 alongside the rest of the gas accounting.
  - SELFDESTRUCT beneficiary balance transfer. Phase 2.3d.
  - Nested CREATE inside the executing init code (host.call still
    stubs to REVERT for CREATE/CALL/etc.). Phase 2.3d.
Moves RTM weis from an EVM account to a UTXO output. Caller adds the
resulting credit (CScript + satoshi amount) to the surrounding block
during ConnectBlock integration (Phase 2.4).

Validation:
  * amount must be a non-zero exact multiple of 10^10 weis (1 satoshi);
    non-multiples are refused so the chain never silently rounds funds
  * outputScript must be non-empty
  * source EVM account must exist
  * account.balance >= amount (no overdraft)

On success: debits the account's balance and emits one UtxoCredit; gas
charged is the Ethereum baseline 21000 for a value transfer. Fee
accounting (debit at effective gas price, refund of unused gas) lands
with the rest of the EIP-1559 wiring in Phase 2.4.

Constants:
  * kWeisPerSatoshi = 10^10 (1 RTM = 10^18 weis = 10^8 satoshis)

Tests (7 cases / 32 assertions; all 14 evm_apply cases green):
  * happy path: balance debited, single UtxoCredit with correct script
    and satoshi amount
  * entire-balance spend (zero-balance account remains)
  * non-multiple amount refused, balance untouched
  * zero amount refused
  * empty output script refused
  * missing source account refused
  * insufficient balance refused, balance untouched
  * debit persists through Flush() to the underlying CEvmStateDB
Replaces the Phase 2.2 host.call() stub with a full nested-call
dispatcher covering all six evmc message kinds, backed by a savepoint
stack on CEvmStateCache.

CEvmStateCache: snapshot stack.
  * Snapshot() returns an opaque id, capturing the four dirty maps.
  * Revert(id) restores them and drops any savepoints below.
  * Commit(id) discards the saved copy, keeping current state.
  * Eager copy on Snapshot — simple, correct, and fine for the small
    dirty sets a single tx produces. Journal-based deltas are a
    future perf optimization.

CEvmHost::call dispatcher.
  * EVMC_CALL / EVMC_CALLCODE perform value transfer (sender debit,
    recipient credit) before running the target code.
  * EVMC_DELEGATECALL preserves the outer frame's sender + value and
    runs the target code in the caller's storage context.
  * EVMC_STATICCALL passes msg.flags through; evmone enforces the
    no-state-mutation contract internally.
  * EVMC_CREATE / EVMC_CREATE2 (CallCreate helper) derive the new
    address, collision-check, bump sender nonce, transfer value,
    run init code, and install runtime code on EVMC_SUCCESS. The
    create_address field is populated in the returned evmc::Result.
  * Every nested frame opens a savepoint up front. EVMC_REVERT, OOG,
    or any failure status reverts; EVMC_SUCCESS commits.
  * INSUFFICIENT_BALANCE is returned for value transfers that exceed
    the sender's balance, matching evmone's expected status code.

CEvmHost::selfdestruct now performs the beneficiary balance transfer
immediately (so subsequent reads in the same frame see the credited
beneficiary). The transfer rides on the savepoint, so an outer revert
also rolls it back. SELFDESTRUCT-on-self is a no-op for balance.

Hashing: ContractAddressFromCreate2 — EIP-1014 formula
  keccak256(0xff || sender || salt || keccak256(init_code))[12:]
mirrors the CREATE helper added in Phase 2.3b and is what CallCreate
uses for EVMC_CREATE2 dispatch.

Tests (+9 cases, all six EVM suites green):
  evm_state_tests +4:
    * snapshot_revert_restores_dirty_state
    * snapshot_commit_keeps_changes
    * snapshot_nested_outer_revert_drops_inner
    * snapshot_inner_revert_preserves_outer
  evm_host_tests +5:
    * nested_call_persists_inner_sstore
    * nested_call_revert_rolls_back_inner_sstore
    * nested_staticcall_blocks_inner_sstore
    * nested_call_with_value_transfers_balance
    * nested_create_installs_child_at_derived_address
  Plus the existing call_stub_returns_revert test was renamed and
  retargeted to the new "codeless zero-value CALL succeeds" semantics
  (matching how Ethereum treats a CALL to an EOA).
Wraps the Phase 2.3 Apply* dispatchers with the per-tx fee accounting
layer that ConnectBlock will use (Phase 2.4e). Each ProcessEvm*Tx does
the standard EIP-1559 flow:

  1. Pre-flight: account exists, nonce matches, priority <= cap,
     baseFee <= cap, and balance >= gasLimit*maxFee + value.
  2. Pre-debit: gasLimit*maxFee + value reserved from sender.
     Sender nonce bumped (charged-failure semantics: a tx that
     executes and reverts still consumes the nonce).
  3. Snapshot the cache, dispatch through ApplyEvm*Tx.
  4. Post:
       * On EVMC_SUCCESS: commit snapshot, refund unused gas at
         the effective rate.
       * On revert / OOG / failure: revert snapshot (rolls back
         all storage / log / created-account effects), refund the
         unused gas AND the value reservation (the value never
         left the sender's account at the apply layer).
  5. Compute ProcessFee:
       burned       = gasUsed * baseFee     (deflationary; no recipient)
       coinbaseTip  = gasUsed * (effective - baseFee)
                                            (credited to the miner)

Shared infrastructure:
  * evm/balance.{h,cpp}: byte-level uint256 BE arithmetic helpers
    (Ge/Sub/Add by uint64; Uint256<->uint64 round-trip). Used by
    both apply.cpp (spend path) and process.cpp; replaces the
    file-local copies that lived in apply.cpp in Phase 2.3c.

Tests (10 cases / 7th EVM suite green):
  * call_success_debits_balance_and_bumps_nonce
  * call_into_sstore_charges_real_gas_and_splits_fee
  * call_rejects_unknown_sender
  * call_rejects_nonce_mismatch
  * call_rejects_insufficient_upfront_balance
  * call_rejects_priority_above_max_fee
  * call_rejects_base_fee_above_max_fee
  * call_revert_charges_gas_refunds_value
  * deploy_success_debits_sender_and_installs_contract
  * spend_success_charges_21000_gas_and_records_utxo

The ConnectBlock wiring (CChainState owns the EVM state, iterates
EVM-typed txs, sums coinbaseTip into the miner's output, burns the
base-fee portion) lands in Phase 2.4e — kept separate because it
touches validation.cpp and changes a long-held call signature.
Adds the function ConnectBlock will call (Phase 2.4e-2) to dispatch
all EVM-typed transactions in a block through the Phase 2.4 process
layer. Keeps the iteration logic in the EVM module where it stays
unit-testable in isolation; the validation.cpp wiring on top becomes
a small follow-up.

ProcessEvmTransactionsInBlock(block, pindex, cache, contextTemplate)
walks block.vtx in order and:
  * skips non-EVM txs (TRANSACTION_NORMAL, asset/ProRegTx etc.);
  * deserialises the payload for each TRANSACTION_EVM_DEPLOY / CALL /
    SPEND via GetTxPayload<T>;
  * builds a per-tx ExecutionContext (origin + effective gas price)
    from the block-level template;
  * dispatches through ProcessEvm{Call,Deploy,Spend}Tx, which carries
    its own preflight + snapshot + EIP-1559 fee accounting;
  * aggregates totalCoinbaseTip + totalBurned + utxoCredits across
    the block's EVM txs;
  * on preflight failure: stops processing, sets ok=false and
    failedTxIndex to the offending tx — the caller rejects the block.
  * on charged failure (REVERT / OOG): records the tx in txResults
    with its real status; block accumulation continues.

The single CEvmStateCache instance is shared across txs in the block,
so tx N+1 sees state changes from tx N (Ethereum sequential-execution
within a block). Phase 2.5's worker pool will replicate this result
via private caches + serial merge.

Tests (7 cases / 8th EVM suite green):
  * empty_block_yields_empty_result
  * single_call_tx_aggregates_correctly
  * two_calls_aggregate_fees_and_advance_nonce
  * preflight_failure_aborts_block
  * charged_failure_included_block_ok
  * spend_tx_produces_aggregated_utxo_credit
  * deploy_then_call_in_same_block (state composes across txs)
Mirrors the passetsdb pattern to integrate the EVM state into the
consensus pipeline.

validation.h:
  * Forward-declares evm::CEvmStateDB / evm::CEvmStateCache near the
    other class forwards so the CChainState member decls below can
    reference them.
  * Adds `extern std::unique_ptr<evm::CEvmStateDB> pevmstatedb;` as
    the persistent EVM state pointer (cs_main-protected).
  * Extends CChainState::ConnectBlock and CChainState::DisconnectBlock
    with `evm::CEvmStateCache* evmStateCache = nullptr` — default
    nullptr keeps every existing caller compiling unchanged.

validation.cpp:
  * Defines pevmstatedb alongside passetsdb (line ~188).
  * Inside ConnectBlock, after the tx loop and EIP-152-style verify
    work, when (evmStateCache != nullptr && Updates().IsEvmActive(
    pindex->pprev)) is true, runs ProcessEvmTransactionsInBlock
    against the supplied per-block cache. Pre-flight failures from
    the iterator surface as state.DoS(100, …, "bad-evm-tx").
  * Updates ConnectTip to build a block-local CEvmStateCache layered
    over pevmstatedb, pass it through, and Flush() on success
    alongside the asset cache flush. Other ConnectBlock call sites
    (TestBlockValidity, VerifyDB) keep evmStateCache=nullptr — they
    are sandbox-only paths and don't yet need EVM state assertions.
  * DisconnectBlock accepts the new parameter but does not act on it
    yet — reverting EVM state requires the undo records from Phase
    2.6 reorg journaling.

init.cpp:
  * Opens pevmstatedb during AppInitMain alongside passetsdb (chain
    reset wipes both in lock-step).
  * Resets pevmstatedb in PrepareShutdown.

Phase 2.4e parking lot, in order:
  * EIP-1559 base fee dynamics + the header `baseFee` field (D2).
    Currently hardcoded to 0 in the dispatch — fee accounting still
    runs, the burn portion is just always zero.
  * Coinbase-output verification: the miner must include an output
    crediting totalCoinbaseTip. Requires miner.cpp work and a
    consensus rule, both behind the EVM activation hard-fork.
  * Phase 2.6: EVM-side journal in CBlockUndo so DisconnectBlock can
    actually revert.

All 8 EVM test suites pass; non-EVM suites unaffected by the wiring
(the EVM dispatch is gated on a non-null cache and an active gate,
neither of which exists in unit tests today).
Adds the EVM-side undo journal that DisconnectBlock will use (Phase
2.6-2) to revert state on a reorg. Mirrors the asset-side pattern
where pre-block deltas are persisted to the state DB keyed by block
hash; DisconnectBlock reads them and applies the inverse.

evm/undo.{h,cpp}:
  * AccountChangeUndo, StorageChangeUndo records carry the pre-block
    value and an existedBefore flag so "didn't exist" is a first-class
    case (the inverse is an erase, not a write of zero).
  * CEvmStateUndo bundles all three change kinds plus the list of
    newly-added code hashes (code is content-addressed so only NEW
    hashes need recording — existing entries stay valid).
  * BuildUndoFromCache(cache, db) walks the cache's dirty layer and
    reads the underlying DB for each entry's pre-block value. Must be
    called BEFORE cache.Flush().
  * ApplyUndoToDB(undo, db) inverts: writes `before` for entries that
    existedBefore, erases the others; erases the newly-added code
    blobs by hash.

evm/state_db.{h,cpp}: persistence helpers
  * 'U'-prefixed key space: block hash -> serialized CEvmStateUndo.
  * WriteBlockUndoBytes / ReadBlockUndoBytes / EraseBlockUndo.
  * EraseCode for revert of new code installations.

evm/state_cache.h: const accessors over the dirty maps
  (DirtyAccounts / DeletedAccounts / DirtyStorage / DirtyCode) so
  BuildUndoFromCache can iterate them without friend coupling.

Tests (9 cases / 9th EVM suite green):
  * build_undo_records_pre_block_account
  * build_undo_marks_new_account_as_not_existed
  * build_undo_captures_storage_pre_state
  * build_undo_lists_only_new_code_hashes
  * apply_undo_restores_pre_block_state
  * apply_undo_restores_deleted_account
  * undo_serialization_round_trip
  * db_block_undo_persistence
  * full_round_trip_via_db_persistence

Phase 2.6-2 (next) wires ConnectBlock to call BuildUndoFromCache +
persist the result, and DisconnectBlock to read+apply+erase.
Closes the loop on the Phase 2.6 undo infrastructure: the journal is
now built and persisted on connect, and applied + erased on disconnect.

validation.cpp:
  * ConnectTip: between cache.Flush() and dbTx->Commit(), build the
    pre-block undo via BuildUndoFromCache(*evmCachePtr, *pevmstatedb),
    serialise it, and write it to pevmstatedb keyed by the new
    block's hash. The build call happens before Flush so it can read
    the pre-block account/storage values from the DB; the write
    happens after Flush so the undo blob lives alongside the post-
    block state in the same dbTx commit. Skip writing when the undo
    is empty (pre-EVM blocks or EVM blocks with no state changes).
  * DisconnectTip: after the asset cache flushes, look up the EVM
    undo journal for the block being disconnected. If present,
    deserialise, apply via ApplyUndoToDB (writes the pre-block
    account / storage values back, erases new code blobs), then
    erase the journal entry. Skip silently when no entry exists.
  * Both paths use assertions on the inner DB writes — a partial
    undo or failed write is a fatal node-state inconsistency and
    should crash the node rather than silently diverge.

With this in place a chain reorg of EVM-active blocks correctly
restores the EVM state to its pre-reorg snapshot, satisfying the
A10 review requirement for Phase 2.

All 9 EVM test suites still green; non-EVM suites unaffected.
Adds ParallelProcessEvmTransactionsInBlock — the D1 worker-pool API
surface for block-level EVM execution.

evm/parallel.{h,cpp}:
  * Stage 1 — parallel pre-flight: each EVM-typed tx is queued to
    std::async with launch::async to deserialize its payload and
    validate structure on a worker thread. The work is pure (no
    cache writes) so the parallel slice is trivially safe.
  * Stage 2 — serial execute: the pre-flighted records are walked
    in block order and dispatched through ProcessEvm{Call,Deploy,
    Spend}Tx against the shared CEvmStateCache, exactly like the
    serial ProcessEvmTransactionsInBlock. Same aggregates surface
    (totalCoinbaseTip / totalBurned / utxoCredits) and the same
    failedTxIndex semantics on pre-flight error.

The MVP guarantee is byte-identical results to the serial baseline
— intentional. Phase 2.5 establishes the parallel-API foundation
without taking the consensus risk of speculative parallel EVM
execution. True parallel execution requires read/write-set tracking
+ optimistic-concurrency conflict detection + retry, and should be
driven by empirical TPS data rather than pre-launch speculation;
the file-level header documents this as the explicit next step.

The famous Qtum cs_main-held-during-EVM problem also remains future
work: it requires releasing cs_main during EVM execution and
re-acquiring for the merge, which is a deeper ConnectBlock refactor.
Phase 2.5 is single-thread-equivalent for execution, so this commit
does NOT yet address Qtum's pathology — flagged in parallel.h for
the next iteration.

Tests (4 paired cases / 10th EVM suite green):
  * empty_block_matches_serial
  * single_call_matches_serial
  * many_txs_match_serial (8 distinct senders + 2 noise non-EVM txs;
      verifies aggregates AND final per-sender balance/nonce match)
  * preflight_failure_index_matches_serial

Phase 2 closes here for correctness. Open follow-ups (review-marked
in memory + plan):
  * D2 header fields (stateRoot / receiptsRoot / chainLocksCommit /
    baseFee) — hard-fork coordinated.
  * Coinbase tip output verification — needs miner.cpp.
  * True parallel EVM execution with read/write-set tracking — perf,
    not correctness; defer until post-testnet workload data.
  * cs_main release during EVM execution — Qtum mitigation.
First slice of the Ethereum JSON-RPC compatibility layer. Extends the
existing rpc/ethereum.cpp (which housed the Phase 0 evm_executeReadOnly
probe) with the read-only eth_* methods that MetaMask / ethers.js /
viem / web3.js need to attach to a Raptoreum node:

  * eth_chainId             — picks 7373/7374/7375 from the active
                              CChainParams (mainnet/testnet/regtest).
                              EIP-155 replay protection ready.
  * eth_blockNumber         — ::ChainActive().Height() as a 0x quantity.
  * eth_gasPrice            — 0x0 until EIP-1559 base-fee dynamics
                              activate (tracked as FUP-1/FUP-2).
  * eth_getBalance          — reads pevmstatedb directly; falls back
                              to 0 for unknown / pre-EVM contexts.
  * eth_getTransactionCount — account.nonce as a 0x quantity.
  * eth_getCode             — runtime bytecode by codeHash; '0x' for
                              EOAs or empty-code accounts (canonical).
  * eth_getStorageAt        — 32-byte storage word; unset reads as
                              the canonical zero word per EVM spec.

Block-tag handling: 'latest' and 'pending' accepted; any other tag
(historical hex block numbers, 'earliest') is rejected with a
descriptive error. Historical EVM state queries arrive together with
the log/receipt indexer in a follow-up commit (FUP-6 trail).

Hex / address parsing: 0x-prefixed mandatory, validated against the
exact byte length (20 for addresses, 32 for slots). Ethereum-style
"quantity" formatting drops leading-zero nibbles ("0x0" for zero);
"data" formatting preserves natural byte length.

Live regtest smoke validated end-to-end:
  $ ./raptoreum-cli eth_chainId         → 0x1ccf  (7375 regtest)
  $ ./raptoreum-cli eth_blockNumber     → 0x0
  $ ./raptoreum-cli eth_gasPrice        → 0x0
  $ ./raptoreum-cli eth_getBalance      0x...0a latest → 0x0
  $ ./raptoreum-cli eth_getTransactionCount 0x...0a latest → 0x0
  $ ./raptoreum-cli eth_getCode         0x...0a latest → 0x
  $ ./raptoreum-cli eth_getStorageAt    0x...0a 0x...01 latest →
       0x0000000000000000000000000000000000000000000000000000000000000000

Phase 3 roadmap (in order of upcoming commits):
  * 3.2  eth_call — read-only EVM execution against a state snapshot
  * 3.3  eth_getBlockByNumber / eth_getBlockByHash (block formatters)
  * 3.4  separate HTTP listener on -evmrpcport (default 8545) so
         MetaMask connects on its expected port
  * 3.5  RLP decoder + eth_sendRawTransaction → CEvmCallTx adapter
  * 3.6  receipts + eth_getLogs (needs receipt persistence)
Adds the read-only EVM execution surface that block explorers, DEX
front-ends, and most dApp tooling exercise on every page load.

rpc/ethereum.cpp:
  * eth_call: runs the supplied call object through evmone against
    a CEvmStateCache layered over pevmstatedb. The cache is never
    flushed, so the call is read-only — no nonce bump, no balance
    debit, no storage writes persisted. RETURN bytes surface as a
    0x-prefixed data hex; REVERT surfaces as a JSON-RPC error with
    the revert data embedded (standard Ethereum semantics so wallets
    can parse "Error(string)" strings out of it).
  * eth_estimateGas: same execution path; returns gasUsed as a 0x
    quantity. Single-attempt estimate today — binary-search-to-the-
    minimum is documented as a follow-up alongside the functional
    test trail.

The call object accepts the Ethereum-standard fields (from / to /
gas / gasPrice / value / data); gasPrice is parsed but ignored
(read-only execution). Block tag handling stays 'latest'/'pending'
only — historical snapshots arrive with the log/receipt indexer.

rpc/client.cpp:
  * Adds entries to vRPCConvertParams so the CLI client deserialises
    the callObject argument as JSON (instead of passing it as a raw
    string) before routing into raptoreumd. Also picks up the
    gas_limit numeric arg on evm_executeReadOnly.

Live regtest smoke:
  $ raptoreum-cli eth_call \
      '{"to":"0x000...000a","data":"0x"}' "latest"  →  0x
  $ raptoreum-cli eth_estimateGas \
      '{"to":"0x000...000a"}' "latest"              →  0x0

Real contract-call round-trip lands when eth_sendRawTransaction
(Phase 3.5) can mutate pevmstatedb from outside; until then this
exercises the call dispatcher + parsers + result formatting and
proves the parser/host wiring is correct.
…unts

Adds the block-exploration surface that explorers and MetaMask poll
on every page refresh. Four methods, all read-only:

  * eth_getBlockByNumber(tag, fullTx)
  * eth_getBlockByHash(blockHash, fullTx)
  * eth_getBlockTransactionCountByNumber(tag)
  * eth_getBlockTransactionCountByHash(blockHash)

Block-tag resolver supports 'latest', 'earliest', 'pending' and
0x-prefixed hex height. Ethereum's quantity hex permits odd-length
nibble strings ("0x0", "0xa", "0x10"); the parser pads before the
IsHex check to match. Out-of-range heights and unknown hashes return
JSON null per the Ethereum spec.

Block format follows the eth_getBlock shape:
  - Fields backed by Raptoreum data: number, hash, parentHash,
    nonce (zero-padded 8 bytes from CBlockHeader::nNonce),
    transactionsRoot (merkle root), timestamp, size, difficulty,
    transactions[], uncles[] (always empty).
  - Fields without a Raptoreum analog today (stateRoot, receiptsRoot,
    logsBloom, sha3Uncles, miner) return canonical zeros — FUP-1
    (D2 header fields) and a future receipt indexer will populate
    them properly.

fullTx=true projects each block tx onto an Ethereum tx object:
  - TRANSACTION_EVM_CALL  -> from / to / value / input / gas /
                             gasPrice / maxFeePerGas /
                             maxPriorityFeePerGas / nonce derived
                             from the CEvmCallTx payload.
  - TRANSACTION_EVM_DEPLOY-> same with to=null (Ethereum spec for
                             deploys) and input=init code.
  - Non-EVM txs           -> synthetic zeros (from=to=0x0, input=0x).
fullTx=false returns just the array of 0x-prefixed tx hashes.

rpc/client.cpp picks up the new boolean params so the CLI deserialises
"fullTx" correctly.

Live regtest smoke (genesis on regtest fork):
  eth_getBlockByNumber  0x0 false  -> full block object
  eth_getBlockByNumber  latest     -> same (height 0 only)
  eth_getBlockByHash    0x485...   -> matches by hash
  eth_getBlockTransactionCountByNumber 0x0 -> 0x1
  eth_getBlockByNumber  0xff false  -> null (out of range)
Adds the canonical-answer surface that every Ethereum-aware client
polls on connect to probe the node. MetaMask in particular expects
several of these to return non-error responses before it considers
itself "Connected" to a custom network.

eth_*:
  * eth_protocolVersion   -> "0x41" (constant; the JSON-RPC surface
                             is the real interop layer, not the wire
                             devp2p version).
  * eth_syncing           -> false at tip; otherwise a sync status
                             object with startingBlock/currentBlock/
                             highestBlock.
  * eth_accounts          -> [] (Phase 5 wallet integration populates).
  * eth_coinbase          -> 0x0 (wallet integration sources the EVM
                             coinbase address; until then, zero).
  * eth_mining            -> false (PoW + MN consensus is reported on
                             the UTXO side, not via the EVM RPC).
  * eth_hashrate          -> "0x0" (mirrors the above).
  * eth_maxPriorityFeePerGas -> "0x0" until EIP-1559 dynamics activate
                             (FUP-1/FUP-2).

net_*:
  * net_version           -> active chainId as a decimal string
                             ("7373" / "7374" / "7375" — required
                             form per the net_ spec, NOT 0x-hex).
  * net_listening         -> true when the connman is up.
  * net_peerCount         -> 0x-prefixed peer count via
                             connman.GetNodeCount(CONNECTIONS_ALL).

web3_*:
  * web3_clientVersion    -> "Raptoreum/<full-version>/EVM" so wallets
                             know which client they are talking to.

All eleven methods return the canonical zero-state values for a
node with no peers and no wallet integration, which is exactly what
MetaMask / ethers / viem / web3 need to begin a session.

Live regtest smoke validated each method individually.

Phase 3 status after this commit:
  * 3.1 read-only state queries        ✓ committed
  * 3.2 eth_call / eth_estimateGas      ✓ committed
  * 3.3 eth_getBlockBy{Number,Hash}     ✓ committed
  * 3.4 metadata methods                ✓ this commit
  * 3.5 eth_sendRawTransaction          ☐ RLP + EIP-155 sig recovery
                                          (deeper work; multi-session)
  * 3.6 eth_getTransactionReceipt /
        eth_getLogs                     ☐ needs receipt persistence
  * 3.7 second HTTP listener on 8545    ☐ MetaMask UX nicety
…r recovery)

The keystone of the Ethereum JSON-RPC namespace. Accepts a wallet-
signed EIP-1559 (type 0x02) or legacy EIP-155 transaction blob over
JSON-RPC, recovers the sender via secp256k1, wraps as a Raptoreum
special transaction with the matching CEvm{Call,Deploy}Tx payload,
and broadcasts through the standard mempool pipeline.

New modules
-----------

src/evm/rlp.{h,cpp}:
  Full RLP encoder + decoder (yellow-paper Appendix B). Phase 2.3b's
  hashing.cpp had only the encoder slice for CREATE address
  derivation; this module is the complete surface — RlpEncodeBytes,
  RlpEncodeUint, RlpEncodeList, RlpEncode, RlpDecode, plus a
  canonical-form-checking decoder that rejects non-minimal length
  prefixes and the 0x81-prefixed single-byte-below-0x80 trap. Output
  is uniquely encoded per value so downstream tx-hash determinism is
  not affected by parser ambiguity.

src/evm/rawtx.{h,cpp}:
  DecodeRawEthTx + EthTxHash. The decoder:
    * Discriminates EIP-1559 (envelope byte 0x02) from legacy
      (first byte >= 0xC0 — RLP list discriminator). Pre-EIP-155
      legacy (v in {27, 28}) is refused to prevent cross-chain
      replay.
    * Validates chainId == ActiveEvmChainId() for both formats.
    * Reconstructs the signing payload (0x02||rlp(first9) for 1559,
      rlp(nonce,gasPrice,gas,to,value,data,chainId,0,0) for legacy)
      and computes msgHash = keccak256(payload).
    * Recovers the EVM sender via CPubKey::RecoverCompact +
      keccak256(uncompressed_pubkey[1:])[12:].
    * Refuses non-empty access lists for now — populating warm
      slots through to evmone is a follow-up.

src/rpc/ethereum.cpp: eth_sendRawTransaction handler
  Parses 0x-prefixed wire bytes, dispatches DecodeRawEthTx, builds
  a CMutableTransaction with nType set to TRANSACTION_EVM_CALL (or
  TRANSACTION_EVM_DEPLOY if the decoded `to` was empty) and the
  serialised CEvm{Call,Deploy}Tx payload in vExtraPayload, then
  BroadcastTransaction. Returns the keccak256 of the original wire
  bytes — the Ethereum tx hash dApps see in eth_getTransactionByHash
  / eth_getTransactionReceipt.

Consensus carve-outs (necessary because EVM-typed txs intentionally
have no UTXO-side inputs/outputs — gas + value accounting lives on
the EVM-state DB side):

src/consensus/tx_check.cpp:
  TRANSACTION_EVM_DEPLOY / CALL / SPEND added to the allowEmptyTxInOut
  set (matches how TRANSACTION_QUORUM_COMMITMENT is treated).

src/validation.cpp:
  EVM tx types added to the AcceptToMemoryPool nType allow-list
  (the "bad-txns-type" check at line 400) and to the relay-fee
  carve-out (mempoolRejectFee + minRelayTxFee both skipped for EVM
  txs since their fee is the EIP-1559 gas debit, not a UTXO-side
  miner fee).

src/chainparams.cpp:
  Regtest EVM activation registered at heightActivated=0 so the
  RPC smoke + functional tests can exercise the full Phase 2 +
  Phase 3 pipeline immediately. Mainnet/testnet activation heights
  land with the D2 header fields in a coordinated hard fork
  (FUP-1).

Tests (12 cases / 11th EVM suite green):
  * 10 RLP round-trip + canonical-form-checking decoder tests
  * EIP-1559 envelope framing decodes 12 fields cleanly
  * Pre-EIP-155 legacy refused
  * Chain-id mismatch refused

Live end-to-end on regtest, EIP-1559 tx signed by eth-account:
  $ raptoreum-cli eth_sendRawTransaction "0x02f864821ccf80016482..."
    -> 0x07b187941c7917d132a703ed1063b9278cf686bfb95be605dc6f4da40870346b
  $ raptoreum-cli getrawmempool
    -> ["5188ec3b3c6a8ebf57bea154700afdb5224548d875e12bb745ecfd1d25a1c016"]

The returned hash is the keccak256 of the original wire bytes (what
ethers/MetaMask key receipts by). The mempool key is the wrapper
sha256d — both are addressable independently.

Open follow-ups (parking lot):
  * Non-empty access lists once evmone warm-slot tracking is wired.
  * eth_getTransactionByHash / eth_getTransactionReceipt — needs
    receipt persistence (Phase 3.6 next).
  * Mainnet activation height in chainparams (FUP-1).
Adds the per-tx receipt infrastructure that block explorers, log
indexers, and dApp post-tx UX depend on, plus the eth_getTransactionReceipt /
eth_getTransactionByHash JSON-RPC handlers.

evm/receipt.{h,cpp}:
  CEvmReceipt struct carrying:
    * Both tx hashes (ethTxHash from keccak-of-wire-bytes;
      rtmTxHash = wrapper sha256d).
    * Block coordinates (hash, height, txIndex).
    * Status (1 success / 0 failure).
    * gasUsed + cumulativeGasUsed.
    * effectiveGasPrice (derived from burned/tip split).
    * Sender + recipient (or contractAddress for deploys).
    * Log array (address, topics, data) — converted from the
      evmc::Host's emitted logs via ConvertHostLog.
  Receipts are NOT consensus-critical until D2 lands receiptsRoot
  in the header (FUP-1); they serve the JSON-RPC layer.

evm/state_db.{h,cpp}: persistence
  Three new key spaces:
    'R' + rtmTxHash(32)  -> serialized CEvmReceipt
    'X' + ethTxHash(32)  -> rtmTxHash(32)         (forward cross-index)
    'Y' + rtmTxHash(32)  -> ethTxHash(32)         (reverse cross-index)
  Forward index lets dApps look up receipts by the Ethereum hash they
  got back from eth_sendRawTransaction. Reverse lets ConnectTip's
  receipt-generation step populate CEvmReceipt::ethTxHash without
  iterating the forward index.

evm/connectblock.{h,cpp}, evm/parallel.cpp:
  BlockProcessResult.txBlockIndices — parallel to txResults, gives
  ConnectTip the block-vtx index each result came from so it can
  key receipts by the wrapper tx's GetHash().

validation.cpp:
  ConnectBlock now generates + persists receipts after the EVM
  dispatch returns successfully:
    * Walks BlockProcessResult.txResults.
    * For each, pulls the wrapper tx's nType + payload to populate
      sender / to / contractAddress.
    * Reads the rtm -> eth cross-index to populate ethTxHash (the
      eth_sendRawTransaction path pre-registers this).
    * Writes the receipt under the rtm-hash primary key.

rpc/ethereum.cpp:
  * eth_sendRawTransaction registers BOTH cross-index directions
    (eth->rtm + rtm->eth) at submit time so receipts persist with
    the eth-side hash visible immediately on block inclusion.
  * eth_getTransactionReceipt(txHash) — resolves via cross-index
    (or treats the input as an rtm-hash directly for internal
    tooling), loads CEvmReceipt, formats as Ethereum-shaped JSON
    with logs array. Null on miss; descriptive error on malformed
    input.
  * eth_getTransactionByHash(txHash) — minimum-viable surface
    sufficient for "what's the tx I just sent?" patterns; sources
    from the receipt rather than the original wrapper tx (txindex
    integration is a follow-up).

Live regtest validation:
  $ raptoreum-cli eth_getTransactionReceipt 0x0...0   -> null
  $ raptoreum-cli eth_getTransactionByHash  0x0...0   -> null
  $ raptoreum-cli eth_getTransactionReceipt 0xZZ      -> descriptive error
  $ raptoreum-cli eth_sendRawTransaction <signed-tx>  -> 0x07b187... (Eth hash)
  $ raptoreum-cli getrawmempool                       -> [<rtm hash>]
A full block-inclusion round-trip requires the sender's EVM account
to have funds (Phase 2.4 pre-flight rejects an unfunded sender at
block-validation time, which is correct consensus behavior).
Pre-funding paths land with the wallet integration (Phase 5) or an
internal admin RPC (FUP). All 11 EVM test suites green.

Phase 3.6d (eth_getLogs with filter — fromBlock/toBlock/address/
topics) is the natural follow-up — it iterates receipts across a
block range, same persistence layer; documented for next commit.
…/ topics)

Completes the log surface that event-listener libraries (ethers
Provider.on(), viem watchEvent, web3 subscriptions polled on-poll)
and indexers depend on.

rpc/ethereum.cpp:
  eth_getLogs(filter) accepts the standard Ethereum filter shape:
    {
      "fromBlock": "0x..." | "latest" | "earliest",
      "toBlock":   "0x..." | "latest" | "earliest",
      "address":   "0x..." | ["0x...", "0x..."]
      "topics":    [ topic_or_null_or_array, ... ]
    }

  Implementation:
    * Resolves fromBlock/toBlock via the existing tag resolver.
    * Caps the block span to kMaxLogBlockSpan = 10000 to keep a
      single call bounded.
    * For each block in range, reads from disk, walks vtx, picks out
      EVM-typed txs, loads their receipts (Phase 3.6 persistence),
      and applies the filter to each log.
    * Address filter: empty matches all; otherwise exact uint160
      equality against the log's emitting address.
    * Topic filter: per-position OR-of-array (Ethereum's standard
      semantics). Null at a position matches anything; a string is
      a single-value match; an array is "any of these"; positions
      beyond the filter array are unconstrained.

  Out-of-range or inverted block ranges return [] (matches what
  geth/erigon do). Malformed filter inputs surface as descriptive
  RPC errors.

rpc/client.cpp: filter is decoded as JSON before reaching the handler.

Live regtest validation:
  $ eth_getLogs {fromBlock:0x0, toBlock:latest}                 -> []
  $ eth_getLogs {address: 0x...}                                -> []
  $ eth_getLogs {topics:[null, "0x...01"]}                      -> []
  $ eth_getLogs {fromBlock:latest, toBlock:0x0}                 -> []
  $ eth_getLogs {address: 42}                                   -> error -8

The empty results are expected on regtest with no successful EVM
blocks; a populated dataset arrives once wallet pre-funding lands
(Phase 5) and a real contract emits LOGs that ConnectTip persists
into receipts.

Phase 3 RPC surface at this point:
  read-only state    : chainId, blockNumber, gasPrice, getBalance,
                       getTransactionCount, getCode, getStorageAt
  EVM probes         : call, estimateGas
  block exploration  : getBlockByNumber/Hash, getBlockTxCount/Hash
  metadata           : protocolVersion, syncing, accounts, coinbase,
                       mining, hashrate, maxPriorityFeePerGas,
                       net_{version,listening,peerCount},
                       web3_clientVersion
  write              : sendRawTransaction (EIP-1559 + legacy)
  receipts/logs      : getTransactionReceipt, getTransactionByHash,
                       getLogs

22 ethereum-namespace RPCs total. The 3.7 parking lot (a separate
8545 HTTP listener for MetaMask UX) is the only Phase 3 deliverable
that doesn't materially expand functionality.
Closes Phase 3. Binds the same JSON-RPC dispatcher to an additional
TCP port so MetaMask / ethers / viem / web3.js can attach to a
Raptoreum node on their conventional 8545 without per-port
configuration.

init.cpp: registers two new CLI args
  -evmrpcport=<port>  Default 8545. Set to 0 to disable the second
                      listener entirely.
  -evmrpcbind=<addr>  Bind address(es) for the EVM port. Defaults
                      to the same hosts -rpcbind uses (or loopback
                      when -rpcbind isn't set).

httpserver.cpp: HTTPBindAddresses now appends the EVM-port endpoints
to the existing endpoint list before binding. libevent's evhttp
accepts repeated evhttp_bind_socket_with_handle calls on the same
http context, so the cost is just an extra accept socket and a few
bytes of state — the dispatcher, work queue, auth, and handler
table are shared.

Side-effect: ALL existing RPC methods (non-eth_* too — getblockcount,
getblockchaininfo, etc.) are reachable on both ports. That's the
right behavior: a "second port" with reduced surface would have
required per-request routing inside the dispatcher and a separate
auth context. Same surface on both ports keeps the security model
identical to today.

Live regtest validation:
  \$ raptoreumd -evmrpcport=18545 ...
  \$ raptoreum-cli -rpcport=29918 eth_chainId  -> 0x1ccf
  \$ raptoreum-cli -rpcport=18545 eth_chainId  -> 0x1ccf     (same)
  \$ raptoreum-cli -rpcport=18545 getblockcount -> 0         (non-eth ok)

  \$ raptoreumd -evmrpcport=0 ...
  \$ raptoreum-cli -rpcport=8545 eth_chainId  -> connection refused

Phase 3 is functionally complete. 28 commits on feat/evm-integration,
22 ethereum-namespace RPC methods + the legacy raptoreum namespace
all reachable on both ports. The remaining Phase 3 parking lot is
the FUP register — receipt-pre-funding via wallet (Phase 5) and the
non-empty access list path in eth_sendRawTransaction.
…pile

Phase 4 of the integration plan ships RTM-native precompiles — the
key differentiator of the Camino C design. This commit lays the
framework and the first live precompile (ChainLocks).

evm/precompiles.{h,cpp}: dispatcher + ABI codec
  * IsPrecompileAddress(addr) recognises two address bands:
    - The fixed range 0x00..00a0X for X in {1, 2, 3, 4} — one
      address per RTM-subsystem precompile.
    - The Smart-Asset ERC-20 prefix 0xA55E70... per A11; the
      low 96 bits encode hash160(assetId).
  * ExecutePrecompile(host, msg, &result) is the entry point.
    Returns false when the address is not a precompile (caller
    falls through to normal CALL); otherwise fills `result`.
  * Minimal ABI codec: function selectors via Keccak256(signature)[0:4],
    word reads (uint256/uint64/uint8/bool/address/dynamic bytes),
    word writes, dynamic-payload tails. No general-purpose Solidity
    ABI codec — only the surface the precompiles need.

evm/host.cpp: precompile dispatch is now the first branch of
CEvmHost::call(). Nested CALLs from a deployed contract reach
precompiles via this path. CREATE/CREATE2 are unaffected (they
can't target precompiles).

rpc/ethereum.cpp: eth_call short-circuits precompile addresses too.
The direct (non-nested) call path goes through vm.execute() against
recipient's deployed code, which is empty for precompile addresses;
without this short-circuit eth_call to a precompile would return
empty success. The check runs before vm.execute() so the precompile
fires with the right msg.input_data + msg.gas.

evm/precompile_chainlocks.cpp — Phase 4.3 implementation
  Solidity-callable surface:
    interface IChainLocks {
        function isChainLocked(uint32 height, bytes32 blockHash)
            external view returns (bool);
        function isTxInstantLocked(bytes32 txid)
            external view returns (bool);
        function latestChainLockedHeight()
            external view returns (uint32);
    }
  Selectors pinned by their real keccak256[0:4] values; the framework
  helper AbiFunctionSelector is available for compile-time validation
  during dev.
  Backed by llmq::chainLocksHandler->HasChainLock() +
  llmq::quorumInstantSendManager->IsLocked(); cs_main is acquired
  for the lookup. Fixed gas cost: 5000 per call (calibration is a
  FUP).

evm/precompile_masternodes.cpp / precompile_asset_erc20.cpp /
precompile_llmq_oracle.cpp — stub implementations returning
EVMC_FAILURE. Replaced by their real implementations in subsequent
Phase 4.x commits. Splitting one stub per file (instead of weak
symbols in the framework) avoids GNU ld's archive-resolution
behavior where a weak symbol gets picked once and never overridden
by a stronger symbol later in the same archive.

Live regtest validation via eth_call:
  $ eth_call to=0x...0a03 data=0x2a433be9
      latestChainLockedHeight()                  -> 0x000...000 (32 bytes)
  $ eth_call to=0x...0a03 data=0x51c37046<txid>
      isTxInstantLocked(bytes32)                 -> 0x000...000
  $ eth_call to=0x...0a03 data=0x59d66e82<height><hash>
      isChainLocked(uint32,bytes32)              -> 0x000...000
  $ eth_call to=0x...0a03 data=0xdeadbeef
      unknown-selector                           -> EVMC_FAILURE
  $ eth_call to=0x...0a02 (LLMQ Oracle stub)     -> EVMC_FAILURE

Zero results above are correct on regtest with LLMQ inactive. With
ChainLocks signing online the precompile returns the actual locked
state — the dispatch and ABI plumbing is what's validated end-to-end
here. All 11 EVM test suites still green.

Phase 4 roadmap (in order):
  4.4 — Masternode Registry precompile at 0x...0a04
  4.1 — Smart Assets ERC-20 precompile (per-asset addresses)
  4.2 — LLMQ Oracle precompile (sync verify + async request/get)
…/ LLMQ Oracle precompiles

Closes Phase 4 — the four RTM-native precompiles that surface
Raptoreum-specific state (masternode registry, Smart Asset metadata,
LLMQ threshold signatures, ChainLocks) as standard Solidity-callable
contracts. Built on the Phase 4.0 framework + 4.3 ChainLocks
infrastructure already on the branch.

Phase 4.4 — Masternode Registry (precompile_masternodes.cpp, 0x...0a04)

  interface IMasternodeRegistry {
      function getCount()              external view returns (uint256);
      function isMasternode(bytes32)   external view returns (bool);
      function getByProTxHash(bytes32) external view returns (Masternode);
      function getByIndex(uint256)     external view returns (Masternode);
  }

  Each Masternode struct encodes proTxHash + ipv4 + port + BLS
  operator pubkey (48 bytes) + collateral amount + isBanned flag.
  Backed by deterministicMNManager->GetListAtChainTip() under
  cs_main; ForEachMN iterates the immer::map for getByIndex.

Phase 4.1 — Smart Assets ERC-20 (precompile_asset_erc20.cpp,
                                  0xA55E70...prefix + hash160 tag)

  interface IRtmAsset {
      function name()                   external view returns (string);
      function symbol()                 external view returns (string);
      function decimals()               external view returns (uint8);
      function totalSupply()            external view returns (uint256);
      function balanceOf(address)       external view returns (uint256);
  }

  One precompile address per asset. The high 8 bytes spell A55E70...
  (per A11 collision rule blocking user CREATE); the low 12 bytes
  carry a truncated hash160(assetId). Resolution from address back
  to assetId is a linear scan over passetsCache->mapAsset today —
  fine for testnet / early mainnet. A hash160 -> assetId index in
  CAssets ships with the D4-revised bidirectional mirror.

  MVP scope: metadata reads (name/symbol/decimals/totalSupply) +
  balanceOf returning 0. transfer/transferFrom/approve/allowance
  arrive with the bidirectional mirror, which is the D4-revised
  highest-risk track and has its own acceptance gate (the T-mirror
  test). The read surface is enough for MetaMask "Add Token" and
  read-only dApp displays today.

Phase 4.2 — LLMQ Oracle (precompile_llmq_oracle.cpp, 0x...0a02)

  interface ILlmqOracle {
      function hasSignature(uint8 llmqType, bytes32 id)
          external view returns (bool);
      function getSignature(uint8 llmqType, bytes32 id)
          external view returns (bool, bytes);
      function verifySignature(uint8 llmqType, uint32 signedAtHeight,
                               bytes32 id, bytes32 msgHash,
                               bytes signature)
          external view returns (bool);
  }

  hasSignature + getSignature read from
  llmq::quorumSigningManager (HasRecoveredSigForId /
  GetRecoveredSigForId). verifySignature dispatches to the static
  CSigningManager::VerifyRecoveredSig — pure BLS verification with
  no signing-state mutation. Async requestSignature is intentionally
  not exposed: it writes signing state and isn't safe in a read-only
  EVM call. It lands together with the A7 gas-escrow + timeout
  pattern (FUP) — a state-mutating Phase 5 RPC will trigger it.

  Unknown LLMQType values are rejected explicitly so a dApp can't
  silently probe a non-existent quorum and get false.

ABI selectors for all three precompiles pinned to their
eth_hash-verified keccak256[0:4] values.

Live regtest validation:
  $ eth_call to=0x..0a04 data=0xa87d942c     # MN getCount()
    -> 0x000...000   (no masternodes on regtest)
  $ eth_call to=0x..0a02 data=0x76f0824900..01<id-32>   # LLMQ has(50_60)
    -> 0x000...000   (no recovered sig present)
  $ eth_call to=0x..0a02 data=0x76f0824900..7f<id>   # unknown LLMQ type
    -> EVMC_FAILURE  (correctly rejected)
  $ eth_call to=0xA55E70..AA data=0x06fdde03  # name() on non-asset
    -> EVMC_FAILURE  (correctly rejected, no matching assetId)
  $ ChainLocks precompile (Phase 4.3, already shipped) still green.

11 EVM test suites all green. Phase 4 is functionally complete.

Open follow-ups (FUP register):
  * Hash160 -> assetId index in CAssetsCache (replaces linear scan).
  * Bidirectional Smart Asset balance mirror (D4-revised "T-mirror";
    enables transfer/transferFrom/approve/allowance).
  * Async LLMQ requestSignature with gas escrow + timeout (A7).
  * Gas-cost calibration for the precompiles (current: 5000-8000
    flat per call).
  * IPv6 support in Masternode.ip (currently IPv4 only).
Lifting the Phase-2.4 SPEND miner stopgap (D2 inc 6c, once EVM_COMMIT
is active) made SPEND txs selectable into block templates — but the
miner only added the EIP-1559 tip to the coinbase, never the SPEND's
UTXO credit. ConnectBlock's CheckCoinbaseRealisesSpendCredits REQUIRES
each SPEND's destination output to be present (the SPEND already
debited the EVM account; the destroyed balance must reappear as that
UTXO). So a SPEND-bearing block built by the miner would fail its own
TestBlockValidity and wedge block production. Now that FUND makes EVM
accounts spendable, a user could FUND then SPEND and stall mining —
a real liveness/correctness gap.

Fix: surface the EVM_SPEND UtxoCredits from the shared
ComputeCoinbaseEvmCommitment helper (EvmCoinbaseCommitment.utxoCredits)
and have the miner emit each as a coinbase output. The committed value
is permitted by IsBlockValueValid via the validator-recomputed
nEvmSpendCreditTotal (non-inflationary: the EVM balance was destroyed
1:1). The test harness CreateBlock mirrors this so harness blocks
carrying SPEND txs are valid.

Test: evm_fund_tests +1 (spend_end_to_end_and_reorg_safe) — FUND an
account, SPEND part back to a UTXO, assert the block connects, the
destination output is in the coinbase, the EVM account is debited,
and a reorg restores the debited balance (undo journal). Both
money-directions (FUND UTXO->EVM, SPEND EVM->UTXO) are now proven
mineable + reorg-safe end to end. 37 chain/EVM cases green; regtest
daemon mines; Capa B baseline 20328/10 zero-unexpected.
A single block containing FUND(acct) followed by SPEND(acct) must
apply in order against the shared block cache — the FUND credit must
land before the SPEND reads the balance — and the coinbase must
realise the SPEND credit even though the funding came from an earlier
tx in the SAME block. evm_fund_tests +1 (fund_then_spend_same_block):
asserts the block connects, the SPEND destination output is in the
coinbase, and the account balance reflects funded - spent - gas.
Confirms ProcessEvmTransactionsInBlock sequencing + multi-credit
coinbase assembly. All green; tests-only.
Every EVM transaction passes through CheckEvm{Deploy,Call,Spend,Fund}Tx
via CheckSpecialTx; these reject malformed payloads before execution
and had no direct coverage. evm_evmtx_validation_tests (5 cases, under
TestChain100Setup so the EVM activation gate is satisfied against a
real tip):
  - valid Deploy/Call/Spend/Fund payloads pass;
  - Deploy: empty code, > MAX_EVM_CONTRACT_CODE_SIZE, zero gas,
    inverted EIP-1559 fees, bad version -> rejected;
  - Call: > MAX_EVM_CALLDATA_SIZE, zero gas, inverted fees -> rejected;
  - Spend: zero amount, empty output script, zero gas -> rejected;
  - Fund: zero amount (bad-evm-fund-amount), non-10^10-multiple
    (bad-evm-fund-precision), bad version -> rejected, with the exact
    reject reasons asserted.
Locks the consensus structural-validation boundary against regression.
tests-only, no production change.
…art Asset

Implementa el ledger interno del token envuelto (wrapped) en el propio
storage trie del precompile por-asset (0xA55E70..||hash160), con layout
compatible con Solidity para que herramientas estándar puedan inspeccionarlo:

  mapping(address=>uint256) balanceOf                 en slot 0
  mapping(address=>mapping(address=>uint256)) allowance en slot 1
  uint256 wrappedSupply (totalSupply)                 en slot 2

Superficie de escritura ERC-20 sobre ese ledger, despachada por el
ExecutePrecompile real:

  transfer(address,uint256)
  transferFrom(address,address,uint256)   (descuenta allowance)
  approve(address,uint256)
  allowance(address,address)

balanceOf y totalSupply ahora leen el ledger (antes devolvían 0 fijo).
Las cantidades se acotan a uint64 reutilizando los helpers de balance.h
(Uint256{GreaterOrEqual,Sub,Add}Uint64), igual que los puentes FUND/SPEND.
Las escrituras se rechazan bajo EVMC_STATIC. Se emiten los logs canónicos
Transfer/Approval (topic0 = keccak de la firma del evento) con los dos
addresses indexados y el importe en data.

Los saldos viven en el state cache del EVM: se comprometen en evmStateRoot
y se revierten con el journal de undo de reorg, exactamente como cualquier
storage de contrato. El supply envuelto arranca en 0 hasta que M2
(wrap/unwrap) acuñe unidades cruzando el puente desde el balance UTXO del
asset, así que desplegar esto es inerte y seguro.

Tests: nuevo caso asset_erc20_ledger_writes que siembra un saldo
directamente en el storage del precompile y ejercita toda la superficie
de escritura por el dispatch real — transfer mueve saldos y conserva
totalSupply, approve/allowance/transferFrom con decremento de allowance,
guardia EVMC_STATIC en transfer y approve, y los caminos de fallo por
saldo y por allowance insuficientes (sin mutación de estado). El caso de
lectura previo sigue verde (ledger vacío => 0). Regresión EVM completa y
baseline Capa B sin cambios.
…l layout)

Extrae el layout del ledger ERC-20 del lado EVM a un módulo compartido
evm/asset_ledger.{h,cpp}, fuente única de verdad para CÓMO se representa
un Smart Asset envuelto en el estado EVM. Tanto la superficie de
lectura/escritura del precompile (M1) como el camino de aplicación de
wrap/unwrap (M2) calcularán los slots a través de estos helpers, de modo
que el espejo no puede divergir: el saldo que acredita un wrap es
exactamente el slot que lee balanceOf().

API:
  evmc::address AssetErc20Address(assetId)   // forward assetId -> dirección
  uint256 AssetBalanceSlot / AssetAllowanceSlot / AssetWrappedSupplySlot
  bool CreditAssetLedger(cache, assetId, holder, amount)  // wrap
  bool DebitAssetLedger(cache, assetId, holder, amount)   // unwrap
  uint64 AssetLedgerBalanceOf / AssetLedgerWrappedSupply  // lectura (tests)

AssetErc20Address es la inversa del resolver por barrido lineal del
precompile (0xA55E70||hash160(assetId)[8:20]). Credit/Debit operan
directamente sobre CEvmStateCache::Get/SetStorage con claves uint256
byte-idénticas a las bytes32 que usa el precompile vía el host (la
conversión bytes32<->uint256 es copia cruda), así que ambos caminos
pegan en el mismo slot. Las cantidades se acotan a uint64 reutilizando
balance.h; Credit sube wrappedSupply, Debit exige saldo y wrappedSupply
suficientes y no muta nada si faltan.

precompile_asset_erc20.cpp ahora delega el cálculo de slots a este
módulo (BalanceSlot/AllowanceSlot/WrappedSupplySlot son wrappers finos
bytes32). Los 3 casos de evm_asset_erc20_tests siguen verdes tras la
refactorización, lo que prueba que los slots del módulo compartido son
byte-idénticos a los que fijó M1.
Implementa los dos sentidos del puente que mueve un Smart Asset entre el
lado UTXO y el ledger ERC-20 del lado EVM (M1), con conservación de
suministro y seguridad ante reorgs.

Modelo (sin custodia, sin vault, sin canal de asset en coinbase): el chequeo
de conservación por-tx checkAssetsOutputs ya exime a MINT (puede crear
unidades sin inputs). Se exime también a WRAP/UNWRAP, y cada uno reimpone una
conservación CONSTREÑIDA en su propio handler:
  - WRAP quema N unidades del lado UTXO (inputs de asset superan a outputs en
    N) y acredita N en el ledger EVM del recipient.
  - UNWRAP acuña N unidades del lado UTXO (outputs superan a inputs en N) y
    debita N del ledger EVM del sender.
Invariante: sum(saldos UTXO del asset) + wrappedSupply(EVM) == circulatingSupply.
circulatingSupply NO se toca: las unidades envueltas simplemente cambian de
lado.

Payloads (evmtx.h): CWrapAssetTx{assetId, evmRecipient, amount},
CUnwrapAssetTx{assetId, evmSender, amount}.

Validación estructural (evmtx.cpp): CheckWrapAssetTx/CheckUnwrapAssetTx
verifican gate de EVM activo, versión, amount>0/MoneyRange, assetId existente,
y el binding de delta de asset contra el CCoinsViewCache —
CheckConstrainedAssetDelta exige que SOLO el assetId declarado tenga delta
(vout-vin) == -amount (wrap) o +amount (unwrap), y que cualquier OTRO asset
quede balanceado, impidiendo abusar la exención para crear/destruir assets
arbitrarios.

Aplicación EVM (apply.cpp): ApplyWrapAssetTx (CreditAssetLedger) /
ApplyUnwrapAssetTx (DebitAssetLedger; falla sin mutar si el sender no tiene
saldo o wrappedSupply suficientes). Sin gas, como FUND. ProcessEvmWrap/Unwrap
(process.cpp) las envuelven bajo snapshot; un fallo se reporta como
preflightFailed para que ConnectBlock rechace TODO el bloque — clave en
unwrap: el mint del lado UTXO ya se validó y jamás puede quedar sin su quema
EVM correspondiente. Despacho en ProcessEvmTransactionsInBlock
(connectblock.cpp), de modo que las mutaciones entran en el evmStateRoot v3 y
las captura el journal de undo de reorg; la quema/acuñación del lado UTXO
revierte por el camino normal de restaurar-inputs / quitar-outputs.

Cableado de consenso: despacho en CheckSpecialTx + Process/UndoSpecialTx
(specialtx.cpp), exención de conservación en tx_verify.cpp, forma vin/vout en
tx_check.cpp (WRAP exige vin, permite vout vacío; UNWRAP usa el require-ambos
por defecto), whitelist de tipos en mempool (validation.cpp). checkSpecialTxFee
ya cubría WRAP/UNWRAP (gate EVM, sin fee de protocolo; aplica el fee UTXO
normal).

Tests (evm_asset_wrap_tests): wrap acredita el ledger y el precompile
balanceOf/totalSupply leen EXACTAMENTE ese slot sobre la misma cache (espejo a
nivel unitario); acumulación multi-holder con sum(balanceOf)==wrappedSupply;
unwrap debita y rechaza sobregiro sin mutar estado; round-trip wrap/unwrap
conserva en cada paso. Suite completa verde incluyendo Capa B sin cambios.
…ión)

Añade el gate de aceptación del puente bidireccional: una secuencia
aleatoria (PRNG de semilla fija, reproducible) de 2000 operaciones que
mueven un Smart Asset por los dos ledgers a través de los caminos REALES —
transfer UTXO, WRAP (ApplyWrapAssetTx), transfer EVM (precompile transfer),
UNWRAP (ApplyUnwrapAssetTx), todo sobre una única CEvmStateCache que el
precompile y el camino de apply comparten.

Tras CADA operación se comprueban las dos invariantes que definen un espejo
correcto:
  (1) conservación cruzada: sum(saldos UTXO) + wrappedSupply == circulatingSupply
  (2) invariante ERC-20: sum(balanceOf de todos los holders) == wrappedSupply
y que el balanceOf del precompile coincide con la lectura del módulo de
ledger compartido por holder (mismo ledger en ambos caminos).

El test verifica además que la secuencia ejercita las cuatro operaciones
(guarda contra un cambio futuro de PRNG que silenciosamente salte una rama)
y que el ledger EVM nunca queda negativo. Verde.
…dress

Hace usable el puente de extremo a extremo desde la wallet, al nivel de
FUND/SPEND. Tres RPCs nuevas en el namespace evo:

- wrap_asset(assetid, amount, evmrecipient, [changeaddress], [submit]):
  construye un TRANSACTION_WRAP_ASSET. Reutiliza la selección de moneda de
  asset ya probada vía CreateTransaction(sign=false) con un output de asset
  "fantasma" de `amount` unidades a un script único; tras fondear, re-tipa la
  tx a WRAP, BORRA el fantasma (la quema: inputs de asset superan a outputs
  en `amount`) y fija el payload. Espejo del truco de quema-RTM-fantasma de
  evm_fund. El cambio de asset (sobrante) vuelve al usuario.

- unwrap_asset(assetid, amount, evmsender, rtmrecipient, fundaddress, [submit]):
  construye un TRANSACTION_UNWRAP_ASSET. Fondea SOLO el fee RTM con
  FundSpecialTx (sin output de asset, para que la selección de moneda no
  intente conservar el asset), y luego AÑADE el output de acuñación de
  `amount` unidades a rtmrecipient. La exención de conservación lo permite y
  CheckUnwrapAssetTx lo liga al `amount` exacto.

- get_asset_evm_address(assetid): devuelve la dirección EVM determinista del
  token ERC-20 envuelto (0xA55E70||hash160(assetId)[8:20]) para añadirlo en
  MetaMask / un contrato Solidity sin calcular el hash a mano.

Conversiones de argumentos en client.cpp (amount numérico, submit booleano).

Validado e2e en una cadena regtest viva (raptoreumd + raptoreum-cli):
create+mint asset GOLD (1000) -> wrap_asset 10 a 0xaaaa -> minado ->
eth_call balanceOf(0xaaaa)=0x3b9aca00 (10*1e8) -> unwrap_asset 5 a una
dirección RTM -> minado -> balanceOf=0x1dcd6500 (5*1e8), totalSupply=0x1dcd6500,
y el destinatario recibe 5 GOLD (amount=500000000) en el lado UTXO.
Suministro conservado (1000 = UTXO 995 + EVM 5). El ledger EVM persiste tras
reiniciar el demonio. get_asset_evm_address devuelve exactamente la dirección
del precompile.

Nota (FUP): el ledger EVM guarda nAmount escalado por COIN (1e8); decimals()
del precompile devuelve asset.decimalPoint. Si decimalPoint != 8 hay un
desajuste de visualización en clientes ERC-20 (el espejo es exacto en
unidades base); a documentar/resolver por separado.
El ledger ERC-20 del lado EVM refleja el nAmount del asset en su unidad de
consenso nativa, que es COIN-escalada (1e8) para TODO asset
independientemente de su decimalPoint (decimalPoint solo restringe qué
cantidades fraccionarias son válidas, no la escala). Por tanto el precompile
debe reportar decimals()=8, no asset.decimalPoint: así balanceOf/10**decimals
muestra el valor en unidades enteras correcto en cualquier cliente ERC-20.
Reportar decimalPoint mal-escalaba la visualización para cualquier asset con
decimalPoint != 8 (el espejo seguía exacto en unidades base; solo fallaba el
punto decimal de cara al usuario).

Test: asset_erc20_read_surface ahora siembra el asset con decimalPoint=4 y
exige decimals()==8, fijando el comportamiento.
El e2e en regtest viva prueba el happy-path on-chain; esto fija en CI la
guarda CRÍTICA de seguridad: la conservación de asset CONSTREÑIDA que
reimponen wrap/unwrap tras quedar exentos de checkAssetsOutputs (la exención
compartida con MINT es lo que permite quemar/acuñar; la restricción es lo que
impide abusarla).

Test in-process (TestChain100Setup, EVM activo a altura 0) que construye txs a
mano + un CCoinsViewCache con UTXOs de asset y ejercita CheckWrapAssetTx /
CheckUnwrapAssetTx directamente:
- wrap_delta_binding: válido pasa (amount == delta quemado); amount erróneo,
  amount cero y assetId desconocido rechazados.
- wrap_rejects_other_asset_perturbation: un wrap que quemaría silenciosamente
  un asset DISTINTO (input sin output correspondiente) se rechaza
  (bad-evm-wrap-delta) — la exención no puede crear/destruir assets arbitrarios.
- unwrap_delta_binding: mint válido pasa (amount == delta acuñado); amount
  erróneo y cero rechazados.

Una regresión aquí dejaría que un wrap/unwrap creara o destruyera unidades de
asset arbitrarias (ruptura de integridad de suministro); ahora está bloqueado
en CI. Cobertura D4 completa en CI: check estructural + apply + convergencia.
El job evm-consensus-gate (Linux, gate duro) solo corría Capa B + reorg fuzz;
las suites del D4 mirror y de los puentes de valor solo se ejecutaban en el
full-suite de Windows. Como wrap/unwrap es crítico para el consenso, se añaden
al --run_test del gate para que una regresión bloquee la build, no solo el run
de Windows:

  evm_wrap_consensus_tests        (binding de delta constreñido wrap/unwrap)
  evm_asset_wrap_tests            (apply: crédito/débito del ledger + espejo)
  evm_asset_mirror_convergence_tests (invariantes T-mirror, 2000 ops)
  evm_asset_erc20_tests           (superficie del ledger ERC-20)
  evm_evmtx_validation_tests      (validación estructural de tx EVM)
  evm_fund_tests, evm_spend_credit_tests (puentes UTXO<->EVM)
  evm_d2_consensus_tests, cbtx_evm_commit_tests (commitment D2 / CCbTx v3)

Suites rápidas (~segundos); todas verdes con la lista exacta del gate.
- SMART-ASSET-MIRROR.md (nuevo): design-of-record del puente bidireccional —
  los dos ledgers, la invariante de conservación, el modelo de quema/acuñación
  vía la exención de conservación, atomicidad/reorg, mapa de código, superficie
  de tests, y el procedimiento e2e en regtest (probado en vivo).
- STATUS.md: nueva sección Post-Phase-5 (hard fork D2/CCbTx v3, puentes de
  valor FUND/SPEND, D4 mirror) + nota de Capa B ~99.95% Cancun.
- RPC.md: sección de puentes evo — evm_fund, wrap_asset, unwrap_asset,
  get_asset_evm_address con ejemplos.
- PRECOMPILES.md: interfaz ERC-20 completa del Smart Asset (transfer/
  transferFrom/approve/allowance + eventos + selectores), semántica del ledger
  EVM, y decimals()=8 (escala COIN).
- README.md: índice con SMART-ASSET-MIRROR.md + OFFICIAL-TESTS.md; sección
  "deferred" actualizada (D2 y D4 ya shipped); snapshot de estado al día.
…ste ciclo

Añade la prioridad ✅ "shipped" y marca como entregados los ítems que han
landeado: FUP-2.1/2.2/2.3 (commitment D2 vía CCbTx v3, dinámica de baseFee
EIP-1559, verificación de realización en coinbase), FUP-3.1 (pre-fondeo de
receipts vía evm_fund), FUP-4.2 (mirror bidireccional D4 — el grande),
FUP-X.4 (T1 Capa B en CI), FUP-X.6 (T3 reorg fuzz), FUP-X.7 (T-mirror).
Nuevos ítems D4: FUP-4.8 (ajuste fino del fee de unwrap_asset), FUP-4.9
(mirror de NFTs/unique assets). FUP-X.5 (T2 divergencia de estado) queda
desbloqueado ahora que el commitment D2 está vivo.
…a binaria)

Corrige FUP-3.6. La implementación anterior corría una sola ejecución al
límite suministrado (30M por defecto) y devolvía solo el gas de EJECUCIÓN
(gasUsed de evmone), SIN el gas intrínseco (21000 + coste de calldata
EIP-2028) que la capa de proceso cobra antes de ejecutar. Un cliente que
dimensionara el gasLimit de una tx real con esa estimación lo infrafondeaba
en ~21000+ y chocaba con out-of-gas. Tampoco contemplaba la regla 63/64 de
reenvío de gas en llamadas anidadas.

Ahora estima el gasLimit TOTAL de la tx:
- IntrinsicCallGas(data) = 21000 + 16/byte no-cero + 4/byte cero (EIP-2028).
- Búsqueda binaria del menor límite total al que la llamada sigue teniendo
  éxito; el oráculo cobra el intrínseco y pasa (total - intrínseco) a evmone,
  de modo que la regla 63/64 cae correctamente (un frame que consume G a
  límite alto puede necesitar un límite > G para reenviar suficiente a sus
  llamadas internas). ~25 sondeos para el cap de 30M.
- Si la llamada no tiene éxito ni al cap, error JSON-RPC limpio.

Test (eth_rpc_conformance_tests): estimateGas a una EOA sin código devuelve
exactamente el intrínseco — 21000 (sin data), 21016 (1 byte no-cero), 21004
(1 byte cero), 21036 (mixto).
…sPrice/tip)

Con el commitment D2 (CCbTx v3) ya vivo, varios RPC eth_* seguían
reportando ceros obsoletos, rompiendo el dimensionado de fees de las wallets
(MetaMask construía txs con fee 0 que el mempool/consenso rechaza):

- eth_gasPrice: devolvía 0x0. Ahora = baseFee del próximo bloque (recurrencia
  EIP-1559 sobre el (baseFee,gasUsed) commiteado en el tip) + tip sugerido.
- eth_maxPriorityFeePerGas: devolvía 0x0. Ahora = 1 gwei (tip no-cero).
- objeto de bloque (FormatBlock): faltaba baseFeePerGas (clientes post-Londres
  lo exigen) y stateRoot/receiptsRoot/gasUsed eran cero pese a estar
  commiteados. Ahora se leen del CCbTx v3 del coinbase del propio bloque
  (cero pre-fork / en redes sin EVM_COMMIT registrado).

Helpers nuevos (namespace anónimo): ReadEvmCommitment(block) extrae el CCbTx
v3 del coinbase; NextBlockEvmBaseFeeWei() aplica ComputeNextBaseFee al tip.

Tests (eth_rpc_conformance_tests): maxPriorityFeePerGas == 1 gwei, gasPrice
>= tip y bien formado; el objeto de bloque incluye baseFeePerGas/gasUsed
(QUANTITY) y stateRoot/receiptsRoot (DATA).

Hallazgos de la auditoría diferidos a FUP (riesgo/alcance): byte-order de
hashes de tx entre bloque y receipt, campos value/input/gas/nonce en
eth_getTransactionByHash, y difficulty/totalDifficulty (nBits vs real).
Marca FUP-3.6 como shipped (estimateGas con intrínseco + búsqueda binaria) y
sube FUP-3.4 a ⚠️ (campos value/input/gas/nonce + type correcto en
getTransactionByHash/receipt). Nuevos: FUP-3.7 (consistencia de byte-order de
hashes de tx entre bloque y receipt — impide el cruce en dApps), FUP-3.8
(difficulty/totalDifficulty exponen nBits compacto en vez de la dificultad
real / nChainWork), FUP-3.9 (value acotado a uint64 weis ~18.44 RTM por tx,
por diseño de CEvmCallTx.value).
…yHash

Corrige FUP-3.7 + FUP-3.4. Antes coexistían dos convenciones de hash de tx
que no coincidían para la misma tx EVM:
- el bloque (eth_getBlockBy*) emitía el sha256d INVERTIDO (Uint256ToEthHex),
- el receipt y getTransactionByHash emitían FORWARD (ToEthData), y para las
  tx enviadas por eth_sendRawTransaction usaban el keccak ethTxHash que el
  bloque NUNCA exponía.
Resultado: un dApp que iterase los tx de un bloque no podía cruzarlos con sus
receipts. Bug de compatibilidad con wallets/exploradores.

Helper EthFacingTxHash(tx): una tx EVM se identifica por su identidad
Ethereum — el keccak ethTxHash del cross-index si existe, si no el rtmTxHash
forward (exactamente el fallback que ya usaban receipt/getTxByHash). Las tx
no-EVM conservan su txid RTM (display invertido). Usado por
FormatTransactionForBlock y por la lista de hashes de FormatBlock, de modo
que bloque ↔ receipt ↔ getTransactionByHash coinciden.

eth_getTransactionByHash (FUP-3.4): ahora carga la wrapper tx desde su bloque
(receipt.blockHash + txIndex -> ReadBlockFromDisk) y devuelve el objeto
COMPLETO vía FormatTransactionForBlock (value/input/gas/nonce/maxFeePerGas/
to=null en creación), con la misma convención de hash. Si el bloque no se
puede leer, degrada al objeto parcial del receipt (comportamiento previo).

Test (eth_funded_e2e_tests, round-trip real): tras FUND + deploy firmado
EIP-155 + minado, el MISMO ethTxHash aparece en el listado fullTx del bloque,
en la lista de hashes, en el receipt y en getTransactionByHash; este último
expone input/gas/nonce y to=null. El flujo wallet->receipt por eth-hash sigue
verde (sin regresión). baseFeePerGas presente en el bloque.
El objeto de bloque exponía `nBits` (el target compacto) tanto en
`difficulty` como en `totalDifficulty`, valores sin sentido para
exploradores que ordenan por dificultad. Ahora:
- difficulty = GetDifficulty(pindex) (la dificultad PoW real).
- totalDifficulty = nChainWork acumulado del índice, como QUANTITY
  (vía ArithToUint256 + BalanceAsEthQuantity).

Test: el caso block_object_has_eip1559_and_evm_fields verifica además que
difficulty/totalDifficulty son QUANTITY bien formados.

Actualiza el registro FUP: 3.4/3.7 (identidad de hash + campos ricos) y 3.8
marcados como shipped.
… (FUP-4.1)

El precompile ERC-20 de Smart Asset resuelve la dirección EVM por-asset a su
assetId en CADA llamada (eth_call y ejecución EVM en ConnectBlock — ruta de
consenso) mediante un barrido lineal O(N) que computa Hash160 por cada asset.
Coste ~O(N) Hash160 por llamada; no escala.

Se sustituye por CAssetsCache::ResolveAssetIdByTag con un índice hint
AUTO-CORRECTOR tag(12B)->assetId: O(log N) en caliente. La clave de seguridad
de consenso: cada acierto del hint se RE-VERIFICA contra mapAsset (la fuente
de verdad) y, ante cualquier discrepancia, cae al barrido lineal autoritativo.
Por construcción, el resultado es SIEMPRE función pura de mapAsset, nunca del
contenido (posiblemente obsoleto) del hint — un índice obsoleto/ausente jamás
puede devolver un resultado erróneo, solo más lento. Por eso es seguro
cruzando la frontera de consenso (nodos con hints distintos resuelven igual).

El hint vive en CAssets (mAssetTagHint), se limpia en SetNull, y NO se copia
en el copy-ctor/operator= (una copia arranca con hint vacío y se auto-pobla;
las copias por-bloque del cache nunca resuelven —el precompile resuelve contra
el passetsCache global— así que no pagan nada). No se engancha ningún punto de
mutación de mapAsset: la auto-corrección lo hace innecesario y elimina el
riesgo catastrófico de "olvidar un punto de mutación -> índice obsoleto ->
fork".

Tests (evm_asset_resolver_tests): el método coincide con un barrido lineal
independiente para todo tag bajo 4000 mutaciones aleatorias insert/erase de
mapAsset; y un hint caliente se auto-corrige en cuanto su asset desaparece de
mapAsset (el caso exacto "índice obsoleto devuelve asset muerto" que
divergiría el consenso). Suite completa verde incluida Capa B; las suites de
asset/precompile siguen verdes (cambio que preserva el comportamiento).
Marca FUP-4.1 como shipped en el registro y añade evm_asset_resolver_tests al
--run_test del job evm-consensus-gate (testea un resolver de la ruta de
consenso; su auto-corrección debe quedar bajo gate duro).
…C (FUP-5.4)

evm_signTransaction/evm_sendTransaction solo firmaban EIP-1559 (type 0x2);
herramientas y hardware wallets antiguos emiten tx legacy. eth_sendRawTransaction
ya las DECODIFICA, pero faltaba el primitivo de FIRMA.

Nuevo evm::SignLegacyTx(privKey, LegacyTxFields): payload de firma EIP-155
rlp([nonce,gasPrice,gasLimit,to,value,data,chainId,0,0]) (sin byte de envelope),
v = chainId*2+35+y_parity, cuerpo firmado rlp([nonce,gasPrice,gasLimit,to,value,
data,v,r,s]). Exige chainId>0 (protección de replay EIP-155 obligatoria).

RPC: helper compartido SignFromCallObject despacha legacy vs 1559 según el
campo "type" del callObject ("0x0" -> legacy; ausente/"0x2" -> EIP-1559),
usado por evm_signTransaction y evm_sendTransaction. ParseEvmLegacyFields
decodifica el objeto (gasPrice en vez de maxFeePerGas).

Test (eth_sendrawtx_roundtrip_tests): el primitivo de producción
evm::SignLegacyTx produce un blob que el decodificador de producción
DecodeRawEthTx recupera al address firmante exacto + todos los campos; creación
de contrato (emptyTo) round-trip; protección de replay (no decodifica bajo otro
chainId); chainId 0 rechazado. Misma garantía que ya tenía SignEip1559Tx, ahora
para legacy.
unwrap_asset fondea el fee RTM con FundSpecialTx ANTES de añadir el output de
acuñación del asset (no puede añadirlo antes: CreateTransaction intentaría
conservar el asset y seleccionar inputs que no existen). El resultado era un
fee ligeramente bajo (no contaba los ~40-50 bytes del output añadido después).

FundSpecialTx gana un parámetro opcional extraFeeBytes (default 0, retro-
compatible) que se suma al tamaño usado en el cálculo del fee. unwrap_asset
construye el output del asset primero, mide su tamaño serializado y lo pasa,
de modo que la tx final paga el fee correcto. Se aplica a ambas definiciones
de FundSpecialTx (la local de rpcevo.cpp —la que usa unwrap— y la de
specialtx_utilities.h, por consistencia).

Validado e2e en regtest: create+mint asset, wrap 100, unwrap 40 con el fee
reservado -> minado correctamente (la tx entra en bloque). Marca FUP-4.8 como
shipped.
RPC.md: actualizadas las entradas de eth_gasPrice (baseFee+tip),
eth_estimateGas (intrínseco + búsqueda binaria), eth_maxPriorityFeePerGas
(1 gwei), eth_getTransactionByHash (objeto completo + identidad de hash
unificada) y evm_signTransaction (switch "type":"0x0" legacy), con sus
descripciones de la tabla de contenidos.

STATUS.md: nueva subsección "Post-mirror hardening" con la tabla de
correcciones de esta sesión (estimateGas, superficie EIP-1559, identidad de
hash, campos ricos, firma legacy, resolver de asset, fee de unwrap).

FUP.md: FUP-4.8 marcado como shipped.
@JSanchezFDZ

Copy link
Copy Markdown
Author

Progress update — EVM is now usable on-chain end-to-end, plus the D4 Smart-Asset mirror

Since the draft snapshot the branch has moved from "Phases 0–5 scaffolding" to a fully on-chain-usable native EVM, and the flagship differentiator — the D4 Smart-Asset bidirectional mirror — is implemented, tested, and proven e2e on a live regtest daemon. This is a heads-up for reviewers on what changed and, importantly, where I deliberately stopped and why those parts want the audit, not more solo commits.

Docs are the source of truth and have been kept current: docs/evm/STATUS.md (phase/commit map), docs/evm/SMART-ASSET-MIRROR.md (the D4 design-of-record), docs/evm/FUP.md (the follow-up register, with ✅-shipped markers), docs/evm/RPC.md, docs/evm/PRECOMPILES.md.

1. D2 — EVM commitment hard fork (RIP-vote-gated)

The five EVM consensus roots are committed in the coinbase special tx (CCbTx v3)evmStateRoot, evmReceiptsRoot, evmBaseFee, evmGasUsed, evmExecTime — gated by EUpdate::EVM_COMMIT (force-active at height 0 on regtest; unregistered on testnet/mainnet — those activate via the standard RIP miner+smartnode vote, configured when scheduled). Miner output == validator recompute via a single shared helper (ComputeCoinbaseEvmCommitment), so the miner can never build a template the validator would reject. EIP-1559 base-fee dynamics + burn/tip are live. Regression-locked in evm_d2_consensus_tests + cbtx_evm_commit_tests.

2. Value bridges both ways (UTXO ↔ EVM)

  • TRANSACTION_EVM_FUND (UTXO → EVM) + evm_fund RPC — supply-conserving (the funded amount leaves the miner-claimable fee and reappears as EVM balance).
  • TRANSACTION_EVM_SPEND (EVM → UTXO) — credits realised as coinbase outputs, verified by CheckCoinbaseRealisesSpendCredits; the miner now realises them (a real bug fix — a SPEND-bearing block would otherwise wedge mining).
  • Funded e2e proven in eth_funded_e2e_tests: FUND → EIP-155-signed eth_sendRawTransaction → mine → eth_getTransactionReceipt status 0x1, resolved by the Ethereum tx hash.

3. D4 — Smart-Asset bidirectional mirror (the flagship)

Every Smart Asset is callable as a standard ERC-20 at its per-asset precompile address, and units move both ways across the UTXO ↔ EVM boundary, supply-conserving and reorg-safe. Full design in SMART-ASSET-MIRROR.md. Key design choices reviewers should sanity-check:

  • No vault, no custody, no coinbase-asset channel. wrap/unwrap reuse the per-tx asset-conservation exemption (the same one MINT uses) and each re-imposes a constrained conservation in its own CheckSpecialTx handler: for the declared assetId the (vout−vin) delta must be exactly ∓amount, and every other asset must be balanced — so the exemption can't be abused to mint/burn arbitrary assets. This is the security-critical guard.
  • EVM-side ledger in the precompile's own storage trie (Solidity-compatible layout), so it's committed in evmStateRoot and reverted by the EVM reorg undo journal like any contract storage.
  • Invariant: sum(UTXO asset balances) + EVM wrappedSupply == circulatingSupply (untouched).
  • unwrap's EVM-side debit is the authorization for the UTXO mint: a shortfall rejects the whole block, so a minted output can never stand without its matching EVM burn.

Milestones M1–M6: the EVM ERC-20 ledger, the shared asset_ledger layout module, the wrap/unwrap consensus bridge, the T-mirror convergence gate (2000-op randomized sequence asserting both invariants after every op), wallet RPCs (wrap_asset/unwrap_asset/get_asset_evm_address), and an in-process consensus test of the constrained-delta guard (incl. a "silently burn another asset" attack). Proven e2e on a live regtest daemon: create+mint → wrap → eth_call balanceOf → unwrap → balances/supply verified; EVM ledger persists across daemon restart.

4. eth_* / asset correctness pass (each with a regression test)

A wallet-compatibility + correctness sweep — see the ✅-shipped rows in FUP.md:

  • eth_estimateGas now returns a complete tx gas limit (intrinsic 21000 + EIP-2028 calldata + binary-searched minimum execution gas honouring the 63/64 rule). It previously returned execution gas only, under-funding real txs.
  • EIP-1559 RPC surface un-staled now the base fee is live: eth_gasPrice = next-block base fee + tip; eth_maxPriorityFeePerGas = 1 gwei; the block object now carries baseFeePerGas/gasUsed/stateRoot/receiptsRoot from the CCbTx v3 commitment, and real difficulty/totalDifficulty.
  • Tx-hash identity unified across block listing / receipt / eth_getTransactionByHash (so dApps cross-reference block ↔ receipt); eth_getTransactionByHash now returns the full tx shape.
  • Legacy EIP-155 signing primitive + a "type":"0x0" path in evm_signTransaction/evm_sendTransaction.
  • Asset resolver O(N)→O(log N): CAssetsCache::ResolveAssetIdByTag with a self-correcting verified hint index — every hit re-verified against mapAsset with a scan fallback, so the result is a pure function of mapAsset and a stale hint can never diverge consensus (property + stale-hint tests).

5. CI

The Linux evm-consensus-gate job is a hard gate running Capa B (official Ethereum tests, ~99.95% of applicable Cancun, pass-floor + classified non-fixable allow-list — see OFFICIAL-TESTS.md) + reorg fuzz + the D4 mirror + value-bridge + D2-commitment suites. Full local suite is green (572 cases).

6. Deliberately NOT done solo — these want the audit, not autonomous commits

I drew a hard line at any change that could diverge consensus without a safety-by-construction property or external review. Tracked in FUP.md:

  • EIP-2930 access lists in eth_sendRawTransaction (gas accounting must be byte-identical across nodes — fork risk if wrong).
  • EVM validation in TestBlockValidity/VerifyDB (FUP-2.7): for miner-built blocks it's a tautology and doubles EVM exec per template; the VerifyDB half needs historical EVM state that isn't snapshotted. ConnectTip already validates, so the gap is defense-in-depth, not a hole.
  • Precompile encoding changes (IPv6 in the MN struct, exact ChainLocks water-line) — they change returned bytes / determinism.
  • True parallel EVM execution, releasing cs_main during execution, gas-cost calibration — large/data-dependent.

The genuinely next step for these — and for testnet activation — is public testnet + a paid security audit of the consensus-level EVM integration before mainnet. Happy to prep any of the deferred items as separate, clearly-marked "pending-audit-review" branches if the team wants to look at a specific one.

Review can proceed phase-by-phase against STATUS.md; the per-phase implementation notes live in the commit bodies. Questions still owed by the core team are in QUESTIONS.md.

…sta de activación testnet

Prepara la rama para la revisión de seguridad del core team (que, por
decisión de proyecto de 2026-06, es interna — no se contrata auditoría
externa). Tres documentos nuevos y el registro de preguntas al día:

- REVIEW-GUIDE.md (nuevo): cómo revisar la rama sin ahogarse en los commits.
  La propiedad nº1 (mergear NO cambia nada en mainnet/testnet: todo gateado
  en EVM/EVM_COMMIT, solo registrados en regtest), orden de lectura por
  subsistema, y los 8 hot-spots de consenso rankeados con prompts de ataque
  concretos (gating, exención de conservación wrap/unwrap, conservación de
  suministro en los puentes, paridad minero==validador del commitment D2,
  journal de reorg, ingest de raw-tx, resolver auto-corrector, semántica EVM
  vía Capa B). Incluye cómo reproducir cada verificación (suites, gate de CI,
  e2e en regtest) y checklist de revisión.

- TESTNET-ACTIVATION.md (nuevo): propuesta concreta para Q-A5 — altura
  forzada en testnet (tip + ~21.600 bloques ≈ 30 días) activando EVM y
  EVM_COMMIT a la vez, bits 3/4, parche draft de chainparams (NO aplicado;
  requiere ratificación), plan de rollout con checklist de humo
  post-activación y palanca de aborto; mainnet por voto RIP tras ventana de
  90 días, como esbozo.

- QUESTIONS.md: cada pregunta lleva ahora una "Proposed answer" lista para
  ratificar. Q-A1 RESUELTA (aclaración out-of-band: errata por "EVM"; el
  requisito real —dual consensus, sin firmas nuevas, sin cambios de
  serialización invasivos— lo cumple el diseño del wrapper AAL). Q-A2 y Q-A6
  marcadas answered-by-implementation (v1 estricto; commitment en CCbTx v3
  en vez de campos de header, con la justificación). Nuevo FUP-4.10
  propuesto: flag por-asset de opt-out del EVM (garantía de consenso de que
  un asset regulado jamás adquiere exposición a riesgo Solidity — requisito
  directo del core team).

- STATUS/README/FUP: reflejan el modelo de revisión interna, enlazan los
  documentos nuevos y añaden FUP-4.10.
@JSanchezFDZ

Copy link
Copy Markdown
Author

Review entry point + proposed decisions

To make this branch actually reviewable, the docs now include:

  • docs/evm/REVIEW-GUIDE.md — start here. Subsystem-ordered reading path (not commit-by-commit), the 8 consensus hot-spots ranked with concrete "try to break this" prompts, and how to reproduce every verification claim locally (unit gates, Capa B, the live regtest e2e). The single most important property is spelled out and verifiable first: merging this changes nothing on mainnet or testnet — every consensus-visible behavior is gated on EVM/EVM_COMMIT, which are registered only for regtest.
  • docs/evm/QUESTIONS.md — every open question now carries a proposed answer written so a single 👍 ratifies it. Q-A1 is marked resolved per the out-of-band clarification (the requirement — stay inside the existing dual-consensus and signature model, no invasive serialization changes — is satisfied by the special-tx wrapper design; details in the entry). Q-A2/Q-A6 are answered-by-implementation and only need ratification.
  • docs/evm/TESTNET-ACTIVATION.md — concrete activation proposal (forced height on testnet ≈ tip+30d, bits 3/4, draft chainparams patch NOT applied, rollout + post-activation smoke checklist, abort lever). Nothing activates anywhere without sign-off on this document.

One item worth highlighting because it came from your requirements: assets are non-EVM by default (nothing touches the EVM unless explicitly wrapped), and FUP-4.10 proposes a per-asset opt-out flag so an issuer can forbid wrapping at consensus level — a hard guarantee that a given asset can never acquire Solidity-risk exposure. Small change inside the already-tested wrap surface; awaiting your nod before implementing.

Happy to do the call whenever suits — the review guide doubles as the agenda.

…o, reorg (PR review)

Responde al feedback de revisión de AmyPan sobre la superficie que más rompe
a wallets/indexers antes de testnet:

- blockHash (EIP-234): restringe a un bloque; MUTUAMENTE EXCLUSIVO con
  fromBlock/toBlock (pasar ambos = error limpio, no campo ignorado). El
  bloque debe estar en la cadena canónica; un hash reorganizado-fuera da
  error explícito (señal de rollback para el indexer), nunca logs obsoletos.
- Orden (blockNumber, transactionIndex, logIndex). CORREGIDO un bug de spec
  que el comentario destapó: logIndex es la posición del log DENTRO DEL
  BLOQUE (acumulativa entre las txs del bloque), no por-receipt. Dos txs con
  logs en un bloque ahora dan logIndex 0x0/0x1/0x2 (un contador por-receipt
  habría dado 0x0/0x1/0x0 y roto todo indexer).
- Error explícito de límite de rango ANTES de escanear (un rango numérico
  solicitado > 10000 bloques falla rápido, nunca deja que la llamada agote
  timeout). toBlock numérico más allá del tip hace clamp al tip (estilo geth).
- Consistencia tras reorg: cross-check de receipt.blockHash contra el bloque
  escaneado, para que un receipt re-asignado a otro bloque tras un reorg
  jamás se emita con coordenadas del bloque equivocado.

Tests: eth_funded_e2e_tests::getlogs_filters_ordering_and_reorg ejercita los
DOS filtros exactos de la revisión sobre una cadena minada real (dos deploys
emisores de logs en un bloque, orden por nonce), verifica el orden y el
logIndex acumulado por bloque, invalida el bloque y comprueba que los logs
desaparecen de queries de rango + dan error por hash, luego reconecta y
comprueba que vuelven los mismos tres logs. eth_rpc_conformance_tests añade
el contrato estructural (exclusión blockHash/rango, hash desconocido = error,
{"blockHash":..,"topics":[]}, error de rango pre-scan, clamp de toBlock).
Docs en RPC.md.
@JSanchezFDZ

Copy link
Copy Markdown
Author

@BlockReq-AmyPan — agreed, eth_getLogs is exactly the surface indexers trip over. All five points are implemented and covered by a regression test that runs against a real mined chain (eth_funded_e2e_tests::getlogs_filters_ordering_and_reorg), plus a structural set in eth_rpc_conformance_tests.

Your two filter shapes are asserted directly:

  • {"fromBlock","toBlock","address":["0x..."],"topics":[null,["0x..","0x.."]]} — address array + [null, or-set] positional topics; the test emits a LOG2(0x..aa, 0x..bb) and asserts only the log whose position-1 topic is in the or-set comes back.
  • {"blockHash":"0x...","topics":[]} — empty topics = no constraint; returns the whole block's logs.

Point by point:

  • reject blockHash when fromBlock/toBlock is present — done (EIP-234); passing both is a clean JSON-RPC error, not a silently-ignored field. Unknown or reorged-out hashes also error rather than returning [].
  • order by (blockNumber, transactionIndex, logIndex) — iteration is height-ascending → vtx-index-ascending → log-position-ascending. I also fixed a latent bug your wording caught: logIndex is now the log's position within the block (cumulative across its txs), not per-receipt. Two log-emitting txs in one block now report logIndex 0x0, 0x1, 0x2 (a per-receipt counter would have emitted 0x0, 0x1, 0x0 and broken every indexer). The test pins this.
  • DisconnectTip + reconnect consistency — the test invalidates the log-bearing block, asserts its logs vanish from range queries and error by hash, then reconnects and asserts the exact same three logs return. There's also a guard so a receipt re-keyed to a different block after a reorg can never be emitted under the wrong block's coordinates (receipt.blockHash is cross-checked against the block being scanned).
  • explicit range-limit error instead of timeout — an oversized requested numeric range fails fast with an explicit error before any chain access; the 10k-block cap is enforced. A numeric toBlock past the tip clamps to the tip (geth-style) instead of erroring.

Docs updated in docs/evm/RPC.md. Thanks — this is precisely the kind of edge-case feedback that's most useful before testnet; if there are other RPCs you'd want a regression set for (I'd guess eth_getTransactionReceipt log bloom / eth_getBlockByNumber fullTx), name them and I'll add them the same way.

evm_fund/wrap_asset/unwrap_asset assembled their help text with
HELP_REQUIRING_PASSPHRASE, which is declared in wallet/rpcwallet.h and
only included under ENABLE_WALLET. The RPCHelpMan block is compiled
unconditionally, so a --disable-wallet build failed with the identifier
undeclared. Inline a wallet-independent copy of the literal; the
wallet-gated function bodies already throw RPC_METHOD_NOT_FOUND when
built without wallet support, so wallet-less mining/relay nodes now
compile and expose these methods as unavailable.

Also add docs/evm/BANK-GRADE-FINDINGS.md: an independently verified
pre-activation review of the EVM consensus paths (sender authorization,
chain-id consistency, state-undo symmetry) with reproducible file:line
evidence and fix designs. All findings are inert on mainnet/testnet
(EVM gated to regtest) and are tracked as blockers before any shared
network activation.
@JSanchezFDZ

Copy link
Copy Markdown
Author

Pre-activation bank-grade review of the EVM consensus paths

I did a focused, independent review of the EVM consensus paths before any
activation is scheduled, reading each path end to end rather than relying on
tests. Full write-up with reproducible file:line evidence and fix designs is
in docs/evm/BANK-GRADE-FINDINGS.md (added in 5e0894c). Summary below.

Framing / safety caveat first: every EVM path is gated behind
IsEvmActive and the EVM deployment is unregistered on mainnet and
testnet
(force-active at height 0 on regtest only). So none of these is a
live-network issue today
— on any shared network the txs are rejected at
CheckEvmCommon with evm-not-activated. They are pre-merge/pre-activation
blockers, not live incidents. Because no EVM tx has ever been committed on a
shared network, fixing the serialization-level items now has zero
backward-compat cost.

Findings

  • EVM-01 — CRITICAL — consensus trusts an unauthenticated senderHash.
    The Ethereum signature is verified only at eth_sendRawTransaction ingest and
    then discarded; the serialized payload (CEvmDeployTx/CEvmCallTx/
    CEvmSpendTx in src/evm/evmtx.h) carries senderHash/fromAddress + nonce
    but no r/s/v. The consensus Check* functions (src/evm/evmtx.cpp)
    validate structure only, and PreFlightCallOrDeploy
    (src/evm/process.cpp:130) debits the payload-supplied account after checking
    only nonce + balance. Net: once the gate is open on a network with any
    non-cooperating node, a hand-built EVM_SPEND with fromAddress = victim
    drains any EVM account into a UTXO the attacker controls — no signature
    required. Fix design in the doc (carry the standard signed envelope and
    re-verify sender == senderHash in Check using the existing
    DecodeRawEthTx; the wallet-authored bridge txs — SPEND/FUND/WRAP/UNWRAP —
    need their own authorization envelope, which is the part I'd like the core
    team to weigh in on before I implement, since it touches consensus
    serialization).

  • EVM-02 — HIGH — chain-id split. Ingest/signing uses ActiveEvmChainId()
    (regtest 7375, testnet 7374, mainnet 7373) but the execution context hardcodes
    7373 (src/evm/connectblock.cpp:263, src/validation.cpp:2597). So on
    regtest/testnet a tx is signed for one id and the CHAINID opcode reports
    another. Also a prerequisite for the EVM-01 fix (recovery must use the signing
    id). Fix: parameterize via Consensus::Params (the existing FUP-1).

  • EVM-05 — HIGH — EVM state not undone on VerifyDB / RollbackBlock. The
    EVM undo (evm::ApplyUndoToDB) runs only in DisconnectTip
    (src/validation.cpp:3316); VerifyDB (:5344) and RollbackBlock
    (:5475) disconnect via DisconnectBlock directly, which reverts UTXO/asset
    state but not EVM state. A verifychain-level-3+ over a range containing EVM
    txs re-applies them on un-rolled-back state → state-root divergence. Fix: move
    the EVM undo into a helper both disconnect paths call.

  • EVM-06 — MEDIUM (by design) — value is uint64_t. Values ≥ 2⁶⁴ wei are
    unrepresentable. Flagged so the team can decide on widening to uint256
    before mainnet; not a safety bug.

  • DET-01 — open — determinism of the consensus-state-reading precompiles
    (masternodes/chainlocks/llmq oracle) was not independently confirmed in this
    pass and is called out honestly rather than assumed safe.

Shipped in this commit (5e0894c)

  • FUP-X.2--disable-wallet build fix: evm_fund/wrap_asset/
    unwrap_asset referenced HELP_REQUIRING_PASSPHRASE (wallet-only) in their
    unconditionally-compiled help blocks. Inlined a wallet-independent literal so
    wallet-less mining/relay nodes compile (the bodies already throw
    RPC_METHOD_NOT_FOUND without wallet support).

I deliberately did not land the EVM-01/EVM-02 consensus changes here — they
change EVM payload serialization / execution context and should be ratified as a
reviewed batch rather than pushed unilaterally. Designs are ready to implement on
green-light.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant