nimbus-eth1/fluffy/network/state/state_endpoints.nim

415 lines
14 KiB
Nim

# Fluffy
# Copyright (c) 2021-2024 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [].}
import
results,
chronos,
chronicles,
eth/rlp,
eth/common/[hashes, accounts, addresses],
./state_network,
./state_utils
from eth/common/eth_types_rlp import rlpHash
export results, state_network, hashes, addresses
logScope:
topics = "portal_state"
proc getNextNodeHash(
trieNode: TrieNode, nibbles: UnpackedNibbles, nibbleIdx: var int
): Opt[(Nibbles, Hash32)] =
# the trie node should have already been validated against the lookup hash
# so we expect that no rlp errors should be possible
try:
doAssert(nibbles.len() > 0)
doAssert(nibbleIdx < nibbles.len())
let trieNodeRlp = rlpFromBytes(trieNode.asSeq())
doAssert(not trieNodeRlp.isEmpty())
doAssert(trieNodeRlp.listLen() == 2 or trieNodeRlp.listLen() == 17)
if trieNodeRlp.listLen() == 17:
let nextNibble = nibbles[nibbleIdx]
doAssert(nextNibble < 16)
let nextHashBytes = trieNodeRlp.listElem(nextNibble.int).toBytes()
if nextHashBytes.len() == 0:
return Opt.none((Nibbles, Hash32))
nibbleIdx += 1
return Opt.some(
(nibbles[0 ..< nibbleIdx].packNibbles(), Hash32.fromBytes(nextHashBytes))
)
# leaf or extension node
let
(_, isLeaf, prefix) = decodePrefix(trieNodeRlp.listElem(0))
unpackedPrefix = prefix.unpackNibbles()
if unpackedPrefix != nibbles[nibbleIdx ..< nibbleIdx + unpackedPrefix.len()]:
# The nibbles don't match so we stop the search and don't increment
# the nibble index to indicate how many nibbles were consumed
return Opt.none((Nibbles, Hash32))
nibbleIdx += prefix.unpackNibbles().len()
if isLeaf:
return Opt.none((Nibbles, Hash32))
# extension node
let nextHashBytes = trieNodeRlp.listElem(1).toBytes()
if nextHashBytes.len() == 0:
return Opt.none((Nibbles, Hash32))
Opt.some((nibbles[0 ..< nibbleIdx].packNibbles(), Hash32.fromBytes(nextHashBytes)))
except RlpError as e:
raiseAssert(e.msg)
proc getAccountProof(
n: StateNetwork,
stateRoot: Hash32,
address: Address,
maybeBlockHash: Opt[Hash32], # required for poke
): Future[Result[(TrieProof, bool), string]] {.async: (raises: [CancelledError]).} =
let nibbles = address.toPath().unpackNibbles()
var
nibblesIdx = 0
key = AccountTrieNodeKey.init(Nibbles.empty(), stateRoot)
proof = TrieProof.empty()
# Construct the parent offer which is used to provide the data needed to
# implement poke after the account trie node has been retrieved
maybeParentOffer =
if maybeBlockHash.isSome():
Opt.some(AccountTrieNodeOffer.init(proof, maybeBlockHash.get()))
else:
Opt.none(AccountTrieNodeOffer)
while nibblesIdx < nibbles.len():
let
accountTrieNode = (await n.getAccountTrieNode(key, maybeParentOffer)).valueOr:
return err("Failed to get account trie node when building account proof")
trieNode = accountTrieNode.node
added = proof.add(trieNode)
doAssert(added)
if maybeParentOffer.isSome():
let added = maybeParentOffer.get().proof.add(trieNode)
doAssert(added)
let (nextPath, nextNodeHash) = trieNode.getNextNodeHash(nibbles, nibblesIdx).valueOr:
break
key = AccountTrieNodeKey.init(nextPath, nextNodeHash)
doAssert(nibblesIdx <= nibbles.len())
ok((proof, nibblesIdx == nibbles.len()))
proc getStorageProof(
n: StateNetwork,
storageRoot: Hash32,
address: Address,
slotKey: UInt256,
maybeBlockHash: Opt[Hash32],
maybeAccProof: Opt[TrieProof],
): Future[Result[(TrieProof, bool), string]] {.async: (raises: [CancelledError]).} =
let nibbles = slotKey.toPath().unpackNibbles()
var
addressHash = keccak256(address.data)
nibblesIdx = 0
key = ContractTrieNodeKey.init(addressHash, Nibbles.empty(), storageRoot)
proof = TrieProof.empty()
# Construct the parent offer which is used to provide the data needed to
# implement poke after the contract trie node has been retrieved
maybeParentOffer =
if maybeBlockHash.isSome():
doAssert maybeAccProof.isSome()
Opt.some(
ContractTrieNodeOffer.init(proof, maybeAccProof.get(), maybeBlockHash.get())
)
else:
Opt.none(ContractTrieNodeOffer)
while nibblesIdx < nibbles.len():
let
contractTrieNode = (await n.getContractTrieNode(key, maybeParentOffer)).valueOr:
return err("Failed to get contract trie node when building storage proof")
trieNode = contractTrieNode.node
added = proof.add(trieNode)
doAssert(added)
if maybeParentOffer.isSome():
let added = maybeParentOffer.get().storageProof.add(trieNode)
doAssert(added)
let (nextPath, nextNodeHash) = trieNode.getNextNodeHash(nibbles, nibblesIdx).valueOr:
break
key = ContractTrieNodeKey.init(addressHash, nextPath, nextNodeHash)
doAssert(nibblesIdx <= nibbles.len())
ok((proof, nibblesIdx == nibbles.len()))
proc getAccount(
n: StateNetwork, stateRoot: Hash32, address: Address, maybeBlockHash: Opt[Hash32]
): Future[Opt[Account]] {.async: (raises: [CancelledError]).} =
let (accountProof, exists) = (
await n.getAccountProof(stateRoot, address, maybeBlockHash)
).valueOr:
warn "Failed to get account proof", error = error
return Opt.none(Account)
if not exists:
info "Account doesn't exist, returning default account"
# return an empty account if the account doesn't exist
return Opt.some(EMPTY_ACCOUNT)
let account = accountProof.toAccount().valueOr:
error "Failed to get account from accountProof"
return Opt.none(Account)
Opt.some(account)
proc getBalanceByStateRoot*(
n: StateNetwork,
stateRoot: Hash32,
address: Address,
maybeBlockHash = Opt.none(Hash32),
): Future[Opt[UInt256]] {.async: (raises: [CancelledError]).} =
let account = (await n.getAccount(stateRoot, address, maybeBlockHash)).valueOr:
return Opt.none(UInt256)
Opt.some(account.balance)
proc getTransactionCountByStateRoot*(
n: StateNetwork,
stateRoot: Hash32,
address: Address,
maybeBlockHash = Opt.none(Hash32),
): Future[Opt[AccountNonce]] {.async: (raises: [CancelledError]).} =
let account = (await n.getAccount(stateRoot, address, maybeBlockHash)).valueOr:
return Opt.none(AccountNonce)
Opt.some(account.nonce)
proc getStorageAtByStateRoot*(
n: StateNetwork,
stateRoot: Hash32,
address: Address,
slotKey: UInt256,
maybeBlockHash = Opt.none(Hash32),
): Future[Opt[UInt256]] {.async: (raises: [CancelledError]).} =
let (accountProof, exists) = (
await n.getAccountProof(stateRoot, address, maybeBlockHash)
).valueOr:
warn "Failed to get account proof", error = error
return Opt.none(UInt256)
let account =
if exists:
accountProof.toAccount().valueOr:
error "Failed to get account from accountProof"
return Opt.none(UInt256)
else:
info "Account doesn't exist, returning default account"
# return an empty account if the account doesn't exist
EMPTY_ACCOUNT
if account.storageRoot == EMPTY_ROOT_HASH:
info "Storage doesn't exist, returning default storage value"
# return zero if the storage doesn't exist
return Opt.some(0.u256)
let (storageProof, slotExists) = (
await n.getStorageProof(
account.storageRoot, address, slotKey, maybeBlockHash, Opt.some(accountProof)
)
).valueOr:
warn "Failed to get storage proof", error = error
return Opt.none(UInt256)
if not slotExists:
info "Slot doesn't exist, returning default storage value"
# return zero if the slot doesn't exist
return Opt.some(0.u256)
let slotValue = storageProof.toSlot().valueOr:
error "Failed to get slot from storageProof"
return Opt.none(UInt256)
Opt.some(slotValue)
proc getCodeByStateRoot*(
n: StateNetwork,
stateRoot: Hash32,
address: Address,
maybeBlockHash = Opt.none(Hash32),
): Future[Opt[Bytecode]] {.async: (raises: [CancelledError]).} =
let (accountProof, exists) = (
await n.getAccountProof(stateRoot, address, maybeBlockHash)
).valueOr:
warn "Failed to get account proof", error = error
return Opt.none(Bytecode)
let account =
if exists:
accountProof.toAccount().valueOr:
error "Failed to get account from accountProof"
return Opt.none(Bytecode)
else:
info "Account doesn't exist, returning default account"
# return an empty account if the account doesn't exist
EMPTY_ACCOUNT
if account.codeHash == EMPTY_CODE_HASH:
info "Code doesn't exist, returning default code value"
# return empty bytecode if the code doesn't exist
return Opt.some(Bytecode.empty())
let
contractCodeKey = ContractCodeKey.init(keccak256(address.data), account.codeHash)
maybeParentOffer =
if maybeBlockHash.isSome():
Opt.some(
ContractCodeOffer.init(Bytecode.empty(), accountProof, maybeBlockHash.get())
)
else:
Opt.none(ContractCodeOffer)
contractCodeRetrieval = (await n.getContractCode(contractCodeKey, maybeParentOffer)).valueOr:
warn "Failed to get contract code"
return Opt.none(Bytecode)
Opt.some(contractCodeRetrieval.code)
type Proofs* = ref object
account*: Account
accountProof*: TrieProof
slots*: seq[(UInt256, UInt256)]
slotProofs*: seq[TrieProof]
proc getProofsByStateRoot*(
n: StateNetwork,
stateRoot: Hash32,
address: Address,
slotKeys: seq[UInt256],
maybeBlockHash = Opt.none(Hash32),
): Future[Opt[Proofs]] {.async: (raises: [CancelledError]).} =
let
(accountProof, accountExists) = (
await n.getAccountProof(stateRoot, address, maybeBlockHash)
).valueOr:
warn "Failed to get account proof", error = error
return Opt.none(Proofs)
account =
if accountExists:
accountProof.toAccount().valueOr:
error "Failed to get account from accountProof"
return Opt.none(Proofs)
else:
EMPTY_ACCOUNT
storageExists = account.storageRoot != EMPTY_ROOT_HASH
var
slots = newSeqOfCap[(UInt256, UInt256)](slotKeys.len)
slotProofs = newSeqOfCap[TrieProof](slotKeys.len)
for slotKey in slotKeys:
if not storageExists:
slots.add((slotKey, 0.u256))
slotProofs.add(TrieProof.empty())
continue
let
(storageProof, slotExists) = (
await n.getStorageProof(
account.storageRoot, address, slotKey, maybeBlockHash, Opt.some(accountProof)
)
).valueOr:
warn "Failed to get storage proof", error = error
return Opt.none(Proofs)
slotValue =
if slotExists:
storageProof.toSlot().valueOr:
error "Failed to get slot from storageProof"
return Opt.none(Proofs)
else:
0.u256
slots.add((slotKey, slotValue))
slotProofs.add(storageProof)
return Opt.some(
Proofs(
account: account, accountProof: accountProof, slots: slots, slotProofs: slotProofs
)
)
# Used by: eth_getBalance,
proc getBalance*(
n: StateNetwork, blockNumOrHash: uint64 | Hash32, address: Address
): Future[Opt[UInt256]] {.async: (raises: [CancelledError]).} =
let header = (await n.getBlockHeaderByBlockNumOrHash(blockNumOrHash)).valueOr:
warn "Failed to get block header by block number or hash", blockNumOrHash
return Opt.none(UInt256)
await n.getBalanceByStateRoot(header.stateRoot, address, Opt.some(header.rlpHash()))
# Used by: eth_getTransactionCount
proc getTransactionCount*(
n: StateNetwork, blockNumOrHash: uint64 | Hash32, address: Address
): Future[Opt[AccountNonce]] {.async: (raises: [CancelledError]).} =
let header = (await n.getBlockHeaderByBlockNumOrHash(blockNumOrHash)).valueOr:
warn "Failed to get block header by block number or hash", blockNumOrHash
return Opt.none(AccountNonce)
await n.getTransactionCountByStateRoot(
header.stateRoot, address, Opt.some(header.rlpHash())
)
# Used by: eth_getStorageAt
proc getStorageAt*(
n: StateNetwork, blockNumOrHash: uint64 | Hash32, address: Address, slotKey: UInt256
): Future[Opt[UInt256]] {.async: (raises: [CancelledError]).} =
let header = (await n.getBlockHeaderByBlockNumOrHash(blockNumOrHash)).valueOr:
warn "Failed to get block header by block number or hash", blockNumOrHash
return Opt.none(UInt256)
await n.getStorageAtByStateRoot(
header.stateRoot, address, slotKey, Opt.some(header.rlpHash())
)
# Used by: eth_getCode
proc getCode*(
n: StateNetwork, blockNumOrHash: uint64 | Hash32, address: Address
): Future[Opt[Bytecode]] {.async: (raises: [CancelledError]).} =
let header = (await n.getBlockHeaderByBlockNumOrHash(blockNumOrHash)).valueOr:
warn "Failed to get block header by block number or hash", blockNumOrHash
return Opt.none(Bytecode)
await n.getCodeByStateRoot(header.stateRoot, address, Opt.some(header.rlpHash()))
# Used by: eth_getProof
proc getProofs*(
n: StateNetwork,
blockNumOrHash: uint64 | Hash32,
address: Address,
slotKeys: seq[UInt256],
): Future[Opt[Proofs]] {.async: (raises: [CancelledError]).} =
let header = (await n.getBlockHeaderByBlockNumOrHash(blockNumOrHash)).valueOr:
warn "Failed to get block header by block number or hash", blockNumOrHash
return Opt.none(Proofs)
await n.getProofsByStateRoot(
header.stateRoot, address, slotKeys, Opt.some(header.rlpHash())
)