Skip to content

Commit

Permalink
Add push raises to Fluffy state network code. (#2352)
Browse files Browse the repository at this point in the history
* Add push raises to Fluffy state network code.

* Refactor handling of rlp errors.
  • Loading branch information
web3-developer committed Jun 14, 2024
1 parent 329a8f0 commit 7fd777c
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 109 deletions.
2 changes: 2 additions & 0 deletions fluffy/network/state/state_content.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# * 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 ./content/content_keys, ./content/content_values, ./content/nibbles

export content_keys, content_values, nibbles
67 changes: 37 additions & 30 deletions fluffy/network/state/state_endpoints.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# * 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,
Expand All @@ -23,46 +25,51 @@ logScope:
proc getNextNodeHash(
trieNode: TrieNode, nibbles: UnpackedNibbles, nibbleIdx: var int
): Opt[(Nibbles, NodeHash)] =
doAssert(nibbles.len() > 0)
doAssert(nibbleIdx < nibbles.len())
# 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)
doAssert(not nextHashBytes.isEmpty())

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

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)
# leaf or extension node
let (_, isLeaf, prefix) = decodePrefix(trieNodeRlp.listElem(0))
if isLeaf:
return Opt.none((Nibbles, NodeHash))

if trieNodeRlp.listLen() == 17:
let nextNibble = nibbles[nibbleIdx]
doAssert(nextNibble < 16)
# extension node
nibbleIdx += prefix.unpackNibbles().len()

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

nibbleIdx += 1
return Opt.some(
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()),
)
)
except RlpError as e:
raiseAssert(e.msg)

