Skip to content

Commit

Permalink
Implementation of procs to support Fluffy state JSON-RPC endpoints. (#…
Browse files Browse the repository at this point in the history
…2318)

* Started implementation of state endpoints.

* Add rpc calls and server stubs.

* Initial implementation of getAccountProof and getStorageProof.

* Refactor validation to use toAccount utils functions.

* Add state endpoints tests.
  • Loading branch information
web3-developer committed Jun 10, 2024
1 parent f6be4bd commit c72d6aa
Show file tree
Hide file tree
Showing 11 changed files with 584 additions and 13 deletions.
12 changes: 9 additions & 3 deletions fluffy/network/history/history_network.nim
Original file line number Diff line number Diff line change
Expand Up @@ -610,17 +610,23 @@ proc getEpochAccumulator(

return Opt.none(EpochAccumulator)

proc getBlock*(
proc getBlockHashByNumber*(
n: HistoryNetwork, bn: UInt256
): Future[Result[Opt[Block], string]] {.async.} =
): Future[Result[BlockHash, string]] {.async.} =
let
epochData = n.accumulator.getBlockEpochDataForBlockNumber(bn).valueOr:
return err(error)
digest = Digest(data: epochData.epochHash)
epoch = (await n.getEpochAccumulator(digest)).valueOr:
return err("Cannot retrieve epoch accumulator for given block number")
blockHash = epoch[epochData.blockRelativeIndex].blockHash

ok(epoch[epochData.blockRelativeIndex].blockHash)

proc getBlock*(
n: HistoryNetwork, bn: UInt256
): Future[Result[Opt[Block], string]] {.async.} =
let
blockHash = ?(await n.getBlockHashByNumber(bn))
maybeBlock = await n.getBlock(blockHash)

return ok(maybeBlock)
Expand Down
3 changes: 3 additions & 0 deletions fluffy/network/state/content/content_values.nim
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ func toRetrievalValue*(offer: ContractTrieNodeOffer): ContractTrieNodeRetrieval
func toRetrievalValue*(offer: ContractCodeOffer): ContractCodeRetrieval =
ContractCodeRetrieval.init(offer.code)

func empty*(T: type TrieProof): T =
TrieProof.init(@[])

func encode*(value: ContentValueType): seq[byte] =
SSZ.encode(value)

Expand Down
3 changes: 3 additions & 0 deletions fluffy/network/state/content/nibbles.nim
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ func init*(T: type Nibbles, packed: openArray[byte], isEven: bool): T =

Nibbles(output)

func empty*(T: type Nibbles): T =
Nibbles.init(@[], true)

func encode*(nibbles: Nibbles): seq[byte] =
SSZ.encode(nibbles)

Expand Down
184 changes: 184 additions & 0 deletions fluffy/network/state/state_endpoints.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# 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.

import
results,
chronos,
chronicles,
eth/common/eth_hash,
eth/common/eth_types,
../../common/common_utils,
./state_network,
./state_utils

export results, state_network

logScope:
topics = "portal_state"

proc getNextNodeHash(
trieNode: TrieNode, nibbles: UnpackedNibbles, nibbleIdx: var int
): Opt[(Nibbles, NodeHash)] =
doAssert(nibbles.len() > 0)
doAssert(nibbleIdx < nibbles.len())

let trieNodeRlp = rlpFromBytes(trieNode.asSeq())
# the trie node should have already been validated
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)
doAssert(not nextHashBytes.isEmpty())

nibbleIdx += 1
return Opt.some(
(
nibbles[0 ..< nibbleIdx].packNibbles(),
KeccakHash.fromBytes(nextHashBytes.toBytes()),
)
)

# leaf or extension node
let (_, isLeaf, prefix) = decodePrefix(trieNodeRlp.listElem(0))
if isLeaf:
return Opt.none((Nibbles, NodeHash))

# extension node
nibbleIdx += prefix.unpackNibbles().len()

let nextHashBytes = trieNodeRlp.listElem(1)
doAssert(not nextHashBytes.isEmpty())

Opt.some(
(
nibbles[0 ..< nibbleIdx].packNibbles(),
KeccakHash.fromBytes(nextHashBytes.toBytes()),
)
)

proc getAccountProof(
n: StateNetwork, stateRoot: KeccakHash, address: Address
): Future[Opt[TrieProof]] {.async.} =
let nibbles = address.toPath().unpackNibbles()

var
nibblesIdx = 0
key = AccountTrieNodeKey.init(Nibbles.empty(), stateRoot)
proof = TrieProof.empty()

while nibblesIdx < nibbles.len():
let
accountTrieNode = (await n.getAccountTrieNode(key)).valueOr:
# log something here
return Opt.none(TrieProof)
trieNode = accountTrieNode.node

