implement more eth rpc and keystore management

This commit is contained in:
jangko 2020-07-23 14:54:32 +07:00
parent 336efdb0c3
commit f82dff64fa
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
11 changed files with 195 additions and 71 deletions

View File

@ -8,7 +8,7 @@
# those terms.
import
parseopt, strutils, macros, os, times, json, stew/[byteutils],
parseopt, strutils, macros, os, times, json, tables, stew/[byteutils],
chronos, eth/[keys, common, p2p, net/nat], chronicles, nimcrypto/hash,
eth/p2p/bootnodes, eth/p2p/rlpx_protocols/whisper_protocol,
./db/select_backend, eth/keys,
@ -121,6 +121,11 @@ type
Full
Archive
NimbusAccount* = object
privateKey*: PrivateKey
keystore*: JsonNode
unlocked*: bool
ChainConfig* = object
chainId*: uint
homesteadBlock*: BlockNumber
@ -143,7 +148,7 @@ type
NimbusConfiguration* = ref object
## Main Nimbus configuration object
dataDir*: string
keyFile*: string
keyStore*: string
prune*: PruneMode
rpc*: RpcConfiguration ## JSON-RPC configuration
net*: NetConfiguration ## Network configuration
@ -153,6 +158,7 @@ type
# You should only create one instance of the RNG per application / library
# Ref is used so that it can be shared between components
rng*: ref BrHmacDrbgContext
accounts*: Table[EthAddress, NimbusAccount]
CustomGenesisConfig = object
chainId*: uint
@ -557,11 +563,8 @@ proc processEthArguments(key, value: string): ConfigStatus =
result = Success
let config = getConfiguration()
case key.toLowerAscii()
of "keyfile":
if fileExists(value):
config.keyFile = value
else:
result = ErrorIncorrectOption
of "keystore":
config.keyStore = value
of "datadir":
config.dataDir = value
of "prune":
@ -825,16 +828,20 @@ template processArgument(processor, key, value, msg: untyped) =
proc getDefaultDataDir*(): string =
when defined(windows):
"AppData" / "Roaming" / "Nimbus" / "DB"
"AppData" / "Roaming" / "Nimbus"
elif defined(macosx):
"Library" / "Application Support" / "Nimbus" / "DB"
"Library" / "Application Support" / "Nimbus"
else:
".cache" / "nimbus" / "db"
".cache" / "nimbus"
proc getDefaultKeystoreDir*(): string =
getDefaultDataDir() / "keystore"
proc initConfiguration(): NimbusConfiguration =
## Allocates and initializes `NimbusConfiguration` with default values
result = new NimbusConfiguration
result.rng = newRng()
result.accounts = initTable[EthAddress, NimbusAccount]()
## RPC defaults
result.rpc.flags = {}
@ -853,9 +860,12 @@ proc initConfiguration(): NimbusConfiguration =
result.net.protocols = defaultProtocols
result.net.nodekey = random(PrivateKey, result.rng[])
const dataDir = getDefaultDataDir()
const
dataDir = getDefaultDataDir()
keystore = getDefaultKeystoreDir()
result.dataDir = getHomeDir() / dataDir
result.keystore = getHomeDir() / keystore
result.prune = PruneMode.Full
## Whisper defaults
@ -897,7 +907,7 @@ USAGE:
nimbus [options]
ETHEREUM OPTIONS:
--keyfile:<value> Use keyfile storage file
--keystore:<value> Directory for the keystore (default = inside the datadir)
--datadir:<value> Base directory for all blockchain-related data
--prune:<value> Blockchain prune mode(full or archive)

View File

