From 0ecbfcec9fb0a6fa978494c651430b1f9735740a Mon Sep 17 00:00:00 2001 From: Dmitriy Ryajov Date: Tue, 1 Nov 2022 18:58:41 -0600 Subject: [PATCH] setup and persist private key (#292) * setup and persist private key * return dht record spr * helper to remap multiaddr ip and port * set/update discovery and announce addrs * add nat and discovery IPs * allow for announce and DHT addresses separatelly * update tests * check for nat or discoveryIp * fix integration tests * misc align * don't share data dirs and and set bootstrap node * add log scope * remap announceAddrs after node start * simplify discovery initialization * make nat and disc-ip required * add log scope don't init dht spr in constructor * bump dht * dissallow `0.0.0.0` for `--nat` --- codex.nim | 20 +++++- codex/codex.nim | 85 +++++++++++------------ codex/conf.nim | 26 ++++--- codex/discovery.nim | 82 ++++++++++++---------- codex/rest/api.nim | 23 +++--- codex/utils/addrutils.nim | 40 +++++++++++ codex/utils/keyutils.nim | 50 +++++++++++++ tests/codex/helpers/nodeutils.nim | 5 +- tests/codex/storageproofs/testnetwork.nim | 4 +- tests/codex/testnode.nim | 5 +- tests/testIntegration.nim | 43 ++++++++---- vendor/nim-libp2p-dht | 2 +- 12 files changed, 265 insertions(+), 120 deletions(-) create mode 100644 codex/utils/addrutils.nim create mode 100644 codex/utils/keyutils.nim diff --git a/codex.nim b/codex.nim index 01ed5459..c3ba4087 100644 --- a/codex.nim +++ b/codex.nim @@ -14,14 +14,13 @@ import pkg/libp2p import ./codex/conf import ./codex/codex +import ./codex/utils/keyutils export codex, conf, libp2p, chronos, chronicles when isMainModule: import std/os - import pkg/confutils/defs - import ./codex/utils/fileutils logScope: @@ -39,6 +38,13 @@ when isMainModule: case config.cmd: of StartUpCommand.noCommand: + if config.nat == ValidIpAddress.init(IPv4_any()): + error "`--nat` cannot be set to the any (`0.0.0.0`) address" + quit QuitFailure + + if config.nat == ValidIpAddress.init("127.0.0.1"): + warn "`--nat` is set to local loopback, your node wont be properly announce over the DHT" + if not(checkAndCreateDataDir((config.dataDir).string)): # We are unable to access/create data folder or data folder's # permissions are insecure. @@ -53,7 +59,15 @@ when isMainModule: trace "Repo dir initialized", dir = config.dataDir / "repo" - let server = CodexServer.new(config) + let + keyPath = + if isAbsolute(string config.netPrivKeyFile): + string config.netPrivKeyFile + else: + string config.dataDir / string config.netPrivKeyFile + + privateKey = setupKey(keyPath).expect("Should setup private key!") + server = CodexServer.new(config, privateKey) ## Ctrl+C handling proc controlCHandler() {.noconv.} = diff --git a/codex/codex.nim b/codex/codex.nim index be85bf59..b3753b44 100644 --- a/codex/codex.nim +++ b/codex/codex.nim @@ -31,6 +31,11 @@ import ./utils/fileutils import ./erasure import ./discovery import ./contracts +import ./utils/keyutils +import ./utils/addrutils + +logScope: + topics = "codex node" type CodexServer* = ref object @@ -39,10 +44,37 @@ type restServer: RestServerRef codexNode: CodexNodeRef + CodexPrivateKey* = libp2p.PrivateKey # alias + proc start*(s: CodexServer) {.async.} = s.restServer.start() await s.codexNode.start() + let + # TODO: Can't define this as constants, pity + natIpPart = MultiAddress.init("/ip4/" & $s.config.nat & "/") + .expect("Should create multiaddress") + anyAddrIp = MultiAddress.init("/ip4/0.0.0.0/") + .expect("Should create multiaddress") + loopBackAddrIp = MultiAddress.init("/ip4/127.0.0.1/") + .expect("Should create multiaddress") + + # announce addresses should be set to bound addresses, + # but the IP should be mapped to the provided nat ip + announceAddrs = s.codexNode.switch.peerInfo.addrs.mapIt: + block: + let + listenIPPart = it[multiCodec("ip4")].expect("Should get IP") + + if listenIPPart == anyAddrIp or + (listenIPPart == loopBackAddrIp and natIpPart != loopBackAddrIp): + it.remapAddr(s.config.nat.some) + else: + it + + s.codexNode.discovery.updateAnnounceRecord(announceAddrs) + s.codexNode.discovery.updateDhtRecord(s.config.nat, s.config.discoveryPort) + s.runHandle = newFuture[void]() await s.runHandle @@ -67,39 +99,9 @@ proc new(_: type ContractInteractions, config: CodexConf): ?ContractInteractions else: ContractInteractions.new(config.ethProvider, account) -proc new*(T: type CodexServer, config: CodexConf): T = +proc new*(T: type CodexServer, config: CodexConf, privateKey: CodexPrivateKey): T = - const SafePermissions = {UserRead, UserWrite} let - privateKey = - if config.netPrivKeyFile == "random": - PrivateKey.random(Rng.instance()[]).get() - else: - let path = - if config.netPrivKeyFile.isAbsolute: - config.netPrivKeyFile - else: - config.dataDir / config.netPrivKeyFile - - if path.fileAccessible({AccessFlags.Find}): - info "Found a network private key" - - if path.getPermissionsSet().get() != SafePermissions: - warn "The network private key file is not safe, aborting" - quit QuitFailure - - PrivateKey.init(path.readAllBytes().expect("accessible private key file")). - expect("valid private key file") - else: - info "Creating a private key and saving it" - let - res = PrivateKey.random(Rng.instance()[]).get() - bytes = res.getBytes().get() - - path.writeFile(bytes, SafePermissions.toInt()).expect("writing private key file") - - PrivateKey.init(bytes).expect("valid key bytes") - switch = SwitchBuilder .new() .withPrivateKey(privateKey) @@ -124,16 +126,11 @@ proc new*(T: type CodexServer, config: CodexConf): T = config.dataDir / "dht") .expect("Should not fail!")) - announceAddrs = - if config.announceAddrs.len <= 0: - config.announceAddrs - else: - config.listenAddrs - - blockDiscovery = Discovery.new( + discovery = Discovery.new( switch.peerInfo.privateKey, - announceAddrs = config.announceAddrs, - discoveryPort = config.discoveryPort, + announceAddrs = config.listenAddrs, + bindIp = config.discoveryIp, + bindPort = config.discoveryPort, bootstrapNodes = config.bootstrapNodes, store = discoveryStore) @@ -150,18 +147,18 @@ proc new*(T: type CodexServer, config: CodexConf): T = localStore = FSStore.new(repoDir, cache = cache) peerStore = PeerCtxStore.new() pendingBlocks = PendingBlocksManager.new() - discovery = DiscoveryEngine.new(localStore, peerStore, network, blockDiscovery, pendingBlocks) - engine = BlockExcEngine.new(localStore, wallet, network, discovery, peerStore, pendingBlocks) + blockDiscovery = DiscoveryEngine.new(localStore, peerStore, network, discovery, pendingBlocks) + engine = BlockExcEngine.new(localStore, wallet, network, blockDiscovery, peerStore, pendingBlocks) store = NetworkStore.new(engine, localStore) erasure = Erasure.new(store, leoEncoderProvider, leoDecoderProvider) contracts = ContractInteractions.new(config) - codexNode = CodexNodeRef.new(switch, store, engine, erasure, blockDiscovery, contracts) + codexNode = CodexNodeRef.new(switch, store, engine, erasure, discovery, contracts) restServer = RestServerRef.new( codexNode.initRestApi(config), initTAddress("127.0.0.1" , config.apiPort), bufferSize = (1024 * 64), maxRequestBodySize = int.high) - .tryGet() + .expect("Should start rest server!") switch.mount(network) T( diff --git a/codex/conf.nim b/codex/conf.nim index 1cde715b..9c1a966f 100644 --- a/codex/conf.nim +++ b/codex/conf.nim @@ -95,22 +95,29 @@ type abbr: "i" name: "listen-addrs" }: seq[MultiAddress] - announceAddrs* {. - desc: "Multi Addresses to announce behind a NAT" - defaultValue: @[] - defaultValueDesc: "" + nat* {. + # TODO: change this once we integrate nat support + desc: "IP Addresses to announce behind a NAT" + defaultValue: ValidIpAddress.init("127.0.0.1") + defaultValueDesc: "127.0.0.1" abbr: "a" - name: "announce-addrs" }: seq[MultiAddress] + name: "nat" }: ValidIpAddress + + discoveryIp* {. + desc: "Discovery listen address" + defaultValue: ValidIpAddress.init(IPv4_any()) + defaultValueDesc: "0.0.0.0" + name: "disc-ip" }: ValidIpAddress discoveryPort* {. - desc: "Specify the discovery (UDP) port" + desc: "Discovery (UDP) port" defaultValue: Port(8090) defaultValueDesc: "8090" - name: "udp-port" }: Port + name: "disc-port" }: Port netPrivKeyFile* {. - desc: "Source of network (secp256k1) private key file (random|)" - defaultValue: "random" + desc: "Source of network (secp256k1) private key file path or name" + defaultValue: "key" name: "net-privkey" }: string bootstrapNodes* {. @@ -183,7 +190,6 @@ const "Codex build " & codexVersion & "\p" & nimBanner - proc defaultDataDir*(): string = let dataDir = when defined(windows): "AppData" / "Roaming" / "Codex" diff --git a/codex/discovery.nim b/codex/discovery.nim index 4f6a424d..94ae12d5 100644 --- a/codex/discovery.nim +++ b/codex/discovery.nim @@ -22,7 +22,6 @@ import pkg/libp2pdht/discv5/protocol as discv5 import ./rng import ./errors -import ./formats export discv5 @@ -30,12 +29,18 @@ export discv5 # deprecated, this could have been implemented # much more elegantly. +logScope: + topics = "codex discovery" + type Discovery* = ref object of RootObj - protocol: discv5.Protocol - key: PrivateKey - announceAddrs: seq[MultiAddress] - record: SignedPeerRecord + protocol: discv5.Protocol # dht protocol + key: PrivateKey # private key + peerId: PeerId # the peer id of the local node + announceAddrs: seq[MultiAddress] # addresses announced as part of the provider records + providerRecord*: ?SignedPeerRecord # record to advertice node connection information, this carry any + # address that the node can be connected on + dhtRecord*: ?SignedPeerRecord # record to advertice DHT connection information proc toNodeId*(cid: Cid): NodeId = ## Cid to discovery id @@ -57,9 +62,9 @@ proc findPeer*( return if node.isSome(): - some(node.get().record.data) + node.get().record.data.some else: - none(PeerRecord) + PeerRecord.none method find*( d: Discovery, @@ -81,7 +86,7 @@ method provide*(d: Discovery, cid: Cid) {.async, base.} = trace "Providing block", cid let nodes = await d.protocol.addProvider( - cid.toNodeId(), d.record) + cid.toNodeId(), d.providerRecord.get) if nodes.len <= 0: trace "Couldn't provide to any nodes!" @@ -116,7 +121,7 @@ method provide*(d: Discovery, host: ca.Address) {.async, base.} = trace "Providing host", host = $host let nodes = await d.protocol.addProvider( - host.toNodeId(), d.record) + host.toNodeId(), d.providerRecord.get) if nodes.len > 0: trace "Provided to nodes", nodes = nodes.len @@ -127,20 +132,32 @@ method removeProvider*(d: Discovery, peerId: PeerId): Future[void] {.base.} = trace "Removing provider", peerId d.protocol.removeProvidersLocal(peerId) -proc updateRecord*(d: Discovery, addrs: openArray[MultiAddress]) = +proc updateAnnounceRecord*(d: Discovery, addrs: openArray[MultiAddress]) = ## Update providers record ## d.announceAddrs = @addrs - d.record = SignedPeerRecord.init( - d.key, - PeerRecord.init( - PeerId.init(d.key).expect("Should construct PeerId"), - d.announceAddrs)).expect("Should construct signed record") + + trace "Updating announce record", addrs = d.announceAddrs + d.providerRecord = SignedPeerRecord.init( + d.key, PeerRecord.init(d.peerId, d.announceAddrs)) + .expect("Should construct signed record").some if not d.protocol.isNil: - d.protocol.updateRecord(d.record.some) - .expect("should update SPR") + d.protocol.updateRecord(d.providerRecord) + .expect("Should update SPR") + +proc updateDhtRecord*(d: Discovery, ip: ValidIpAddress, port: Port) = + ## Update providers record + ## + + trace "Updating Dht record", ip, port = $port + d.dhtRecord = SignedPeerRecord.init( + d.key, PeerRecord.init(d.peerId, @[ + MultiAddress.init( + ip, + IpTransportProtocol.udpProtocol, + port)])).expect("Should construct signed record").some proc start*(d: Discovery) {.async.} = d.protocol.open() @@ -152,34 +169,25 @@ proc stop*(d: Discovery) {.async.} = proc new*( T: type Discovery, key: PrivateKey, - discoveryIp = IPv4_any(), - discoveryPort = 0.Port, - announceAddrs: openArray[MultiAddress] = [], + bindIp = ValidIpAddress.init(IPv4_any()), + bindPort = 0.Port, + announceAddrs: openArray[MultiAddress], bootstrapNodes: openArray[SignedPeerRecord] = [], store: Datastore = SQLiteDatastore.new(Memory) - .expect("Should not fail!")): T = - - let - announceAddrs = - if announceAddrs.len <= 0: - @[ - MultiAddress.init( - ValidIpAddress.init(discoveryIp), - IpTransportProtocol.tcpProtocol, - discoveryPort)] - else: - @announceAddrs + .expect("Should not fail!")): T = var - self = T(key: key) + self = T( + key: key, + peerId: PeerId.init(key).expect("Should construct PeerId")) - self.updateRecord(announceAddrs) + self.updateAnnounceRecord(announceAddrs) self.protocol = newProtocol( key, - bindIp = discoveryIp, - bindPort = discoveryPort, - record = self.record, + bindIp = bindIp.toNormalIp, + bindPort = bindPort, + record = self.providerRecord.get, bootstrapRecords = bootstrapNodes, rng = Rng.instance(), providers = ProvidersManager.new(store)) diff --git a/codex/rest/api.nim b/codex/rest/api.nim index 0f98d71d..f5fef243 100644 --- a/codex/rest/api.nim +++ b/codex/rest/api.nim @@ -195,9 +195,9 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter = "/api/codex/v1/storage/request/{cid}") do (cid: Cid) -> RestApiResponse: ## Create a request for storage ## - ## cid - the cid of a previously uploaded dataset - ## duration - the duration of the contract - ## reward - the maximum price the client is willing to pay + ## cid - the cid of a previously uploaded dataset + ## duration - the duration of the contract + ## reward - the maximum price the client is willing to pay without cid =? cid.tryGet.catch, error: return RestApiResponse.error(Http400, error.msg) @@ -265,12 +265,17 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter = ## Print rudimentary node information ## - let json = %*{ - "id": $node.switch.peerInfo.peerId, - "addrs": node.switch.peerInfo.addrs.mapIt( $it ), - "repo": $conf.dataDir, - "spr": node.switch.peerInfo.signedPeerRecord.toURI - } + let + json = %*{ + "id": $node.switch.peerInfo.peerId, + "addrs": node.switch.peerInfo.addrs.mapIt( $it ), + "repo": $conf.dataDir, + "spr": + if node.discovery.dhtRecord.isSome: + node.discovery.dhtRecord.get.toURI + else: + "" + } return RestApiResponse.response($json) diff --git a/codex/utils/addrutils.nim b/codex/utils/addrutils.nim new file mode 100644 index 00000000..6ae00e39 --- /dev/null +++ b/codex/utils/addrutils.nim @@ -0,0 +1,40 @@ +## Nim-Codex +## Copyright (c) 2022 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +import pkg/upraises +push: {.upraises: [].} + +import std/strutils +import std/options + +import pkg/libp2p +import pkg/stew/shims/net + +func remapAddr*( + address: MultiAddress, + ip: Option[ValidIpAddress] = ValidIpAddress.none, + port: Option[Port] = Port.none): MultiAddress = + ## Remap addresses to new IP and/or Port + ## + + var + parts = ($address).split("/") + + parts[2] = if ip.isSome: + $ip.get + else: + parts[2] + + parts[4] = if port.isSome: + $port.get + else: + parts[4] + + MultiAddress.init(parts.join("/")) + .expect("Should construct multiaddress") diff --git a/codex/utils/keyutils.nim b/codex/utils/keyutils.nim new file mode 100644 index 00000000..14d59bb2 --- /dev/null +++ b/codex/utils/keyutils.nim @@ -0,0 +1,50 @@ + +## Nim-Codex +## Copyright (c) 2022 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +import pkg/upraises +push: {.upraises: [].} + +import std/os + +import pkg/chronicles +import pkg/questionable/results +import pkg/libp2p + +import ./fileutils +import ../conf +import ../errors +import ../rng + +const + SafePermissions = {UserRead, UserWrite} + +type + CodexKeyError = object of CodexError + CodexKeyUnsafeError = object of CodexKeyError + +proc setupKey*(path: string): ?!PrivateKey = + if not path.fileAccessible({AccessFlags.Find}): + info "Creating a private key and saving it" + let + res = ? PrivateKey.random(Rng.instance()[]).mapFailure(CodexKeyError) + bytes = ? res.getBytes().mapFailure(CodexKeyError) + + ? path.writeFile(bytes, SafePermissions.toInt()).mapFailure(CodexKeyError) + return PrivateKey.init(bytes).mapFailure(CodexKeyError) + + info "Found a network private key" + if path.getPermissionsSet().get() != SafePermissions: + warn "The network private key file is not safe, aborting" + return failure newException( + CodexKeyUnsafeError, "The network private key file is not safe") + + return PrivateKey.init( + ? path.readAllBytes().mapFailure(CodexKeyError)) + .mapFailure(CodexKeyError) diff --git a/tests/codex/helpers/nodeutils.nim b/tests/codex/helpers/nodeutils.nim index 978f20c5..081de7f9 100644 --- a/tests/codex/helpers/nodeutils.nim +++ b/tests/codex/helpers/nodeutils.nim @@ -29,7 +29,10 @@ proc generateNodes*( for i in 0..