Skip to content

Commit

Permalink
Experimental RPC endpoints for generating block witnesses (#1977)
Browse files Browse the repository at this point in the history
* Completed draft implementation of witness JSON-RPC endpoints for portal network bridge.

* Updated Nimbus RPC configuration to support enabling experimental endpoints.

* Moved witness verification tests.

* Added json test for getProof.

* Added main procs to new tests to fix test suite.

* Added getBlockWitness test to blockchain json test suite.

* Added tests for experimental RPC endpoints and improved the API to support returning state proofs from before or after block execution.

* Correctly rollback transaction in getBlockWitness proc.
  • Loading branch information
web3-developer committed Jan 22, 2024
1 parent b098484 commit 48630cc
Show file tree
Hide file tree
Showing 20 changed files with 825 additions and 191 deletions.
8 changes: 0 additions & 8 deletions fluffy/network/state/experimental/state_proof_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ type
AccountProof* = distinct MptProof
StorageProof* = distinct MptProof

BlockWitness* = seq[byte]

AccountData* = object
account*: Account
code* : seq[byte]
storage*: Table[UInt256, UInt256]


proc getBranch*(
self: AccountState;
key: openArray[byte]): seq[seq[byte]] {.borrow.}
Expand Down
53 changes: 2 additions & 51 deletions fluffy/network/state/experimental/state_proof_verification.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
{.push raises: [].}

import
std/[sequtils, tables],
std/sequtils,
stint,
eth/[common, rlp, trie/hexary_proof_verification],
stew/results,
./state_proof_types,
../../../../stateless/[tree_from_witness, witness_types],
../../../../nimbus/db/[core_db, state_db]
./state_proof_types

export results

Expand Down Expand Up @@ -67,50 +65,3 @@ func verifyContractBytecode*(
ok()
else:
err("hash of bytecode doesn't match the expected code hash")

proc buildAccountsTableFromKeys(
db: ReadOnlyStateDB,
keys: openArray[AccountAndSlots]): TableRef[EthAddress, AccountData] {.raises: [RlpError].} =

var accounts = newTable[EthAddress, AccountData]()

for key in keys:
let account = db.getAccount(key.address)
let code = if key.codeLen > 0:
db.getTrie().parent().kvt().get(account.codeHash.data)
else: @[]
var storage = initTable[UInt256, UInt256]()

if code.len() > 0:
for slot in key.slots:
let slotKey = fromBytesBE(UInt256, slot)
let (slotValue, slotExists) = db.getStorage(key.address, slotKey)
if slotExists:
storage[slotKey] = slotValue

accounts[key.address] = AccountData(
account: account,
code: code,
storage: storage)

return accounts

proc verifyWitness*(
trustedStateRoot: KeccakHash,
witness: BlockWitness): Result[TableRef[EthAddress, AccountData], string] =
if witness.len() == 0:
return err("witness is empty")

let db = newCoreDbRef(LegacyDbMemory)
var tb = initTreeBuilder(witness, db, {wfEIP170}) # what flags to use here?

try:
let stateRoot = tb.buildTree()
if stateRoot != trustedStateRoot:
return err("witness stateRoot doesn't match trustedStateRoot")

let ac = newAccountStateDB(db, trustedStateRoot, false)
let accounts = buildAccountsTableFromKeys(ReadOnlyStateDB(ac), tb.keys)
ok(accounts)
except Exception as e:
err(e.msg)
72 changes: 1 addition & 71 deletions fluffy/tests/test_state_proof_verification.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import
unittest2,
stew/results,
eth/[common, rlp, trie, trie/trie_defs],
../../nimbus/db/[ledger, core_db],
../../nimbus/common/chain_config,
../../stateless/[witness_from_tree, multi_keys, witness_types],
../network/state/experimental/[state_proof_types, state_proof_generation, state_proof_verification],
./test_helpers

Expand Down Expand Up @@ -142,52 +140,6 @@ proc checkInvalidProofsWithBadValue(
proofResult.isErr()
proofResult.error() == "proof does not contain expected value"

proc setupStateDB(
genAccounts: GenesisAlloc,
stateDB: LedgerRef): (Hash256, MultikeysRef) =

var keys = newSeqOfCap[AccountKey](genAccounts.len)

for address, genAccount in genAccounts:
var storageKeys = newSeqOfCap[StorageSlot](genAccount.storage.len)

for slotKey, slotValue in genAccount.storage:
storageKeys.add(slotKey.toBytesBE)
stateDB.setStorage(address, slotKey, slotValue)

stateDB.setNonce(address, genAccount.nonce)
stateDB.setCode(address, genAccount.code)
stateDB.setBalance(address, genAccount.balance)

let sKeys = if storageKeys.len != 0: newMultiKeys(storageKeys) else: MultikeysRef(nil)
let codeTouched = genAccount.code.len > 0
keys.add(AccountKey(address: address, codeTouched: codeTouched, storageKeys: sKeys))

stateDB.persist()
(stateDB.rootHash, newMultiKeys(keys))

proc buildWitness(
genAccounts: GenesisAlloc): (KeccakHash, BlockWitness) {.raises: [CatchableError].} =

let
coreDb = newCoreDbRef(LegacyDbMemory)
accountsCache = AccountsCache.init(coreDb, emptyRlpHash, true)
(rootHash, multiKeys) = setupStateDB(genAccounts, accountsCache)

var wb = initWitnessBuilder(coreDb, rootHash, {wfEIP170})
(rootHash, wb.buildWitness(multiKeys))

proc checkWitnessDataMatchesAccounts(
genAccounts: GenesisAlloc,
witnessData: TableRef[EthAddress, AccountData]) {.raises: [CatchableError].} =

for address, genAccount in genAccounts:
let accountData = witnessData[address]
check genAccount.code == accountData.code
check genAccount.storage == accountData.storage
check genAccount.balance == accountData.account.balance
check genAccount.nonce == accountData.account.nonce

suite "State Proof Verification Tests":

let genesisFiles = ["berlin2000.json", "chainid1.json", "chainid7.json", "merge.json"]
Expand All @@ -214,26 +166,4 @@ suite "State Proof Verification Tests":
for file in genesisFiles:
let accounts = getGenesisAlloc("fluffy" / "tests" / "custom_genesis" / file)
var state = accounts.toState()
checkInvalidProofsWithBadValue(accounts, state[0], state[1])

test "Block witness verification with valid state root":
for file in genesisFiles:
let
accounts = getGenesisAlloc("fluffy" / "tests" / "custom_genesis" / file)
(stateRoot, witness) = buildWitness(accounts)
verifyResult = verifyWitness(stateRoot, witness)

check verifyResult.isOk()
checkWitnessDataMatchesAccounts(accounts, verifyResult.get())

test "Block witness verification with invalid state root":
let badStateRoot = toDigest("2cb1b80b285d09e0570fdbbb808e1d14e4ac53e36dcd95dbc268deec2915b3e7")

for file in genesisFiles:
let
accounts = getGenesisAlloc("fluffy" / "tests" / "custom_genesis" / file)
(stateRoot, witness) = buildWitness(accounts)
verifyResult = verifyWitness(badStateRoot, witness)

check verifyResult.isErr()
check verifyResult.error() == "witness stateRoot doesn't match trustedStateRoot"
checkInvalidProofsWithBadValue(accounts, state[0], state[1])
2 changes: 2 additions & 0 deletions nimbus/config.nim
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ type
## RPC flags
Eth ## enable eth_ set of RPC API
Debug ## enable debug_ set of RPC API
Exp ## enable exp_ set of RPC API

DiscoveryType* {.pure.} = enum
None
Expand Down Expand Up @@ -689,6 +690,7 @@ proc getRpcFlags(api: openArray[string]): set[RpcFlag] =
case item.toLowerAscii()
of "eth": result.incl RpcFlag.Eth
of "debug": result.incl RpcFlag.Debug
of "exp": result.incl RpcFlag.Exp
else:
error "Unknown RPC API: ", name=item
quit QuitFailure
Expand Down
2 changes: 1 addition & 1 deletion nimbus/core/executor/process_block.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Nimbus
# Copyright (c) 2018-2023 Status Research & Development GmbH
# Copyright (c) 2018-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http:https://www.apache.org/licenses/LICENSE-2.0)
Expand Down
15 changes: 10 additions & 5 deletions nimbus/evm/state.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

import
std/[options, sets, strformat],
eth/[keys],
../../stateless/[witness_from_tree, witness_types],
eth/keys,
../../stateless/[witness_from_tree, witness_types, multi_keys],
../db/ledger,
../common/[common, evmforks],
./async/data_sources,
Expand Down Expand Up @@ -290,10 +290,10 @@ proc `generateWitness=`*(vmState: BaseVMState, status: bool) =
if status: vmState.flags.incl GenerateWitness
else: vmState.flags.excl GenerateWitness

proc buildWitness*(vmState: BaseVMState): seq[byte]
{.raises: [CatchableError].} =
proc buildWitness*(
vmState: BaseVMState,
mkeys: MultikeysRef): seq[byte] {.raises: [CatchableError].} =
let rootHash = vmState.stateDB.rootHash
let mkeys = vmState.stateDB.makeMultiKeys()
let flags = if vmState.fork >= FkSpurious: {wfEIP170} else: {}

# A valid block having no transactions should return an empty witness
Expand All @@ -304,6 +304,11 @@ proc buildWitness*(vmState: BaseVMState): seq[byte]
var wb = initWitnessBuilder(vmState.com.db, rootHash, flags)
wb.buildWitness(mkeys)

proc buildWitness*(
vmState: BaseVMState): seq[byte] {.raises: [CatchableError].} =
let mkeys = vmState.stateDB.makeMultiKeys()
buildWitness(vmState, mkeys)

func forkDeterminationInfoForVMState*(vmState: BaseVMState): ForkDeterminationInfo =
# FIXME-Adam: Is this timestamp right? Note that up above in blockNumber we add 1;
# should timestamp be adding 12 or something?
Expand Down
4 changes: 4 additions & 0 deletions nimbus/nimbus.nim
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
setupEthRpc(nimbus.ethNode, nimbus.ctx, com, nimbus.txPool, nimbus.rpcServer)
if RpcFlag.Debug in rpcFlags:
setupDebugRpc(com, nimbus.rpcServer)
if RpcFlag.Exp in rpcFlags:
setupExpRpc(com, nimbus.rpcServer)

nimbus.rpcServer.rpc("admin_quit") do() -> string:
{.gcsafe.}:
Expand Down Expand Up @@ -322,6 +324,8 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
setupEthRpc(nimbus.ethNode, nimbus.ctx, com, nimbus.txPool, nimbus.wsRpcServer)
if RpcFlag.Debug in wsFlags:
setupDebugRpc(com, nimbus.wsRpcServer)
if RpcFlag.Exp in wsFlags:
setupExpRpc(com, nimbus.rpcServer)

nimbus.wsRpcServer.start()

Expand Down
6 changes: 4 additions & 2 deletions nimbus/rpc.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import
./rpc/p2p,
./rpc/jwt_auth,
./rpc/cors,
./rpc/rpc_server
./rpc/rpc_server,
./rpc/experimental

export
common,
Expand All @@ -23,4 +24,5 @@ export
p2p,
jwt_auth,
cors,
rpc_server
rpc_server,
experimental
129 changes: 129 additions & 0 deletions nimbus/rpc/experimental.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Nimbus
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

{.push raises: [].}

import
std/[typetraits],
json_rpc/rpcserver, stint, web3/conversions,
eth/p2p,
../[transaction, vm_state, constants, vm_types],
../db/state_db,
rpc_types, rpc_utils,
../core/tx_pool,
../common/common,
../utils/utils,
../beacon/web3_eth_conv,
./filters,
../core/executor/process_block,
../db/ledger,
../../stateless/[witness_verification, witness_types],
./p2p

type
BlockHeader = eth_types.BlockHeader
ReadOnlyStateDB = state_db.ReadOnlyStateDB

proc getBlockWitness*(
com: CommonRef,
blockHeader: BlockHeader,
statePostExecution: bool): (KeccakHash, BlockWitness, WitnessFlags)
{.raises: [CatchableError].} =

let
chainDB = com.db
blockHash = chainDB.getBlockHash(blockHeader.blockNumber)
blockBody = chainDB.getBlockBody(blockHash)
vmState = BaseVMState.new(blockHeader, com)
flags = if vmState.fork >= FKSpurious: {wfEIP170} else: {}
vmState.generateWitness = true # Enable saving witness data

var dbTx = vmState.com.db.beginTransaction()
defer: dbTx.dispose()

# Execute the block of transactions and collect the keys of the touched account state
let processBlockResult = processBlock(vmState, blockHeader, blockBody)
doAssert processBlockResult == ValidationResult.OK

let mkeys = vmState.stateDB.makeMultiKeys()

if statePostExecution:
result = (vmState.stateDB.rootHash, vmState.buildWitness(mkeys), flags)
else:
# Reset state to what it was before executing the block of transactions
let initialState = BaseVMState.new(blockHeader, com)
result = (initialState.stateDB.rootHash, initialState.buildWitness(mkeys), flags)

dbTx.rollback()


proc getBlockProofs*(
accDB: ReadOnlyStateDB,
witnessRoot: KeccakHash,
witness: BlockWitness,
flags: WitnessFlags): seq[ProofResponse] {.raises: [RlpError].} =

if witness.len() == 0:
return @[]

let verifyWitnessResult = verifyWitness(witnessRoot, witness, flags)
doAssert verifyWitnessResult.isOk()

var blockProofs = newSeqOfCap[ProofResponse](verifyWitnessResult.value().len())

for address, account in verifyWitnessResult.value():
var slots = newSeqOfCap[UInt256](account.storage.len())

for slotKey, _ in account.storage:
slots.add(slotKey)

blockProofs.add(getProof(accDB, address, slots))

return blockProofs

proc setupExpRpc*(com: CommonRef, server: RpcServer) =

let chainDB = com.db

proc getStateDB(header: BlockHeader): ReadOnlyStateDB =
## Retrieves the account db from canonical head
# we don't use accounst_cache here because it's only read operations
let ac = newAccountStateDB(chainDB, header.stateRoot, com.pruneTrie)
result = ReadOnlyStateDB(ac)

server.rpc("exp_getWitnessByBlockNumber") do(quantityTag: BlockTag, statePostExecution: bool) -> seq[byte]:
## Returns the block witness for a block by block number or tag.
##
## quantityTag: integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter.
## statePostExecution: bool which indicates whether to return the witness based on the state before or after executing the block.
## Returns seq[byte]

let
blockHeader = chainDB.headerFromTag(quantityTag)
(_, witness, _) = getBlockWitness(com, blockHeader, statePostExecution)

return witness

server.rpc("exp_getProofsByBlockNumber") do(quantityTag: BlockTag, statePostExecution: bool) -> seq[ProofResponse]:
## Returns the block proofs for a block by block number or tag.
##
## quantityTag: integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter.
## statePostExecution: bool which indicates whether to return the proofs based on the state before or after executing the block.
## Returns seq[ProofResponse]

let
blockHeader = chainDB.headerFromTag(quantityTag)
(witnessRoot, witness, flags) = getBlockWitness(com, blockHeader, statePostExecution)

let accDB = if statePostExecution:
getStateDB(blockHeader)
else:
getStateDB(chainDB.getBlockHeader(blockHeader.parentHash))

return getBlockProofs(accDB, witnessRoot, witness, flags)
Loading

0 comments on commit 48630cc

Please sign in to comment.