@ -159,16 +159,21 @@ iterator getBlockTransactionHashes(self: BaseChainDB, blockHeader: BlockHeader):
for encodedTx in self.getBlockTransactionData(blockHeader.txRoot):
yield keccakHash(encodedTx)
proc getTransactionCount*(chain: BaseChainDB, blockHash: Hash256): int =
var header: BlockHeader
if chain.getBlockHeader(blockHash, header):
var trie = initHexaryTrie(chain.db, header.txRoot)
var txCount = 0
while true:
let txKey = rlp.encode(txCount)
if txKey notin trie:
break
inc txCount
proc getTransactionCount*(chain: BaseChainDB, txRoot: Hash256): int =
var trie = initHexaryTrie(chain.db, txRoot)
var txCount = 0
while true:
let txKey = rlp.encode(txCount)
if txKey notin trie:
break
inc txCount
proc getUnclesCount*(self: BaseChainDB, ommersHash: Hash256): int =
if ommersHash != EMPTY_UNCLE_HASH:
let encodedUncles = self.db.get(genericHashKey(ommersHash).toOpenArray)
if encodedUncles.len != 0:
let r = rlpFromBytes(encodedUncles)
result = r.listLen
proc getBlockBody*(self: BaseChainDB, blockHash: Hash256, output: var BlockBody): bool =
var header: BlockHeader

View File

@ -15,7 +15,7 @@ import
eth/p2p/rlpx_protocols/[eth_protocol, les_protocol, whisper_protocol],
eth/p2p/blockchain_sync, eth/net/nat, eth/p2p/peer_pool,
config, genesis, rpc/[common, p2p, debug, whisper, key_storage], p2p/chain,
eth/trie/db, metrics, metrics/chronicles_support
eth/trie/db, metrics, metrics/chronicles_support, utils
## TODO:
## * No IPv6 support
@ -36,6 +36,7 @@ type
proc start(nimbus: NimbusNode) =
var conf = getConfiguration()
conf.loadKeystoreFiles()
## logging
setLogLevel(conf.debug.logLevel)

View File

