EVM integration — 32 commits across Phases 0–5 (draft for review)#443
EVM integration — 32 commits across Phases 0–5 (draft for review)#443JSanchezFDZ wants to merge 156 commits into
Conversation
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.
Progress update — EVM is now usable on-chain end-to-end, plus the D4 Smart-Asset mirrorSince 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: 1. D2 — EVM commitment hard fork (RIP-vote-gated)The five EVM consensus roots are committed in the coinbase special tx ( 2. Value bridges both ways (UTXO ↔ EVM)
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
Milestones M1–M6: the EVM ERC-20 ledger, the shared 4. eth_* / asset correctness pass (each with a regression test)A wallet-compatibility + correctness sweep — see the ✅-shipped rows in
5. CIThe Linux 6. Deliberately NOT done solo — these want the audit, not autonomous commitsI drew a hard line at any change that could diverge consensus without a safety-by-construction property or external review. Tracked in
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 |
…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.
Review entry point + proposed decisionsTo make this branch actually reviewable, the docs now include:
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.
|
@BlockReq-AmyPan — agreed, Your two filter shapes are asserted directly:
Point by point:
Docs updated in |
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.
Pre-activation bank-grade review of the EVM consensus pathsI did a focused, independent review of the EVM consensus paths before any Framing / safety caveat first: every EVM path is gated behind Findings
Shipped in this commit (5e0894c)
I deliberately did not land the EVM-01/EVM-02 consensus changes here — they |
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-RPCnamespace (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-accounton 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
audits are expensive).
(
Q-A1..Q-A10indocs/evm/QUESTIONS.md) — these unblockPhase 5.4 (wallet HD), Phase 6 (testnet), Phase 7 (mainnet).
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 surfaceRPC.md— all 25 RPC methods with examplesPRECOMPILES.md— Solidity interfaces, selectors, addresses, gas costsFUP.md— follow-up register (mainnet-blocking vs. before-testnet vs. post-launch)QUESTIONS.md— Q-A1..Q-A10 still open with the core teamBUILD.md— build / run / test guide + full file inventoryPLAN.md+PROPOSAL-FOR-CORE-TEAM.md— historical reference (the original plan and the formal acceptance with D1–D7 decisions)What ships in this PR
evm_executeReadOnlysmoke RPC, smoke test, depends/ wiringCEvmStateDB/CEvmStateCache+ evmc::Host with nested CALL/CREATE + ApplyEvm{Call,Deploy,Spend}Tx + EIP-1559 ProcessEvm*Tx + ConnectBlock/DisconnectTip wiring + reorg journal + parallel pre-flighteth_*/net_*/web3_*methods,eth_sendRawTransactionwith EIP-1559+legacy decode + secp256k1 sender recovery, receipt persistence with eth↔rtm cross-index,eth_getLogswith filter, default 8545 listener0x…0a03), Masternode Registry (0x…0a04), Smart Asset ERC-20 (0xA55E70…hash160), LLMQ Oracle (0x…0a02)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.cpp—allowEmptyTxInOutextended toTRANSACTION_EVM_DEPLOY/_CALL/_SPEND(matches theexisting pattern for
TRANSACTION_QUORUM_COMMITMENT).src/validation.cpp—bad-txns-typeallow-list extended; relay-fee + mempool-min-fee skipped for EVM tx types (fee lives on the
EVM side); ConnectBlock dispatches EVM-typed txs through
ProcessEvmTransactionsInBlockbehindUpdates().IsEvmActive();ConnectTip generates + persists receipts; DisconnectTip applies
src/chainparams.cpp—Update(EUpdate::EVM, …, heightActivated=0)registered for REGTEST. Mainnet/testnetactivation 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.1andQ-A6.What's deferred (and why)
stateRoot/receiptsRoot/transactionsRoot/chainLocksCommitheader fields (D2). Coordinated hard-fork;needs
Q-A6resolution. While deferred, EIP-1559baseFeestays at 0 and coinbase-tip-output verification is unenforced
(
FUP-2.1→FUP-2.3). All mainnet-blocking.Q-A4(m/44'/60'Ethereum-standard vs
m/44'/10226'/0'/1/iRTM-subtree). Signingprimitives are ready; this is just the "select-key-from-wallet"
wiring.
"T-mirror" gate). Highest-risk integration piece still ahead.
The ERC-20 precompile's
transfer/approvesurface staysunimplemented until the mirror lands. Read surface
(
name/symbol/decimals/totalSupply) is live.requestSignatureon the LLMQ Oracle. Needs the gas-escrow + timeout pattern (
Q-A8). SyncverifySignatureislive.
Full register in
docs/evm/FUP.md.Open questions for the core team
10 questions in
docs/evm/QUESTIONS.md. The four that unblock immediatenext 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-A1clarification,Q-A2/3asset-fee semantics,
Q-A9MN IPv6,Q-A10bug-bounty multisig)or design-tier (
Q-A8async LLMQ).How to validate
docs/evm/STATUS.mdfor the high-level shape.docs/evm/BUILD.md(Dockerinstructions included; full build is ~10 minutes from clean).
EVMis force-active at height 0); pokeat the RPC namespace per
docs/evm/RPC.mdexamples.evm_signTransactionoutput against Pythoneth-accountfor the canonical0x4646…4646key — bytesshould match exactly.
Test plan
bls, bip32, transaction)
eth-accounton canonicaltest vectors
(T1)
regtest nodes (T2) — requires
FUP-2.1header fields first(T3)
FUP-4.2landsChainSecurity, 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: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.