proc getAccountProof(
n: StateNetwork, stateRoot: KeccakHash, address: Address
Expand Down
35 changes: 21 additions & 14 deletions fluffy/network/state/state_gossip.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# * 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,
Expand Down Expand Up @@ -41,26 +43,31 @@ func withKey*(
(key: key, offer: offer)

func getParent(p: ProofWithPath): ProofWithPath =
doAssert(p.path.len() > 0, "nibbles too short")
doAssert(p.proof.len() > 1, "proof too short")
# this function assumes that the proof contains valid rlp therefore
# if required these proofs should be validated beforehand
try:
doAssert(p.path.len() > 0, "nibbles too short")
doAssert(p.proof.len() > 1, "proof too short")

let
parentProof = TrieProof.init(p.proof[0 ..^ 2])
parentEndNode = rlpFromBytes(parentProof[^1].asSeq())
let
parentProof = TrieProof.init(p.proof[0 ..^ 2])
parentEndNode = rlpFromBytes(parentProof[^1].asSeq())

# the trie proof should have already been validated when receiving the offer content
doAssert(parentEndNode.listLen() == 2 or parentEndNode.listLen() == 17)
# the trie proof should have already been validated when receiving the offer content
doAssert(parentEndNode.listLen() == 2 or parentEndNode.listLen() == 17)

var unpackedNibbles = p.path.unpackNibbles()
var unpackedNibbles = p.path.unpackNibbles()

if parentEndNode.listLen() == 17:
# branch node so only need to remove a single nibble
return parentProof.withPath(unpackedNibbles.dropN(1).packNibbles())
if parentEndNode.listLen() == 17:
# branch node so only need to remove a single nibble
return parentProof.withPath(unpackedNibbles.dropN(1).packNibbles())

# leaf or extension node so we need to remove one or more nibbles
let prefixNibbles = decodePrefix(parentEndNode.listElem(0))[2]
# leaf or extension node so we need to remove one or more nibbles
let (_, _, prefixNibbles) = decodePrefix(parentEndNode.listElem(0))

parentProof.withPath(unpackedNibbles.dropN(prefixNibbles.len()).packNibbles())
parentProof.withPath(unpackedNibbles.dropN(prefixNibbles.len()).packNibbles())
except RlpError as e:
raiseAssert(e.msg)

func getParent*(offerWithKey: AccountTrieOfferWithKey): AccountTrieOfferWithKey =
let
Expand Down
2 changes: 2 additions & 0 deletions fluffy/network/state/state_network.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# * 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,
Expand Down
56 changes: 31 additions & 25 deletions fluffy/network/state/state_utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
# * 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, eth/common, ./state_content

export results, common

func decodePrefix*(nodePrefixRlp: Rlp): (byte, bool, Nibbles) =
func decodePrefix*(nodePrefixRlp: Rlp): (byte, bool, Nibbles) {.raises: RlpError.} =
doAssert(not nodePrefixRlp.isEmpty())

let
Expand All @@ -23,36 +25,40 @@ func decodePrefix*(nodePrefixRlp: Rlp): (byte, bool, Nibbles) =
(firstNibble.byte, isLeaf, nibbles)

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")

let accNodePrefixRlp = accNodeRlp.listElem(0)
if accNodePrefixRlp.isEmpty():
return err("invalid account trie node - empty prefix")
try:
let accNodeRlp = rlpFromBytes(accountTrieNode.asSeq())
if accNodeRlp.isEmpty() or accNodeRlp.listLen() != 2:
return err("invalid account trie node - malformed")

let (_, isLeaf, _) = decodePrefix(accNodePrefixRlp)
if not isLeaf:
return err("invalid account trie node - leaf prefix expected")
let accNodePrefixRlp = accNodeRlp.listElem(0)
if accNodePrefixRlp.isEmpty():
return err("invalid account trie node - empty prefix")

decodeRlp(accNodeRlp.listElem(1).toBytes(), Account)
let (_, isLeaf, _) = decodePrefix(accNodePrefixRlp)
if not isLeaf:
return err("invalid account trie node - leaf prefix expected")

# TODO: test the below functions
decodeRlp(accNodeRlp.listElem(1).toBytes(), Account)
except RlpError as e:
err(e.msg)

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")
try:
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 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")
let (_, isLeaf, _) = decodePrefix(storageNodePrefixRlp)
if not isLeaf:
return err("invalid contract trie node - leaf prefix expected")

decodeRlp(storageNodeRlp.listElem(1).toBytes(), UInt256)
decodeRlp(storageNodeRlp.listElem(1).toBytes(), UInt256)
except RlpError as e:
err(e.msg)

func toAccount*(accountProof: TrieProof): Result[Account, string] {.inline.} =
doAssert(accountProof.len() > 1)
Expand All @@ -67,8 +73,8 @@ func toSlot*(storageProof: TrieProof): Result[UInt256, string] {.inline.} =
func toPath*(hash: KeccakHash): Nibbles {.inline.} =
Nibbles.init(hash.data, isEven = true)

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

func toPath*(slotKey: UInt256): Nibbles =
func toPath*(slotKey: UInt256): Nibbles {.inline.} =
keccakHash(toBytesBE(slotKey)).toPath()
87 changes: 47 additions & 40 deletions fluffy/network/state/state_validation.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
# * 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, eth/common, ../../common/common_utils, ./state_content, ./state_utils

export results, state_content

proc hashEquals(value: TrieNode | Bytecode, expectedHash: KeccakHash): bool {.inline.} =
keccakHash(value.asSeq()) == expectedHash

proc isValidNextNode(thisNodeRlp: Rlp, rlpIdx: int, nextNode: TrieNode): bool =
proc isValidNextNode(
thisNodeRlp: Rlp, rlpIdx: int, nextNode: TrieNode
): bool {.raises: RlpError.} =
let hashOrShortRlp = thisNodeRlp.listElem(rlpIdx)
if hashOrShortRlp.isEmpty():
return false
Expand Down Expand Up @@ -63,45 +67,48 @@ proc validateTrieProof*(
else:
return err("proof has more nodes then expected for given path")

case thisNodeRlp.listLen()
of 2:
let nodePrefixRlp = thisNodeRlp.listElem(0)
if nodePrefixRlp.isEmpty():
return err("node prefix is empty")

let (prefix, isLeaf, prefixNibbles) = decodePrefix(nodePrefixRlp)
if prefix >= 4:
return err("invalid prefix in node")

if not isLastNode or (isLeaf and allowKeyEndInPathForLeafs):
let unpackedPrefix = prefixNibbles.unpackNibbles()
if remainingNibbles < unpackedPrefix.len():
return err("not enough nibbles to validate node prefix")

let nibbleEndIdx = nibbleIdx + unpackedPrefix.len()
if nibbles[nibbleIdx ..< nibbleEndIdx] != unpackedPrefix:
return err("nibbles don't match node prefix")
nibbleIdx += unpackedPrefix.len()

if not isLastNode:
if isLeaf:
return err("leaf node must be last node in the proof")
else: # is extension node
if not isValidNextNode(thisNodeRlp, 1, proof[proofIdx + 1]):
return
err("hash of next node doesn't match the expected extension node hash")
of 17:
if not isLastNode:
let nextNibble = nibbles[nibbleIdx]
if nextNibble >= 16:
return err("invalid next nibble for branch node")

if not isValidNextNode(thisNodeRlp, nextNibble.int, proof[proofIdx + 1]):
return err("hash of next node doesn't match the expected branch node hash")

inc nibbleIdx
else:
return err("invalid rlp node, expected 2 or 17 elements")
try:
case thisNodeRlp.listLen()
of 2:
let nodePrefixRlp = thisNodeRlp.listElem(0)
if nodePrefixRlp.isEmpty():
return err("node prefix is empty")

let (prefix, isLeaf, prefixNibbles) = decodePrefix(nodePrefixRlp)
if prefix >= 4:
return err("invalid prefix in node")

if not isLastNode or (isLeaf and allowKeyEndInPathForLeafs):
let unpackedPrefix = prefixNibbles.unpackNibbles()
if remainingNibbles < unpackedPrefix.len():
return err("not enough nibbles to validate node prefix")

let nibbleEndIdx = nibbleIdx + unpackedPrefix.len()
if nibbles[nibbleIdx ..< nibbleEndIdx] != unpackedPrefix:
return err("nibbles don't match node prefix")
nibbleIdx += unpackedPrefix.len()

if not isLastNode:
if isLeaf:
return err("leaf node must be last node in the proof")
else: # is extension node
if not isValidNextNode(thisNodeRlp, 1, proof[proofIdx + 1]):
return
err("hash of next node doesn't match the expected extension node hash")
of 17:
if not isLastNode:
let nextNibble = nibbles[nibbleIdx]
if nextNibble >= 16:
return err("invalid next nibble for branch node")

if not isValidNextNode(thisNodeRlp, nextNibble.int, proof[proofIdx + 1]):
return err("hash of next node doesn't match the expected branch node hash")

inc nibbleIdx
else:
return err("invalid rlp node, expected 2 or 17 elements")
except RlpError as e:
return err(e.msg)

if nibbleIdx < nibbles.len():
err("path contains more nibbles than expected for proof")
Expand Down

0 comments on commit 7fd777c

Please sign in to comment.