@ -96,15 +96,13 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) =
proc accountDbFromTag(tag: string, readOnly = true): ReadOnlyStateDB =
result = getAccountDb(chain.headerFromTag(tag))
#[proc getBlockBody(hash: KeccakHash): BlockBody =
if not chain.getBlockBody(hash, result):
raise newException(ValueError, "Cannot find hash")]#
server.rpc("eth_protocolVersion") do() -> string:
result = $eth_protocol.protocolVersion
server.rpc("eth_syncing") do() -> JsonNode:
## Returns SyncObject or false when not syncing.
# TODO: make sure we are not syncing
# when we reach the recent block
let numPeers = node.peerPool.connectedNodes.len
if numPeers > 0:
var sync = SyncState(
@ -137,7 +135,10 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) =
server.rpc("eth_accounts") do() -> seq[EthAddressStr]:
## Returns a list of addresses owned by client.
result = @[]
let conf = getConfiguration()
result = newSeqOfCap[EthAddressStr](conf.accounts.len)
for k in keys(conf.accounts):
result.add ethAddressStr(k)
server.rpc("eth_blockNumber") do() -> HexQuantityStr:
## Returns integer of the current block number the client is on.
@ -150,9 +151,9 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) =
## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter.
## Returns integer of the current balance in wei.
let
accountDb = accountDbFromTag(quantityTag)
accDB = accountDbFromTag(quantityTag)
address = data.toAddress
balance = accountDb.getBalance(address)
balance = accDB.getBalance(address)
result = encodeQuantity(balance)
server.rpc("eth_getStorageAt") do(data: EthAddressStr, quantity: HexQuantityStr, quantityTag: string) -> HexDataStr:
@ -163,10 +164,10 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) =
## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter.
## Returns: the value at this storage position.
let
accountDb = accountDbFromTag(quantityTag)
accDB = accountDbFromTag(quantityTag)
address = data.toAddress
key = fromHex(Uint256, quantity.string)
value = accountDb.getStorage(address, key)[0]
key = fromHex(Uint256, quantity.string)
value = accDB.getStorage(address, key)[0]
result = hexDataStr(value)
server.rpc("eth_getTransactionCount") do(data: EthAddressStr, quantityTag: string) -> HexQuantityStr:
@ -177,8 +178,8 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) =
## Returns integer of the number of transactions send from this address.
let
address = data.toAddress
accountDb = accountDbFromTag(quantityTag)
result = encodeQuantity(accountDb.getNonce(address))
accDB = accountDbFromTag(quantityTag)
result = encodeQuantity(accDB.getNonce(address))
server.rpc("eth_getBlockTransactionCountByHash") do(data: EthHashStr) -> HexQuantityStr:
## Returns the number of transactions in a block from a block matching the given block hash.
@ -186,33 +187,41 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) =
## data: hash of a block
## Returns integer of the number of transactions in this block.
let
hashData = data.toHash
txCount = chain.getTransactionCount(hashData)
blockHash = data.toHash
header = chain.getBlockHeader(blockHash)
txCount = chain.getTransactionCount(header.txRoot)
result = encodeQuantity(txCount.uint)
#[
server.rpc("eth_getBlockTransactionCountByNumber") do(quantityTag: string) -> int:
server.rpc("eth_getBlockTransactionCountByNumber") do(quantityTag: string) -> HexQuantityStr:
## Returns the number of transactions in a block matching the given block number.
##
## data: integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter.
## Returns integer of the number of transactions in this block.
let header = chain.headerFromTag(quantityTag)
result = getBlockBody(header.hash).transactions.len
let
header = chain.headerFromTag(quantityTag)
txCount = chain.getTransactionCount(header.txRoot)
result = encodeQuantity(txCount.uint)
server.rpc("eth_getUncleCountByBlockHash") do(data: EthHashStr) -> int:
server.rpc("eth_getUncleCountByBlockHash") do(data: EthHashStr) -> HexQuantityStr:
## Returns the number of uncles in a block from a block matching the given block hash.
##
## data: hash of a block.
## Returns integer of the number of uncles in this block.
var hashData = data.toHash
result = getBlockBody(hashData).uncles.len
let
blockHash = data.toHash
header = chain.getBlockHeader(blockHash)
unclesCount = chain.getUnclesCount(header.ommersHash)
result = encodeQuantity(unclesCount.uint)
server.rpc("eth_getUncleCountByBlockNumber") do(quantityTag: string) -> int:
server.rpc("eth_getUncleCountByBlockNumber") do(quantityTag: string) -> HexQuantityStr:
## Returns the number of uncles in a block from a block matching the given block number.
##
## quantityTag: integer of a block number, or the string "latest", "earliest" or "pending", see the default block parameter.
## Returns integer of uncles in this block.
let header = chain.headerFromTag(quantityTag)
result = getBlockBody(header.hash).uncles.len
let
header = chain.headerFromTag(quantityTag)
unclesCount = chain.getUnclesCount(header.ommersHash)
result = encodeQuantity(unclesCount.uint)
server.rpc("eth_getCode") do(data: EthAddressStr, quantityTag: string) -> HexDataStr:
## Returns code at a given address.
@ -221,14 +230,13 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) =
## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter.
## Returns the code from the given address.
let
accountDb = accountDbFromTag(quantityTag)
address = toAddress(data)
storage = accountDb.getCode(address)
# Easier to return the string manually here rather than expect ByteRange to be marshalled
result = byteutils.toHex(storage).HexDataStr
accDB = accountDbFromTag(quantityTag)
address = data.toAddress
storage = accDB.getCode(address)
result = hexDataStr(storage)
template sign(privateKey: PrivateKey, message: string): string =
# TODO: Is message length encoded as bytes or characters?
# message length encoded as ASCII representation of decimal
let msgData = "\x19Ethereum Signed Message:\n" & $message.len & message
$sign(privateKey, msgData.toBytes())
@ -241,9 +249,16 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) =
## data: address.
## message: message to sign.
## Returns signature.
var privateKey: PrivateKey # TODO: Get from key store
result = ("0x" & sign(privateKey, message.string)).HexDataStr
let
address = data.toAddress
conf = getConfiguration()
acc = conf.getAccount(address).tryGet()
msg = hexToSeqByte(message.string)
if not acc.unlocked:
raise newException(ValueError, "Account locked, please unlock it first")
result = ("0x" & sign(acc.privateKey, cast[string](msg))).HexDataStr
#[
# proc setupTransaction(send: EthSend): Transaction =
# let
# source = send.source.toAddress