let added = proof.add(trieNode)
doAssert(added)

let (nextPath, nextNodeHash) = trieNode.getNextNodeHash(nibbles, nibblesIdx).valueOr:
break

key = AccountTrieNodeKey.init(nextPath, nextNodeHash)

Opt.some(proof)

proc getStorageProof(
n: StateNetwork, storageRoot: KeccakHash, address: Address, storageKey: UInt256
): Future[Opt[TrieProof]] {.async.} =
let nibbles = storageKey.toPath().unpackNibbles()

var
nibblesIdx = 0
key = ContractTrieNodeKey.init(address, Nibbles.empty(), storageRoot)
proof = TrieProof.empty()

while nibblesIdx < nibbles.len():
let
contractTrieNode = (await n.getContractTrieNode(key)).valueOr:
# log something here
return Opt.none(TrieProof)
trieNode = contractTrieNode.node

let added = proof.add(trieNode)
doAssert(added)

let (nextPath, nextNodeHash) = trieNode.getNextNodeHash(nibbles, nibblesIdx).valueOr:
break

key = ContractTrieNodeKey.init(address, nextPath, nextNodeHash)

Opt.some(proof)

proc getAccount(
n: StateNetwork, blockHash: BlockHash, address: Address
): Future[Opt[Account]] {.async.} =
let
stateRoot = (await n.getStateRootByBlockHash(blockHash)).valueOr:
warn "Failed to get state root by block hash"
return Opt.none(Account)
accountProof = (await n.getAccountProof(stateRoot, address)).valueOr:
warn "Failed to get account proof"
return Opt.none(Account)
account = accountProof.toAccount().valueOr:
warn "Failed to get account from accountProof"
return Opt.none(Account)

Opt.some(account)

# Used by: eth_getBalance,
proc getBalance*(
n: StateNetwork, blockHash: BlockHash, address: Address
): Future[Opt[UInt256]] {.async.} =
let account = (await n.getAccount(blockHash, address)).valueOr:
return Opt.none(UInt256)

Opt.some(account.balance)

# Used by: eth_getTransactionCount
proc getTransactionCount*(
n: StateNetwork, blockHash: BlockHash, address: Address
): Future[Opt[AccountNonce]] {.async.} =
let account = (await n.getAccount(blockHash, address)).valueOr:
return Opt.none(AccountNonce)

Opt.some(account.nonce)

# Used by: eth_getStorageAt
proc getStorageAt*(
n: StateNetwork, blockHash: BlockHash, address: Address, slotKey: UInt256
): Future[Opt[UInt256]] {.async.} =
let
account = (await n.getAccount(blockHash, address)).valueOr:
return Opt.none(UInt256)
storageProof = (await n.getStorageProof(account.storageRoot, address, slotKey)).valueOr:
warn "Failed to get storage proof"
return Opt.none(UInt256)
slotValue = storageProof.toSlot().valueOr:
warn "Failed to get slot from storageProof"
return Opt.none(UInt256)

Opt.some(slotValue)

# Used by: eth_getCode
proc getCode*(
n: StateNetwork, blockHash: BlockHash, address: Address
): Future[Opt[Bytecode]] {.async.} =
let
account = (await n.getAccount(blockHash, address)).valueOr:
return Opt.none(Bytecode)
contractCodeKey = ContractCodeKey.init(address, account.codeHash)

let contractCodeRetrieval = (await n.getContractCode(contractCodeKey)).valueOr:
warn "Failed to get contract code"
return Opt.none(Bytecode)

Opt.some(contractCodeRetrieval.code)
2 changes: 1 addition & 1 deletion fluffy/network/state/state_network.nim
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ proc getContractCode*(
): Future[Opt[ContractCodeRetrieval]] {.inline.} =
n.getContent(key, ContractCodeRetrieval)

