feat(common): added extensible implementation of the enr typed record

This commit is contained in:
Lorenzo Delgado 2023-03-21 17:27:51 +01:00 committed by GitHub
parent 179be681c4
commit ac56e1dcdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 229 additions and 122 deletions

View File

@ -10,7 +10,7 @@ import
../v2/testlib/waku2
suite "nim-eth ENR - builder":
suite "nim-eth ENR - builder and typed record":
test "Non-supported private key (ECDSA)":
## Given
@ -34,12 +34,19 @@ suite "nim-eth ENR - builder":
## Then
check enrRes.isOk()
let record = enrRes.tryGet().toTypedRecord().get()
let record = enrRes.tryGet().toTyped().get()
let id = record.id
check:
@(record.secp256k1.get()) == expectedPubKey
id == some(RecordId.V4)
let publicKey = record.secp256k1
check:
publicKey.isSome()
@(publicKey.get()) == expectedPubKey
suite "nim-eth ENR - builder ext: IP address and TCP/UDP ports":
suite "nim-eth ENR - Ext: IP address and TCP/UDP ports":
test "EIP-778 test vector":
## Given
@ -92,13 +99,13 @@ suite "nim-eth ENR - builder ext: IP address and TCP/UDP ports":
## Then
check enrRes.isOk()
let record = enrRes.tryGet().toTypedRecord().get()
let record = enrRes.tryGet().toTyped().get()
check:
@(record.secp256k1.get()) == expectedPubKey
record.ip == some(enrIpAddr.address_v4)
record.tcp == some(enrTcpPort.int)
record.udp == none(int)
record.ip6 == none(array[0..15, byte])
record.tcp == some(enrTcpPort.uint16)
record.udp == none(uint16)
record.ip6 == none(array[16, byte])
test "IPv6 and UDP port":
let
@ -122,12 +129,12 @@ suite "nim-eth ENR - builder ext: IP address and TCP/UDP ports":
## Then
check enrRes.isOk()
let record = enrRes.tryGet().toTypedRecord().get()
let record = enrRes.tryGet().toTyped().get()
check:
@(record.secp256k1.get()) == expectedPubKey
record.ip == none(array[0..3, byte])
record.tcp == none(int)
record.udp == none(int)
record.ip == none(array[4, byte])
record.tcp == none(uint16)
record.udp == none(uint16)
record.ip6 == some(enrIpAddr.address_v6)
record.tcp6 == none(int)
record.udp6 == some(enrUdpPort.int)
record.tcp6 == none(uint16)
record.udp6 == some(enrUdpPort.uint16)

View File

@ -179,7 +179,7 @@ suite "Waku ENR - Multiaddresses":
some(enrTcpPort), some(enrUdpPort),
none(CapabilitiesBitfield),
multiaddrs)
typedRecord = record.toTypedRecord.get()
typedRecord = record.toTyped().get()
# Check EIP-778 ENR fields
check:
@ -218,8 +218,8 @@ suite "Waku ENR - Multiaddresses":
let
# Known values correspond to shared test vectors with other Waku implementations
knownIp = ValidIpAddress.init("18.223.219.100")
knownUdpPort = some(9000.int)
knownTcpPort = none(int)
knownUdpPort = some(9000.uint16)
knownTcpPort = none(uint16)
knownMultiaddrs = @[MultiAddress.init("/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss")[],
MultiAddress.init("/dns6/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss")[]]
knownEnr = "enr:-QEnuEBEAyErHEfhiQxAVQoWowGTCuEF9fKZtXSd7H_PymHFhGJA3rGAYDVSH" &
@ -233,7 +233,7 @@ suite "Waku ENR - Multiaddresses":
check:
enrRecord.fromURI(knownEnr)
let typedRecord = enrRecord.toTypedRecord.get()
let typedRecord = enrRecord.toTyped().get()
# Check EIP-778 ENR fields
check:

View File

