Skip to content

Commit

Permalink
Align core types with execution spec
Browse files Browse the repository at this point in the history
Since these types were written, we've gained an executable spec:

https://github.com/ethereum/execution-specs

This PR aligns some of the types we use with this spec to simplify
comparisons and cross-referencing.

Using a `distinct` type is a tradeoff between nim ergonomics, type
safety and the ability to work around nim quirks and stdlib weaknesses.

In particular, it allows us to overload common functions such as `hash`
with correct and performant versions as well as maintain control over
string conversions etc at the cost of a little bit of ceremony when
instantiating them.

Apart from distinct byte types, `Hash32`, is introduced in lieu of the
existing `Hash256`, again aligning this commonly used type with the spec
which picks bytes rather than bits in the name.
  • Loading branch information
arnetheduck committed Sep 23, 2024
1 parent aa1e738 commit a157a70
Show file tree
Hide file tree
Showing 30 changed files with 316 additions and 208 deletions.
4 changes: 2 additions & 2 deletions doc/trie.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ Additional APIs are:
that starts with the same key prefix
* rootNode() -- get root node
* rootNode(node) -- replace the root node
* getRootHash(): `KeccakHash` with `seq[byte]` type
* getRootHash(): `Hash32` with `seq[byte]` type
* getDB(): `DB` -- get flat-db pointer

Constructor API:
* initBinaryTrie(DB, rootHash[optional]) -- rootHash has `seq[byte]` or KeccakHash type
* initBinaryTrie(DB, rootHash[optional]) -- rootHash has `seq[byte]` or Hash32 type
* init(BinaryTrie, DB, rootHash[optional])

Normally you would not set the rootHash when constructing an empty Binary-trie.
Expand Down
18 changes: 9 additions & 9 deletions eth/bloom.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import stint, ./common/eth_hash

type UInt2048 = StUint[2048]

iterator chunksForBloom(h: MDigest[256]): array[2, uint8] =
iterator chunksForBloom(h: Hash32): array[2, uint8] =
yield [h.data[0], h.data[1]]
yield [h.data[2], h.data[3]]
yield [h.data[4], h.data[5]]
Expand All @@ -12,28 +12,28 @@ proc chunkToBloomBits(chunk: array[2, uint8]): UInt2048 =
let l = chunk[1].int
one(UInt2048) shl ((l + (h shl 8)) and 2047)

iterator bloomBits(h: MDigest[256]): UInt2048 =
iterator bloomBits(h: Hash32): UInt2048 =
for chunk in chunksForBloom(h):
yield chunkToBloomBits(chunk)

type BloomFilter* = object
value*: UInt2048

proc incl*(f: var BloomFilter, h: MDigest[256]) =
proc incl*(f: var BloomFilter, h: Hash32) =
for bits in bloomBits(h):
f.value = f.value or bits

proc init*(_: type BloomFilter, h: MDigest[256]): BloomFilter =
proc init*(_: type BloomFilter, h: Hash32): BloomFilter =
result.incl(h)

# TODO: The following 2 procs should be one genric, but it doesn't compile. Nim bug?
proc incl*(f: var BloomFilter, v: string) = f.incl(keccakHash(v))
proc incl*(f: var BloomFilter, v: openArray[byte]) = f.incl(keccakHash(v))
proc incl*(f: var BloomFilter, v: string) = f.incl(keccak256(v))
proc incl*(f: var BloomFilter, v: openArray[byte]) = f.incl(keccak256(v))

proc contains*(f: BloomFilter, h: MDigest[256]): bool =
proc contains*(f: BloomFilter, h: Hash32): bool =
for bits in bloomBits(h):
if (f.value and bits).isZero: return false
return true

template contains*[T](f: BloomFilter, v: openArray[T]): bool =
f.contains(keccakHash(v))
template contains*(f: BloomFilter, v: openArray): bool =
f.contains(keccak256(v))
62 changes: 62 additions & 0 deletions eth/common/base_types.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{.push raises: [].}

import
std/[hashes, macros, typetraits],
results,
stew/[byteutils, staticfor]

export hashes, results

type
FixedBytes*[N: static int] = distinct array[N, byte]
## Fixed-length byte sequence holding arbitrary data
## https://github.com/ethereum/execution-specs/blob/51fac24740e662844446439ceeb96a460aae0ba0/src/ethereum/base_types.py

# A distinct array is used to avoid copying on trivial type conversions

Bytes4* = FixedBytes[4]
Bytes8* = FixedBytes[8]
Bytes20* = FixedBytes[20]
Bytes32* = FixedBytes[32]
Bytes48* = FixedBytes[48]
Bytes64* = FixedBytes[64]
Bytes96* = FixedBytes[96]
Bytes256* = FixedBytes[256]

template to*[N: static int](v: array[N, byte], _: type FixedBytes): FixedBytes[N] =
FixedBytes(N)(v)

template default*(_: type FixedBytes): FixedBytes =
# Avoid bad codegen where fixed bytes are zeroed byte-by-byte at call site
const def = system.default(FixedBytes)
def

template data*(v: FixedBytes): array =
distinctBase(v)