proc getStateRootByBlockHash(
proc getStateRootByBlockHash*(
n: StateNetwork, hash: BlockHash
): Future[Opt[KeccakHash]] {.async.} =
if n.historyNetwork.isNone():
Expand Down
40 changes: 38 additions & 2 deletions fluffy/network/state/state_utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ func decodePrefix*(nodePrefixRlp: Rlp): (byte, bool, Nibbles) =

(firstNibble.byte, isLeaf, nibbles)

func rlpDecodeAccountTrieNode*(accountNode: TrieNode): Result[Account, string] =
let accNodeRlp = rlpFromBytes(accountNode.asSeq())
func rlpDecodeAccountTrieNode*(accountTrieNode: TrieNode): Result[Account, string] =
let accNodeRlp = rlpFromBytes(accountTrieNode.asSeq())
if accNodeRlp.isEmpty() or accNodeRlp.listLen() != 2:
return err("invalid account trie node - malformed")

Expand All @@ -36,3 +36,39 @@ func rlpDecodeAccountTrieNode*(accountNode: TrieNode): Result[Account, string] =
return err("invalid account trie node - leaf prefix expected")

decodeRlp(accNodeRlp.listElem(1).toBytes(), Account)

# TODO: test the below functions

func rlpDecodeContractTrieNode*(contractTrieNode: TrieNode): Result[UInt256, string] =
let storageNodeRlp = rlpFromBytes(contractTrieNode.asSeq())
if storageNodeRlp.isEmpty() or storageNodeRlp.listLen() != 2:
return err("invalid contract trie node - malformed")

let storageNodePrefixRlp = storageNodeRlp.listElem(0)
if storageNodePrefixRlp.isEmpty():
return err("invalid contract trie node - empty prefix")

let (_, isLeaf, _) = decodePrefix(storageNodePrefixRlp)
if not isLeaf:
return err("invalid contract trie node - leaf prefix expected")

decodeRlp(storageNodeRlp.listElem(1).toBytes(), UInt256)

func toAccount*(accountProof: TrieProof): Result[Account, string] {.inline.} =
doAssert(accountProof.len() > 1)

rlpDecodeAccountTrieNode(accountProof[^1])

func toSlot*(storageProof: TrieProof): Result[UInt256, string] {.inline.} =
doAssert(storageProof.len() > 1)

rlpDecodeContractTrieNode(storageProof[^1])

func toPath*(hash: KeccakHash): Nibbles {.inline.} =
Nibbles.init(hash.data, isEven = true)

func toPath*(address: Address): Nibbles =
keccakHash(address).toPath()

func toPath*(slotKey: UInt256): Nibbles =
keccakHash(toBytesBE(slotKey)).toPath()
10 changes: 4 additions & 6 deletions fluffy/network/state/state_validation.nim
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,14 @@ proc validateOffer*(
proc validateOffer*(
trustedStateRoot: KeccakHash, key: ContractTrieNodeKey, offer: ContractTrieNodeOffer
): Result[void, string] =
let addressHash = keccakHash(key.address).data
?validateTrieProof(
trustedStateRoot,
Nibbles.init(addressHash, true),
key.address.toPath(),
offer.accountProof,
allowKeyEndInPathForLeafs = true,
)

let account = ?rlpDecodeAccountTrieNode(offer.accountProof[^1])
let account = ?offer.accountProof.toAccount()

?validateTrieProof(account.storageRoot, key.path, offer.storageProof)

Expand All @@ -157,15 +156,14 @@ proc validateOffer*(
proc validateOffer*(
trustedStateRoot: KeccakHash, key: ContractCodeKey, offer: ContractCodeOffer
): Result[void, string] =
let addressHash = keccakHash(key.address).data
?validateTrieProof(
trustedStateRoot,
Nibbles.init(addressHash, true),
key.address.toPath(),
offer.accountProof,
allowKeyEndInPathForLeafs = true,
)

let account = ?rlpDecodeAccountTrieNode(offer.accountProof[^1])
let account = ?offer.accountProof.toAccount()
if not offer.code.hashEquals(account.codeHash):
return err("hash of bytecode doesn't match the code hash in the account proof")

Expand Down
12 changes: 12 additions & 0 deletions fluffy/rpc/rpc_calls/rpc_eth_calls.nim
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import
std/json,
stint,
json_serialization/stew/results,
json_rpc/[client, jsonmarshal],
web3/conversions,
Expand All @@ -30,3 +31,14 @@ createRpcSigsFromNim(RpcClient):
proc eth_getBlockReceipts(blockId: string): Opt[seq[ReceiptObject]]
proc eth_getBlockReceipts(blockId: BlockNumber): Opt[seq[ReceiptObject]]
proc eth_getBlockReceipts(blockId: RtBlockIdentifier): Opt[seq[ReceiptObject]]

proc eth_getBalance(data: Address, blockId: BlockIdentifier): UInt256
proc eth_getTransactionCount(data: Address, blockId: BlockIdentifier): Quantity
proc eth_getStorageAt(
data: Address, slot: UInt256, blockId: BlockIdentifier
): FixedBytes[32]

proc eth_getCode(data: Address, blockId: BlockIdentifier): seq[byte]
proc eth_getProof(
address: Address, slots: seq[UInt256], blockId: BlockIdentifier
): ProofResponse
Loading

0 comments on commit c72d6aa

Please sign in to comment.