@ -1,107 +1,13 @@
## An extension wrapper around nim-eth's ENR module
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import eth/p2p/discoveryv5/enr
import
std/options,
stew/results,
stew/shims/net,
eth/keys as eth_keys,
eth/p2p/discoveryv5/enr,
libp2p/crypto/crypto as libp2p_crypto
export enr
## Builder
type EnrBuilder* = object
seqNumber: uint64
privateKey: eth_keys.PrivateKey
fields: seq[FieldPair]
proc init*(T: type EnrBuilder, key: eth_keys.PrivateKey, seqNum: uint64 = 1): EnrBuilder =
EnrBuilder(
seqNumber: seqNum,
privateKey: key,
fields: newSeq[FieldPair]()
)
proc init*(T: type EnrBuilder, key: libp2p_crypto.PrivateKey, seqNum: uint64 = 1): EnrBuilder =
# TODO: Inconvenient runtime assertion. Move this assertion to compile time
if key.scheme != PKScheme.Secp256k1:
raise newException(Defect, "invalid private key scheme")
let
bytes = key.getRawBytes().expect("Private key is valid")
privateKey = eth_keys.PrivateKey.fromRaw(bytes).expect("Raw private key is of valid length")
EnrBuilder.init(key=privateKey, seqNum=seqNum)
proc addFieldPair*(builder: var EnrBuilder, pair: FieldPair) =
builder.fields.add(pair)
proc addFieldPair*[V](builder: var EnrBuilder, key: string, value: V) =
builder.addFieldPair(toFieldPair(key, value))
proc build*(builder: EnrBuilder): EnrResult[enr.Record] =
# Note that nim-eth's `Record.init` does not deduplicate the field pairs.
# See: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L143-L144
enr.Record.init(
seqNum = builder.seqNumber,
pk = builder.privateKey,
ip = none(ValidIpAddress),
tcpPort = none(Port),
udpPort = none(Port),
extraFields = builder.fields
)
## Builder extension: IP address and TCP/UDP ports
proc addAddressAndPorts(builder: var EnrBuilder, ip: ValidIpAddress, tcpPort, udpPort: Option[Port]) =
# Based on: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L166
let isV6 = ip.family == IPv6
let ipField = if isV6: toFieldPair("ip6", ip.address_v6)
else: toFieldPair("ip", ip.address_v4)
builder.addFieldPair(ipField)
if tcpPort.isSome():
let
tcpPortFieldKey = if isV6: "tcp6" else: "tcp"
tcpPortFieldValue = tcpPort.get()
builder.addFieldPair(tcpPortFieldKey, tcpPortFieldValue.uint16)
if udpPort.isSome():
let
udpPortFieldKey = if isV6: "udp6" else: "udp"
udpPortFieldValue = udpPort.get()
builder.addFieldPair(udpPortFieldKey, udpPortFieldValue.uint16)
proc addPorts(builder: var EnrBuilder, tcp, udp: Option[Port]) =
# Based on: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L166
if tcp.isSome():
let tcpPort = tcp.get()
builder.addFieldPair("tcp", tcpPort.uint16)
if udp.isSome():
let udpPort = udp.get()
builder.addFieldPair("udp", udpPort.uint16)
proc withIpAddressAndPorts*(builder: var EnrBuilder,
ipAddr = none(ValidIpAddress),
tcpPort = none(Port),
udpPort = none(Port)) =
if ipAddr.isSome():
addAddressAndPorts(builder, ipAddr.get(), tcpPort, udpPort)
else:
addPorts(builder, tcpPort, udpPort)
./enr/builder,
./enr/typed_record
export
enr.Record, enr.EnrResult, enr.get, enr.tryGet,
enr.fromBase64, enr.toBase64, enr.fromURI, enr.toURI,
enr.FieldPair, enr.toFieldPair, enr.init, # TODO: Delete after removing the deprecated procs
builder,
typed_record

103
waku/common/enr/builder.nim Normal file
View File