func len*(_: type FixedBytes): int =
sizeof(FixedBytes)
func `==`*(a, b: FixedBytes): bool {.inline.} =
equalMem(addr a.data[0], addr b.data[0], a.N)

func hash*[N: static int](v: FixedBytes[N]): Hash {.inline.} =
copyMem(addr result, addr v.data[0], min(N, sizeof(Hash)))

when N > sizeof(Hash):
var tmp: Hash
staticFor i, 1 ..< N div sizeof(Hash):
copyMem(addr tmp, addr v.data[i * sizeof(Hash)], sizeof(Hash))
result = result !& tmp
const last = N mod sizeof(Hash)
when last > 0:
copyMem(addr tmp, addr v.data[N - last], last)
result !& tmp

func toHex*(v: FixedBytes): string =
toHex(v.data)

func `$`*(v: FixedBytes): string =
toHex(v)

func fromHex*(T: type FixedBytes, c: openArray[char]): T {.raises: [ValueError].} =
T(hexToByteArray(c, T.N))
71 changes: 52 additions & 19 deletions eth/common/eth_hash.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,74 @@

{.push raises: [].}

## keccak256 is used across ethereum as the "default" hash function and this
## module provides a type and some helpers to produce such hashes
## Keccak256 hash function use thoughout the ethereum execution specification
## https://github.com/ethereum/execution-specs/blob/51fac24740e662844446439ceeb96a460aae0ba0/src/ethereum/crypto/hash.py
import std/typetraits, nimcrypto/keccak, ./base_types

import
nimcrypto/[keccak, hash]
export keccak.update, keccak.finish

export
keccak.update, keccak.finish, hash
type Hash32* = distinct Bytes32

type
KeccakHash* = MDigest[256]
## A hash value computed using keccak256
## note: this aliases Eth2Digest too, which uses a different hash!
template data*(v: Hash32): array[32, byte] =
distinctBase(v)

template default*(_: type Hash32): Hash32 =
# Avoid bad codegen where fixed bytes are zeroed byte-by-byte at call site
const def = system.default(Hash32)
def

func `==`*(a, b: Hash32): bool {.borrow.}
func `$`*(a: Hash32): string {.borrow.}

func fromHex*(_: type Hash32, s: string): Hash32 {.raises: [ValueError].} =
Hash32(Bytes32.fromHex(s))

func hash*(a: Hash32): Hash =
var tmp: array[4, uint64]
copyMem(addr tmp[0], addr a.data[0], sizeof(a))
cast[Hash](tmp[0] + tmp[1] + tmp[2] + tmp[3])

template to*(v: MDigest[256], _: type Hash32): Hash32 =
Hash32(v.data)

template to*(s: static string, _: type Hash32): Hash32 =
const hash = Hash32.fromHex(s)
hash

template hash32*(s: static string): Hash32 =
s.to(Hash32)

func keccak256*(input: openArray[byte]): Hash32 {.noinit.} =
var ctx: keccak.keccak256
ctx.update(input)
ctx.finish().to(Hash32)

func keccak256*(input: openArray[char]): Hash32 {.noinit.} =
keccak256(input.toOpenArrayByte(0, input.high))

type KeccakHash* {.deprecated.} = MDigest[256]

template withKeccakHash*(body: untyped): KeccakHash =
## This little helper will init the hash function and return the sliced
## hash:
## let hashOfData = withHash: h.update(data)
block:
var h {.inject.}: keccak256
var h {.inject.}: keccak.keccak256
# init(h) # not needed for new instance
body
finish(h)

func keccakHash*(input: openArray[byte]): KeccakHash {.noinit.} =
# We use the init-update-finish interface to avoid
# the expensive burning/clearing memory (20~30% perf)
var ctx: keccak256
ctx.update(input)
ctx.finish()
func keccakHash*(input: openArray[byte]): KeccakHash {.noinit, deprecated.} =
# We use the init-update-finish interface to avoid
# the expensive burning/clearing memory (20~30% perf)
var ctx: keccak256
ctx.update(input)
ctx.finish()

func keccakHash*(input: openArray[char]): KeccakHash {.noinit.} =
func keccakHash*(input: openArray[char]): KeccakHash {.noinit, deprecated.} =
keccakHash(input.toOpenArrayByte(0, input.high()))

func keccakHash*(a, b: openArray[byte]): KeccakHash =
func keccakHash*(a, b: openArray[byte]): KeccakHash {.noinit, deprecated.} =
withKeccakHash:
h.update a
h.update b
6 changes: 3 additions & 3 deletions eth/common/eth_hash_rlp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import

export eth_hash, rlp

proc read*(rlp: var Rlp, T: typedesc[MDigest]): T =
result.data = rlp.read(type(result.data))
proc read*(rlp: var Rlp, T: type Hash32): Hash32 =
result = Hash32(rlp.read(type(result.data)))

proc append*(rlpWriter: var RlpWriter, a: MDigest) =
proc append*(rlpWriter: var RlpWriter, a: Hash32) =
rlpWriter.append(a.data)
Loading

0 comments on commit a157a70

Please sign in to comment.