libp2p-dht/libp2pdht/private/eth/p2p/discoveryv5/spr.nim

361 lines
12 KiB
Nim

# Copyright (c) 2020-2022 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
chronicles,
std/[options, sequtils, strutils, sugar],
pkg/stew/[results, byteutils, arrayops],
stew/endians2,
stew/shims/net,
stew/base64,
eth/rlp,
eth/keys,
libp2p/crypto/crypto,
libp2p/crypto/secp,
libp2p/routing_record,
libp2p/multicodec
export routing_record
from chronos import TransportAddress, initTAddress
export options, results
type
SprUri* = distinct string
RecordResult*[T] = Result[T, cstring]
proc seqNum*(r: SignedPeerRecord): uint64 =
r.data.seqNo
#proc encode
proc append*(rlpWriter: var RlpWriter, value: SignedPeerRecord) =
# echo "encoding to:" & $value.signedPeerRecord.encode.get
var encoded = value.encode
trace "Encoding SignedPeerRecord for RLP", bytes = encoded.get(@[])
if encoded.isErr:
error "Error encoding SignedPeerRecord for RLP", error = encoded.error
rlpWriter.append encoded.get(@[])
proc fromBytes(r: var SignedPeerRecord, s: openArray[byte]): bool =
trace "Decoding SignedPeerRecord for RLP", bytes = s
let decoded = SignedPeerRecord.decode(@s)
if decoded.isErr:
error "Error decoding SignedPeerRecord", error = decoded.error
return false
r = decoded.get
return true
proc read*(rlp: var Rlp, T: typedesc[SignedPeerRecord]):
T {.raises: [RlpError, ValueError, Defect].} =
# echo "read:" & $rlp.rawData
## code directly borrowed from spr.nim
trace "Reading RLP SignedPeerRecord", rawData = rlp.rawData, toBytes = rlp.toBytes
if not rlp.hasData() or not result.fromBytes(rlp.toBytes):
# TODO: This could also just be an invalid signature, would be cleaner to
# split of RLP deserialisation errors from this.
raise newException(ValueError, "Could not deserialize")
rlp.skipElem()
proc get*(r: SignedPeerRecord, T: type crypto.PublicKey): Option[T] =
## Get the `PublicKey` from provided `Record`. Return `none` when there is
## no `PublicKey` in the record.
some(r.envelope.publicKey)
func pkToPk(pk: crypto.PublicKey) : Option[keys.PublicKey] =
some((keys.PublicKey)(pk.skkey))
func pkToPk(pk: keys.PublicKey) : Option[crypto.PublicKey] =
some(crypto.PublicKey.init((secp.SkPublicKey)(pk)))
func pkToPk(pk: crypto.PrivateKey) : Option[keys.PrivateKey] =
some((keys.PrivateKey)(pk.skkey))
func pkToPk(pk: keys.PrivateKey) : Option[crypto.PrivateKey] =
some(crypto.PrivateKey.init((secp.SkPrivateKey)(pk)))
proc get*(r: SignedPeerRecord, T: type keys.PublicKey): Option[T] =
## Get the `PublicKey` from provided `Record`. Return `none` when there is
## no `PublicKey` in the record.
## PublicKey* = distinct SkPublicKey
let
pk = r.envelope.publicKey
pkToPk(pk)
proc incSeqNo*(
r: var SignedPeerRecord,
pk: keys.PrivateKey): RecordResult[void] =
let cryptoPk = pk.pkToPk.get() # TODO: remove when eth/keys removed
r.data.seqNo.inc()
r = ? SignedPeerRecord.init(cryptoPk, r.data).mapErr(
(e: CryptoError) =>
("Error initialising SignedPeerRecord with incremented seqNo: " &
$e).cstring
)
ok()
proc update*(r: var SignedPeerRecord, pk: crypto.PrivateKey,
ip: Option[ValidIpAddress],
tcpPort, udpPort: Option[Port] = none[Port]()):
RecordResult[void] =
## Update a `SignedPeerRecord` with given ip address, tcp port, udp port and optional
## custom k:v pairs.
##
## In case any of the k:v pairs is updated or added (new), the sequence number
## of the `Record` will be incremented and a new signature will be applied.
##
## Can fail in case of wrong `PrivateKey`, if the size of the resulting record
## exceeds `maxSprSize` or if maximum sequence number is reached. The `Record`
## will not be altered in these cases.
# TODO: handle custom field pairs?
# TODO: We have a mapping issue here because PeerRecord has multiple
# addresses and the proc signature only allows updating of a single
# ip/tcpPort/udpPort/extraFields
let
pubkey = r.get(crypto.PublicKey)
keysPubKey = pubkey.get.pkToPk.get # remove when move away from eth/keys
keysPrivKey = pk.pkToPk.get
if pubkey.isNone() or keysPubKey != keysPrivKey.toPublicKey:
return err("Public key does not correspond with given private key")
var
changed = false
transProto = IpTransportProtocol.udpProtocol
transProtoPort: Port
var updated: MultiAddress
if r.data.addresses.len == 0:
changed = true
if ip.isNone:
return err "No existing address in SignedPeerRecord with no IP provided"
if udpPort.isNone and tcpPort.isNone:
return err "No existing address in SignedPeerRecord with no port provided"
let ipAddr = try: ValidIpAddress.init(ip.get)
except ValueError as e:
return err ("Existing address contains invalid address: " & $e.msg).cstring
if tcpPort.isSome:
transProto = IpTransportProtocol.tcpProtocol
transProtoPort = tcpPort.get
if udpPort.isSome:
transProto = IpTransportProtocol.udpProtocol
transProtoPort = udpPort.get
updated = MultiAddress.init(ipAddr, transProto, transProtoPort)
else:
let
existing = r.data.addresses[0].address
existingNetProto = ? existing[0].mapErr((e: string) => e.cstring)
existingTransProto = ? existing[1].mapErr((e: string) => e.cstring)
existingNetProtoFam = ? existingNetProto.protoCode
.mapErr((e: string) => e.cstring)
existingNetProtoAddr = ? existingNetProto.protoAddress
.mapErr((e: string) => e.cstring)
existingTransProtoCodec = ? existingTransProto.protoCode
.mapErr((e: string) => e.cstring)
existingTransProtoPort = ? existingTransProto.protoAddress
.mapErr((e: string) => e.cstring)
existingIp =
if existingNetProtoFam == MultiCodec.codec("ip6"):
ipv6 array[16, byte].initCopyFrom(existingNetProtoAddr)
else:
ipv4 array[4, byte].initCopyFrom(existingNetProtoAddr)
ipAddr = ip.get(existingIp)
if tcpPort.isNone and udpPort.isNone:
transProto =
if existingTransProtoCodec == MultiCodec.codec("udp"):
IpTransportProtocol.udpProtocol
else: IpTransportProtocol.tcpProtocol
transProtoPort = Port(uint16.fromBytesBE(existingTransProtoPort))
else:
if tcpPort.isSome:
transProto = IpTransportProtocol.tcpProtocol
transProtoPort = tcpPort.get
if udpPort.isSome:
transProto = IpTransportProtocol.udpProtocol
transProtoPort = udpPort.get
updated = MultiAddress.init(ipAddr, transProto, transProtoPort)
changed = existing != updated
r.data.addresses[0].address = updated
# increase the sequence number only if we've updated the multiaddress
if changed: r.data.seqNo.inc()
r = ? SignedPeerRecord.init(pk, r.data)
.mapErr((e: CryptoError) =>
("Failed to update SignedPeerRecord: " & $e).cstring
)
return ok()
proc update*(r: var SignedPeerRecord, pk: keys.PrivateKey,
ip: Option[ValidIpAddress],
tcpPort, udpPort: Option[Port] = none[Port]()):
RecordResult[void] =
let cPk = pkToPk(pk).get
r.update(cPk, ip, tcpPort, udpPort)
proc toTypedRecord*(r: SignedPeerRecord) : RecordResult[SignedPeerRecord] = ok(r)
proc ip*(r: SignedPeerRecord): Option[array[4, byte]] =
let ma = r.data.addresses[0].address
let code = ma[0].get.protoCode()
if code.isOk and code.get == multiCodec("ip4"):
var ipbuf: array[4, byte]
let res = ma[0].get.protoArgument(ipbuf)
if res.isOk:
return some(ipbuf)
# err("Incorrect IPv4 address")
# else:
# if (?(?ma[1]).protoArgument(pbuf)) == 0:
# err("Incorrect port number")
# else:
# res.port = Port(fromBytesBE(uint16, pbuf))
# ok(res)
# else:
# else:
# err("MultiAddress must be wire address (tcp, udp or unix)")
proc udp*(r: SignedPeerRecord): Option[int] =
let ma = r.data.addresses[0].address
let code = ma[1].get.protoCode()
if code.isOk and code.get == multiCodec("udp"):
var pbuf: array[2, byte]
let res = ma[1].get.protoArgument(pbuf)
if res.isOk:
let p = fromBytesBE(uint16, pbuf)
return some(p.int)
proc fromBase64*(r: var SignedPeerRecord, s: string): bool =
## Loads SPR from base64-encoded rlp-encoded bytes, and validates the
## signature.
let bytes = Base64Url.decode(s)
r.fromBytes(bytes)
proc fromURI*(r: var SignedPeerRecord, s: string): bool =
## Loads SignedPeerRecord from its text encoding. Validates the signature.
## TODO
const prefix = "spr:"
if s.startsWith(prefix):
result = r.fromBase64(s[prefix.len .. ^1])
template fromURI*(r: var SignedPeerRecord, url: SprUri): bool =
fromURI(r, string(url))
proc toBase64*(r: SignedPeerRecord): string =
let encoded = r.encode
if encoded.isErr:
error "Failed to encode SignedPeerRecord", error = encoded.error
result = Base64Url.encode(encoded.get(@[]))
proc toURI*(r: SignedPeerRecord): string = "spr:" & r.toBase64
proc init*(T: type SignedPeerRecord, seqNum: uint64,
pk: crypto.PrivateKey,
ip: Option[ValidIpAddress],
tcpPort, udpPort: Option[Port]):
RecordResult[T] =
## Initialize a `SignedPeerRecord` with given sequence number, private key, optional
## ip address, tcp port, udp port, and optional custom k:v pairs.
##
## Can fail in case the record exceeds the `maxSprSize`.
let peerId = PeerId.init(pk).get
if tcpPort.isSome() and udpPort.isSome:
warn "Both tcp and udp ports specified, using udp in multiaddress",
tcpPort, udpPort
var
ipAddr = try: ValidIpAddress.init("127.0.0.1")
except ValueError as e:
return err ("Existing address contains invalid address: " & $e.msg).cstring
proto: IpTransportProtocol
protoPort: Port
if ip.isSome():
ipAddr = ip.get
if tcpPort.isSome():
proto = IpTransportProtocol.tcpProtocol
protoPort = tcpPort.get()
if udpPort.isSome():
proto = IpTransportProtocol.udpProtocol
protoPort = udpPort.get()
else:
if tcpPort.isSome():
proto = IpTransportProtocol.tcpProtocol
protoPort = tcpPort.get()
if udpPort.isSome():
proto = IpTransportProtocol.udpProtocol
protoPort = udpPort.get()
let ma = MultiAddress.init(ipAddr, proto, protoPort)
# if ip.isSome:
# let
# ipAddr = ip.get
# proto = ipAddr.family
# address = if proto == IPv4: ipAddr.address_v4
# else: ipAddr.address_v6
# u and udpPort.isSome
# # let ta = initTAddress(ip.get, udpPort.get)
# # echo ta
# # ma = MultiAddress.init(ta).get
# #let ma1 = MultiAddress.init("/ip4/127.0.0.1").get() #TODO
# #let ma2 = MultiAddress.init(multiCodec("udp"), udpPort.get.int).get
# #ma = ma1 & ma2
# ma = MultiAddress.init("/ip4/127.0.0.1/udp/" & $udpPort.get.int).get #TODO
# else:
# ma = MultiAddress.init()
# # echo "not implemented"
let pr = PeerRecord.init(peerId, @[ma], seqNum)
SignedPeerRecord.init(pk, pr).mapErr((e: CryptoError) => ("Failed to init SignedPeerRecord: " & $e).cstring)
proc init*(T: type SignedPeerRecord, seqNum: uint64,
pk: keys.PrivateKey,
ip: Option[ValidIpAddress],
tcpPort, udpPort: Option[Port]):
RecordResult[T] =
let kPk = pkToPk(pk).get
SignedPeerRecord.init(seqNum, kPk, ip, tcpPort, udpPort)
proc contains*(r: SignedPeerRecord, fp: (string, seq[byte])): bool =
# TODO: use FieldPair for this, but that is a bit cumbersome. Perhaps the
# `get` call can be improved to make this easier.
# let field = r.tryGet(fp[0], seq[byte])
# if field.isSome():
# if field.get() == fp[1]:
# return true
# TODO: Implement if SignedPeerRecord custom field pairs are implemented
debugEcho "`contains` is not yet implemented for SignedPeerRecords"
return false
proc `==`*(a, b: SignedPeerRecord): bool = a.data == b.data