@ -0,0 +1,103 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
std/options,
stew/results,
stew/shims/net,
eth/keys as eth_keys,
eth/p2p/discoveryv5/enr,
libp2p/crypto/crypto as libp2p_crypto
## Builder
type EnrBuilder* = object
seqNumber: uint64
privateKey: eth_keys.PrivateKey
fields: seq[FieldPair]
proc init*(T: type EnrBuilder, key: eth_keys.PrivateKey, seqNum: uint64 = 1): T =
EnrBuilder(
seqNumber: seqNum,
privateKey: key,
fields: newSeq[FieldPair]()
)
proc init*(T: type EnrBuilder, key: libp2p_crypto.PrivateKey, seqNum: uint64 = 1): T =
# TODO: Inconvenient runtime assertion. Move this assertion to compile time
if key.scheme != PKScheme.Secp256k1:
raise newException(Defect, "invalid private key scheme")
let
bytes = key.getRawBytes().expect("Private key is valid")
privateKey = eth_keys.PrivateKey.fromRaw(bytes).expect("Raw private key is of valid length")
EnrBuilder.init(key=privateKey, seqNum=seqNum)
proc addFieldPair*(builder: var EnrBuilder, pair: FieldPair) =
builder.fields.add(pair)
proc addFieldPair*[V](builder: var EnrBuilder, key: string, value: V) =
builder.addFieldPair(toFieldPair(key, value))
proc build*(builder: EnrBuilder): EnrResult[enr.Record] =
# Note that nim-eth's `Record.init` does not deduplicate the field pairs.
# See: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L143-L144
enr.Record.init(
seqNum = builder.seqNumber,
pk = builder.privateKey,
ip = none(ValidIpAddress),
tcpPort = none(Port),
udpPort = none(Port),
extraFields = builder.fields
)
## Builder extension: IP address and TCP/UDP ports
proc addAddressAndPorts(builder: var EnrBuilder, ip: ValidIpAddress, tcpPort, udpPort: Option[Port]) =
# Based on: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L166
let isV6 = ip.family == IPv6
let ipField = if isV6: toFieldPair("ip6", ip.address_v6)
else: toFieldPair("ip", ip.address_v4)
builder.addFieldPair(ipField)
if tcpPort.isSome():
let
tcpPortFieldKey = if isV6: "tcp6" else: "tcp"
tcpPortFieldValue = tcpPort.get()
builder.addFieldPair(tcpPortFieldKey, tcpPortFieldValue.uint16)
if udpPort.isSome():
let
udpPortFieldKey = if isV6: "udp6" else: "udp"
udpPortFieldValue = udpPort.get()
builder.addFieldPair(udpPortFieldKey, udpPortFieldValue.uint16)
proc addPorts(builder: var EnrBuilder, tcp, udp: Option[Port]) =
# Based on: https://github.com/status-im/nim-eth/blob/4b22fcd/eth/p2p/discoveryv5/enr.nim#L166
if tcp.isSome():
let tcpPort = tcp.get()
builder.addFieldPair("tcp", tcpPort.uint16)
if udp.isSome():
let udpPort = udp.get()
builder.addFieldPair("udp", udpPort.uint16)
proc withIpAddressAndPorts*(builder: var EnrBuilder,
ipAddr = none(ValidIpAddress),
tcpPort = none(Port),
udpPort = none(Port)) =
if ipAddr.isSome():
addAddressAndPorts(builder, ipAddr.get(), tcpPort, udpPort)
else:
addPorts(builder, tcpPort, udpPort)

View File

@ -0,0 +1,91 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
std/options,
stew/results,
eth/keys as eth_keys,
libp2p/crypto/crypto as libp2p_crypto
import eth/p2p/discoveryv5/enr except TypedRecord, toTypedRecord
## ENR typed record
# Record identity scheme
type RecordId* {.pure.} = enum
V4
func toRecordId(id: string): EnrResult[RecordId] =
case id:
of "v4":
ok(RecordId.V4)
else:
err("unknown identity scheme")
func `$`*(id: RecordId): string =
case id:
of RecordId.V4: "v4"
# Typed record
type TypedRecord* = object
raw: Record
proc init(T: type TypedRecord, record: Record): T =
TypedRecord(raw: record)
proc tryGet*(record: TypedRecord, field: string, T: type): Option[T] =
record.raw.tryGet(field, T)
func toTyped*(record: Record): EnrResult[TypedRecord] =
let tr = TypedRecord.init(record)
# Validate record's identity scheme
let idOpt = tr.tryGet("id", string)
if idOpt.isNone():
return err("missing id scheme field")
discard ? toRecordId(idOpt.get())
ok(tr)
# Typed record field accessors
func id*(record: TypedRecord): Option[RecordId] =
let fieldOpt = record.tryGet("id", string)
if fieldOpt.isNone():
return none(RecordId)
let fieldRes = toRecordId(fieldOpt.get())
if fieldRes.isErr():
return none(RecordId)
some(fieldRes.value)
func secp256k1*(record: TypedRecord): Option[array[33, byte]] =
record.tryGet("secp256k1", array[33, byte])
func ip*(record: TypedRecord): Option[array[4, byte]] =
record.tryGet("ip", array[4, byte])
func ip6*(record: TypedRecord): Option[array[16, byte]] =
record.tryGet("ip6", array[16, byte])
func tcp*(record: TypedRecord): Option[uint16] =
record.tryGet("tcp", uint16)
func tcp6*(record: TypedRecord): Option[uint16] =
record.tryGet("tcp6", uint16)
func udp*(record: TypedRecord): Option[uint16] =
record.tryGet("udp", uint16)
func udp6*(record: TypedRecord): Option[uint16] =
record.tryGet("udp6", uint16)

View File

@ -8,7 +8,7 @@ else:
{.push raises: [].}
import
std/[bitops, sequtils],
std/[options, bitops, sequtils],
stew/[endians2, results],
stew/shims/net,
eth/keys,