View File

@ -1,4 +1,6 @@
import eth/trie/db, eth/[trie, rlp, common], nimcrypto
import
os, tables, json, ./config, stew/[results, byteutils],
eth/trie/db, eth/[trie, rlp, common, keyfile], nimcrypto
export nimcrypto.`$`
@ -50,3 +52,40 @@ proc crc32*(crc: uint32, buf: openArray[byte]): uint32 =
crcu32 = (crcu32 shr 4) xor kcrc32[int((crcu32 and 0xF) xor (uint32(b) shr 4'u32))]
result = not crcu32
proc loadKeystoreFiles*(conf: NimbusConfiguration): Result[void, string] =
try:
createDir(conf.keyStore)
except OSError, IOError:
return err("keystore: cannot create directory")
for filename in walkDirRec(conf.keyStore):
try:
var data = json.parseFile(filename)
let address: EthAddress = hexToByteArray[20](data["address"].getStr())
conf.accounts[address] = NimbusAccount(keystore: data, unlocked: false)
except JsonParsingError:
return err("keystore: json parsing error " & filename)
except ValueError:
return err("keystore: data parsing error")
except Exception: # json raises Exception
return err("keystore: " & getCurrentExceptionMsg())
result = ok()
proc getAccount*(conf: NimbusConfiguration, address: EthAddress): Result[NimbusAccount, string] =
conf.accounts.withValue(address, val) do:
result = ok(val[])
do:
result = err("getAccount: not available " & address.toHex)
proc unlockAccount*(conf: NimbusConfiguration, address: EthAddress, password: string): Result[void, string] =
var acc = conf.getAccount(address).tryGet()
let res = decodeKeyFileJson(acc.keystore, password)
if res.isOk:
acc.privateKey = res.get()
acc.unlocked = true
conf.accounts[address] = acc
result = ok()
else:
result = err($res.error)

View File

@ -19,7 +19,7 @@ proc prefixHex*(x: Hash256): string =
"0x" & toLowerAscii($x)
proc prefixHex*(x: int64 | uint64 | byte | int): string =
encodeQuantity(x.uint64).toLowerAscii
toLowerAscii(encodeQuantity(x.uint64).string)
proc prefixHex*(x: openArray[byte]): string =
"0x" & toHex(x, true)

View File

@ -0,0 +1 @@
{"password":"applebanana","address":"0e69cde81b1aa07a45c32c6cd85d67229d36bb1b","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"7ead94c4f16f3bf31e1cdffd44227403"},"ciphertext":"f023cbdd0ebb26bea0342e2c5719da6a0499e9f675fff3d3c4beb84e1071174f","kdf":"pbkdf2","kdfparams":{"dklen":32,"c":1000000,"prf":"hmac-sha256","salt":"91edacb4f74698e6516062b230064385"},"mac":"f37290005290f56c71cce329f61945d9b2a1c719bc84e966d30c8c395477d625"},"id":"29014ab9-38c0-42fa-b5cf-f049d8568a41","version":3}

View File

@ -0,0 +1 @@
{"password":"bananamonkey","address":"597176e9a64aad0845d83afdaf698fbeff77703b","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"5ad7a73aa1b46b336bd9640a8cf19436"},"ciphertext":"068c556204f86b9e0d6670e1e80f8d34dfb55416b605c0b9f9d833ba962cc64c","kdf":"pbkdf2","kdfparams":{"dklen":32,"c":1000000,"prf":"hmac-sha256","salt":"aea9c4e3dd598d41926268ed2358965e"},"mac":"30cd4c38da212a85aa277c60ee8be7ae4e9cceea75cedeae516827d339d90550"},"id":"7667da26-1e5b-4281-98a3-c9aad03e8491","version":3}

View File

@ -0,0 +1 @@
{"password":"monkeyelephant","address":"a3b2222afa5c987da6ef773fde8d01b9f23d481f","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"3ca1d5c5151fba8281f8880ece920740"},"ciphertext":"ec8b7626f3494605e4b66a889c3dbf9bae4d8fe249718d25f010441af3cf6c61","kdf":"pbkdf2","kdfparams":{"dklen":32,"c":1000000,"prf":"hmac-sha256","salt":"364a67e8bc0a782d715832946662850c"},"mac":"7001a9fb0f63db2b7538bab2a34d2103c67035f4754a3bcf4760359f530d5c21"},"id":"5098841b-9060-4ce7-baf7-c748b12d79c9","version":3}

View File

@ -14,7 +14,7 @@ import
../../nimbus/rpc/hexstrings, ../../nimbus/rpc/rpc_types
proc web3_clientVersion(): string
proc web3_sha3(data: string): string
proc web3_sha3(data: HexDataStr): string
proc net_version(): string
proc net_peerCount(): HexQuantityStr
proc net_listening(): bool
@ -30,9 +30,9 @@ proc eth_getBalance(data: EthAddressStr, quantityTag: string): HexQuantityStr
proc eth_getStorageAt(data: EthAddressStr, quantity: HexQuantityStr, quantityTag: string): seq[byte]
proc eth_getTransactionCount(data: EthAddressStr, quantityTag: string): HexQuantityStr
proc eth_getBlockTransactionCountByHash(data: Hash256): HexQuantityStr
proc eth_getBlockTransactionCountByNumber(quantityTag: string)
proc eth_getUncleCountByBlockHash(data: array[32, byte])
proc eth_getUncleCountByBlockNumber(quantityTag: string)
proc eth_getBlockTransactionCountByNumber(quantityTag: string): HexQuantityStr
proc eth_getUncleCountByBlockHash(data: Hash256): HexQuantityStr
proc eth_getUncleCountByBlockNumber(quantityTag: string): HexQuantityStr
proc eth_getCode(data: EthAddressStr, quantityTag: string): HexDataStr
proc eth_sign(data:EthAddressStr, message: HexDataStr): HexDataStr
#proc eth_sendRawTransaction(data: string, quantityTag: int): UInt256

View File

@ -6,11 +6,12 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
unittest, json, strformat, strutils, options, tables, nimcrypto, stew/byteutils,
unittest, json, strformat, strutils, options, tables, os,
nimcrypto, stew/byteutils,
json_rpc/[rpcserver, rpcclient], eth/common as eth_common,
eth/[rlp, keys], eth/trie/db, eth/p2p/rlpx_protocols/eth_protocol,
../nimbus/rpc/[common, p2p, hexstrings, rpc_types],
../nimbus/[constants, vm_state, config, genesis],
../nimbus/[constants, vm_state, config, genesis, utils],
../nimbus/db/[accounts_cache, db_chain, storage_types],
../nimbus/p2p/chain,
./rpcclient/test_hexstrings, ./test_helpers
@ -29,10 +30,6 @@ template sourceDir: string = currentSourcePath.rsplit(DirSep, 1)[0]
const sigPath = &"{sourceDir}{DirSep}rpcclient{DirSep}ethcallsigs.nim"
createRpcSigs(RpcSocketClient, sigPath)
proc toEthAddressStr(address: EthAddress): EthAddressStr =
result = ("0x" & address.toHex).ethAddressStr
proc doTests {.async.} =
# TODO: Include other transports such as Http
var ethNode = setupEthNode(eth)
@ -47,7 +44,25 @@ proc doTests {.async.} =
let
balance = 100.u256
address: EthAddress = hexToByteArray[20]("0x0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6")
signer: EthAddress = hexToByteArray[20]("0x0e69cde81b1aa07a45c32c6cd85d67229d36bb1b")
ks2: EthAddress = hexToByteArray[20]("0xa3b2222afa5c987da6ef773fde8d01b9f23d481f")
ks3: EthAddress = hexToByteArray[20]("0x597176e9a64aad0845d83afdaf698fbeff77703b")
conf = getConfiguration()
conf.keyStore = "tests" / "keystore"
let res = conf.loadKeystoreFiles()
if res.isErr:
debugEcho res.error
doAssert(res.isOk)
let acc1 = conf.getAccount(signer).tryGet()
let unlock = conf.unlockAccount(signer, acc1.keystore["password"].getStr())
if unlock.isErr:
debugEcho unlock.error
doAssert(unlock.isOk)
defaultGenesisBlockForNetwork(conf.net.networkId.toPublicNetwork()).commit(chain)
state.mutateStateDB:
db.setBalance(address, balance)
@ -73,10 +88,10 @@ proc doTests {.async.} =
test "web3_sha3":
expect ValueError:
discard await client.web3_sha3(NimbusName)
discard await client.web3_sha3(NimbusName.HexDataStr)
let data = "0x" & byteutils.toHex(NimbusName.toOpenArrayByte(0, NimbusName.len-1))
let res = await client.web3_sha3(data)
let res = await client.web3_sha3(data.hexDataStr)
let rawdata = nimcrypto.fromHex(data[2 .. ^1])
let hash = "0x" & $keccak_256.digest(rawdata)
check hash == res
@ -135,8 +150,9 @@ proc doTests {.async.} =
test "eth_accounts":
let res = await client.eth_accounts()
# we do not own any accounts, yet
check res.len == 0
check signer.ethAddressStr in res
check ks2.ethAddressStr in res
check ks3.ethAddressStr in res
test "eth_blockNumber":
let res = await client.eth_blockNumber()
@ -163,6 +179,41 @@ proc doTests {.async.} =
let res = await client.eth_getBlockTransactionCountByHash(hash)
check res.string == "0x0"
test "eth_getBlockTransactionCountByNumber":
let res = await client.eth_getBlockTransactionCountByNumber("0x0")
check res.string == "0x0"
test "eth_getUncleCountByBlockHash":
let hash = chain.getBlockHash(0.toBlockNumber)
let res = await client.eth_getUncleCountByBlockHash(hash)
check res.string == "0x0"
test "eth_getUncleCountByBlockNumber":
let res = await client.eth_getUncleCountByBlockNumber("0x0")
check res.string == "0x0"
test "eth_getCode":
let res = await client.eth_getCode(ethAddressStr("0xfff7ac99c8e4feb60c9750054bdc14ce1857f181"), "0x0")
check res.string == "0x"
test "eth_sign":
let msg = "hello world"
let msgHex = hexDataStr(msg.toOpenArrayByte(0, msg.len-1))
expect ValueError:
discard await client.eth_sign(ethAddressStr(ks2), msgHex)
let res = await client.eth_sign(ethAddressStr(signer), msgHex)
let sig = Signature.fromHex(res.string).tryGet()
# now let us try to verify signature
let msgData = "\x19Ethereum Signed Message:\n" & $msg.len & msg
let msgDataHex = hexDataStr(msgData.toOpenArrayByte(0, msgData.len-1))
let sha3Data = await client.web3_sha3(msgDataHex)
let msgHash = hexToByteArray[32](sha3Data)
let pubkey = recover(sig, SkMessage(msgHash)).tryGet()
let recoveredAddr = pubkey.toCanonicalAddress()
check recoveredAddr == signer # verified
#test "eth_call":
# let