From bb89a296dde453a3096c53b78ab6de7de91ba23f Mon Sep 17 00:00:00 2001 From: jangko Date: Fri, 24 Jul 2020 19:44:36 +0700 Subject: [PATCH] implement eth_signTransaction, eth_sendTransaction, eth_sendRawTransaction, eth_call --- nimbus/rpc/hexstrings.nim | 3 + nimbus/rpc/p2p.nim | 102 +++++++++----------------------- nimbus/rpc/rpc_types.nim | 12 ++-- nimbus/rpc/rpc_utils.nim | 65 +++++++++++++++++++- tests/rpcclient/ethcallsigs.nim | 22 +++---- tests/test_rpc.nim | 35 ++++++----- 6 files changed, 132 insertions(+), 107 deletions(-) diff --git a/nimbus/rpc/hexstrings.nim b/nimbus/rpc/hexstrings.nim index 1667277f9..cd0905027 100644 --- a/nimbus/rpc/hexstrings.nim +++ b/nimbus/rpc/hexstrings.nim @@ -188,6 +188,9 @@ proc ethHashStr*(value: string): EthHashStr {.inline.} = value.validateHashStr result = value.EthHashStr +func ethHashStr*(value: Hash256): EthHashStr {.inline.} = + result = EthHashStr("0x" & value.data.toHex) + # Converters for use in RPC import json diff --git a/nimbus/rpc/p2p.nim b/nimbus/rpc/p2p.nim index f09498b95..aefd577c3 100644 --- a/nimbus/rpc/p2p.nim +++ b/nimbus/rpc/p2p.nim @@ -13,8 +13,7 @@ import eth/[common, keys, rlp, p2p], nimcrypto, eth/p2p/rlpx_protocols/eth_protocol, ../transaction, ../config, ../vm_state, ../constants, ../vm_types, - ../vm_state_transactions, ../utils, - ../db/[db_chain, state_db], + ../utils, ../db/[db_chain, state_db], rpc_types, rpc_utils, ../vm/[message, computation], ../vm/interpreter/vm_forks @@ -260,6 +259,8 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) = result = ("0x" & sign(acc.privateKey, cast[string](msg))).HexDataStr server.rpc("eth_signTransaction") do(data: TxSend) -> HexDataStr: + ## Signs a transaction that can be submitted to the network at a later time using with + ## eth_sendRawTransaction let address = data.source.toAddress conf = getConfiguration() @@ -273,62 +274,41 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) = tx = unsignedTx(data, chain, accDB.getNonce(address) + 1) signedTx = signTransaction(tx, chain, acc.privateKey) rlpTx = rlp.encode(signedTx) - - result = hexDataStr(rlpTx) -#[ - # proc setupTransaction(send: EthSend): Transaction = - # let - # source = send.source.toAddress - # destination = send.to.toAddress - # data = nimcrypto.utils.fromHex(send.data.string) - # contractCreation = false # TODO: Check if has code - # v = 0.byte # TODO - # r = 0.u256 - # s = 0.u256 - # result = initTransaction(send.nonce, send.gasPrice, send.gas, destination, send.value, data, v, r, s, contractCreation) - server.rpc("eth_sendTransaction") do(obj: EthSend) -> HexDataStr: + result = hexDataStr(rlpTx) + + server.rpc("eth_sendTransaction") do(data: TxSend) -> EthHashStr: ## Creates new message call transaction or a contract creation, if the data field contains code. ## ## obj: the transaction object. ## Returns the transaction hash, or the zero hash if the transaction is not yet available. ## Note: Use eth_getTransactionReceipt to get the contract address, after the transaction was mined, when you created a contract. # TODO: Relies on pending pool implementation - discard + let + address = data.source.toAddress + conf = getConfiguration() + acc = conf.getAccount(address).tryGet() - server.rpc("eth_sendRawTransaction") do(data: string, quantityTag: int) -> HexDataStr: + if not acc.unlocked: + raise newException(ValueError, "Account locked, please unlock it first") + + let + accDB = accountDbFromTag("latest") + tx = unsignedTx(data, chain, accDB.getNonce(address) + 1) + signedTx = signTransaction(tx, chain, acc.privateKey) + rlpTx = rlp.encode(signedTx) + + result = keccak_256.digest(rlpTx).ethHashStr + + server.rpc("eth_sendRawTransaction") do(data: HexDataStr) -> EthHashStr: ## Creates new message call transaction or a contract creation for signed transactions. ## ## data: the signed transaction data. ## Returns the transaction hash, or the zero hash if the transaction is not yet available. ## Note: Use eth_getTransactionReceipt to get the contract address, after the transaction was mined, when you created a contract. # TODO: Relies on pending pool implementation - discard - - proc setupComputation(vmState: BaseVMState, - value: UInt256, data: seq[byte], - sender, destination: EthAddress, - gasLimit, gasPrice: GasInt, - contractCreation: bool): Computation = - let - # Handle optional defaults. - message = Message( - kind: if contractCreation: evmcCreate else: evmcCall, - depth: 0, - gas: gasLimit, - sender: sender, - contractAddress: destination, - codeAddress: CREATE_CONTRACT_ADDRESS, - value: value, - data: data - ) - - vmState.setupTxContext( - origin = sender, - gasPrice = gasPrice - ) - - result = newComputation(vmState, message) + let rlpBytes = hexToSeqByte(data.string) + result = keccak_256.digest(rlpBytes).ethHashStr server.rpc("eth_call") do(call: EthCall, quantityTag: string) -> HexDataStr: ## Executes a new message call immediately without creating a transaction on the block chain. @@ -336,36 +316,12 @@ proc setupEthRpc*(node: EthereumNode, chain: BaseChainDB , server: RpcServer) = ## call: the transaction call object. ## quantityTag: integer block number, or the string "latest", "earliest" or "pending", see the default block parameter. ## Returns the return value of executed contract. - let header = headerFromTag(chain, quantityTag) - var - # TODO: header.stateRoot to prevStateRoot - vmState = newBaseVMState(header.stateRoot, header, chain) - gasLimit = - if call.gas.isSome: call.gas.get - else: 0.GasInt - gasPrice = - if call.gasPrice.isSome: call.gasPrice.get - else: 0.GasInt - - # Set defaults for gas limit if required - # Price remains zero by default - if gaslimit == 0.GasInt: - gasLimit = header.gasLimit - - var - sender = if call.source.isSome: call.source.get.toAddress else: ZERO_ADDRESS - # Note that destination is a required parameter for call. - # In geth if it's zero they use the first wallet address, - # if no wallets, remains as ZERO_ADDRESS - # TODO: Wallets - destination = if call.to.isSome: call.to.get.toAddress else: ZERO_ADDRESS - data = if call.data.isSome: nimcrypto.utils.fromHex(call.data.get.string) else: @[] - value = if call.value.isSome: call.value.get else: 0.u256 - comp = setupComputation(vmState, value, data, sender, destination, gasLimit, gasPrice, call.to.isNone) - - comp.execComputation - result = ("0x" & nimcrypto.toHex(comp.output)).HexDataStr + let + header = headerFromTag(chain, quantityTag) + callData = callData(call, true) + result = doCall(callData, header, chain) +#[ server.rpc("eth_estimateGas") do(call: EthCall, quantityTag: string) -> GasInt: ## Generates and returns an estimate of how much gas is necessary to allow the transaction to complete. ## The transaction will not be added to the blockchain. Note that the estimate may be significantly more than diff --git a/nimbus/rpc/rpc_types.nim b/nimbus/rpc/rpc_types.nim index c45b54fee..db572405e 100644 --- a/nimbus/rpc/rpc_types.nim +++ b/nimbus/rpc/rpc_types.nim @@ -32,12 +32,12 @@ type EthCall* = object # Parameter from user - source*: Option[EthAddressStr] # (optional) The address the transaction is send from. - to*: Option[EthAddressStr] # (optional in eth_estimateGas, not in eth_call) The address the transaction is directed to. - gas*: Option[GasInt] # (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. - gasPrice*: Option[GasInt] # (optional) Integer of the gasPrice used for each paid gas. - value*: Option[UInt256] # (optional) Integer of the value sent with this transaction. - data*: Option[EthHashStr] # (optional) Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. + source*: Option[EthAddressStr] # (optional) The address the transaction is send from. + to*: Option[EthAddressStr] # (optional in eth_estimateGas, not in eth_call) The address the transaction is directed to. + gas*: Option[HexQuantityStr] # (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. + gasPrice*: Option[HexQuantityStr]# (optional) Integer of the gasPrice used for each paid gas. + value*: Option[HexQuantityStr] # (optional) Integer of the value sent with this transaction. + data*: Option[HexDataStr] # (optional) Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. ## A block object, or null when no block was found ## Note that this includes slightly different information from eth/common.BlockHeader diff --git a/nimbus/rpc/rpc_utils.nim b/nimbus/rpc/rpc_utils.nim index 4f5ee4dd1..ad9b35901 100644 --- a/nimbus/rpc/rpc_utils.nim +++ b/nimbus/rpc/rpc_utils.nim @@ -9,7 +9,9 @@ import hexstrings, eth/[common, rlp, keys], stew/byteutils, nimcrypto, ../db/[db_chain], strutils, algorithm, options, - ../constants, stint, hexstrings, rpc_types + ../constants, stint, hexstrings, rpc_types, ../config, + ../vm_state_transactions, ../vm_state, ../vm_types, ../vm/interpreter/vm_forks, + ../vm/computation type UnsignedTx* = object @@ -21,6 +23,14 @@ type payload : Blob contractCreation {.rlpIgnore.}: bool + CallData* = object + source: EthAddress + to: EthAddress + gas: GasInt + gasPrice: GasInt + value: UInt256 + data: seq[byte] + proc read(rlp: var Rlp, t: var UnsignedTx, _: type EthAddress): EthAddress {.inline.} = if rlp.blobLen != 0: result = rlp.read(EthAddress) @@ -149,3 +159,56 @@ proc signTransaction*(tx: UnsignedTx, chain: BaseChainDB, privateKey: PrivateKey R: Uint256.fromBytesBE(sig[0..31]), S: Uint256.fromBytesBE(sig[32..63]) ) + +proc callData*(call: EthCall, callMode: bool = true): CallData = + if call.source.isSome: + result.source = toAddress(call.source.get) + + if call.to.isSome: + result.to = toAddress(call.to.get) + else: + if callMode: + raise newException(ValueError, "call.to required for eth_call operation") + + if call.gas.isSome: + result.gas = hexToInt(call.gas.get.string, GasInt) + + if call.gasPrice.isSome: + result.gasPrice = hexToInt(call.gasPrice.get.string, GasInt) + + if call.value.isSome: + result.value = UInt256.fromHex(call.value.get.string) + + if call.data.isSome: + result.data = hexToSeqByte(call.data.get.string) + +proc setupComputation(vmState: BaseVMState, call: CallData, fork: Fork) : Computation = + vmState.setupTxContext( + origin = call.source, + gasPrice = call.gasPrice, + forkOverride = some(fork) + ) + + let msg = Message( + kind: evmcCall, + depth: 0, + gas: call.gas, + sender: call.source, + contractAddress: call.to, + codeAddress: call.to, + value: call.value, + data: call.data + ) + + result = newComputation(vmState, msg) + +proc doCall*(call: CallData, header: BlockHeader, chain: BaseChainDB): HexDataStr = + var + # we use current header stateRoot, unlike block validation + # which use previous block stateRoot + vmState = newBaseVMState(header.stateRoot, header, chain) + fork = toFork(chain.config, header.blockNumber) + comp = setupComputation(vmState, call, fork) + + comp.execComputation() + result = hexDataStr(comp.returnData) diff --git a/tests/rpcclient/ethcallsigs.nim b/tests/rpcclient/ethcallsigs.nim index a2bfb5d81..f0f208b52 100644 --- a/tests/rpcclient/ethcallsigs.nim +++ b/tests/rpcclient/ethcallsigs.nim @@ -36,21 +36,21 @@ proc eth_getUncleCountByBlockNumber(quantityTag: string): HexQuantityStr proc eth_getCode(data: EthAddressStr, quantityTag: string): HexDataStr proc eth_sign(data: EthAddressStr, message: HexDataStr): HexDataStr proc eth_signTransaction(data: TxSend): HexDataStr -#proc eth_sendRawTransaction(data: string, quantityTag: int): UInt256 -proc eth_call(call: EthCall, quantityTag: string): string -proc eth_estimateGas(call: EthCall, quantityTag: string): GasInt +proc eth_sendTransaction(data: TxSend): EthHashStr +proc eth_sendRawTransaction(data: HexDataStr): EthHashStr +proc eth_call(call: EthCall, quantityTag: string): HexDataStr -# TODO: Use eth/common types - -#[proc eth_sendTransaction(obj: EthSend): UInt256 -proc eth_getBlockByHash(data: array[32, byte], fullTransactions: bool): BlockObject +proc eth_estimateGas(call: EthCall, quantityTag: string): HexQuantityStr +proc eth_getBlockByHash(data: Hash256, fullTransactions: bool): BlockObject proc eth_getBlockByNumber(quantityTag: string, fullTransactions: bool): BlockObject -proc eth_getTransactionByHash(data: Uint256): TransactionObject -proc eth_getTransactionByBlockHashAndIndex(data: UInt256, quantity: int): TransactionObject +proc eth_getTransactionByHash(data: Hash256): TransactionObject +proc eth_getTransactionByBlockHashAndIndex(data: Hash256, quantity: int): TransactionObject proc eth_getTransactionByBlockNumberAndIndex(quantityTag: string, quantity: int): TransactionObject -proc eth_getTransactionReceipt(data: UInt256): ReceiptObject -proc eth_getUncleByBlockHashAndIndex(data: UInt256, quantity: int64): BlockObject +proc eth_getTransactionReceipt(data: Hash256): ReceiptObject +proc eth_getUncleByBlockHashAndIndex(data: Hash256, quantity: int64): BlockObject proc eth_getUncleByBlockNumberAndIndex(quantityTag: string, quantity: int64): BlockObject + +#[ proc eth_getCompilers(): seq[string] proc eth_compileLLL(): seq[byte] proc eth_compileSolidity(): seq[byte] diff --git a/tests/test_rpc.nim b/tests/test_rpc.nim index 09e0f14cf..9e5202ce4 100644 --- a/tests/test_rpc.nim +++ b/tests/test_rpc.nim @@ -215,7 +215,7 @@ proc doTests {.async.} = let recoveredAddr = pubkey.toCanonicalAddress() check recoveredAddr == signer # verified - test "eth_signTransaction": + test "eth_signTransaction, eth_sendTransaction, eth_sendRawTransaction": var unsignedTx = TxSend( source: ethAddressStr(signer), to: ethAddressStr(ks2).some, @@ -226,23 +226,26 @@ proc doTests {.async.} = nonce: none(HexQuantityStr) ) - let res = await client.eth_signTransaction(unsignedTx) - let signedTx = rlp.decode(hexToSeqByte(res.string), Transaction) + let signedTxHex = await client.eth_signTransaction(unsignedTx) + let signedTx = rlp.decode(hexToSeqByte(signedTxHex.string), Transaction) check signer == signedTx.getSender() # verified - #test "eth_call": - # let - # blockNum = state.blockheader.blockNumber - # callParams = EthCall(value: some(100.u256)) - # r1 = await client.eth_call(callParams, "0x" & blockNum.toHex) - # check r1 == "0x" - #test "eth_getBalance": - # let r2 = await client.eth_getBalance(ZERO_ADDRESS.toEthAddressStr, "0x0") - # check r2 == 0 - # - # let blockNum = state.blockheader.blockNumber - # let r3 = await client.eth_getBalance(address.toEthAddressStr, "0x" & blockNum.toHex) - # check r3 == 0 + let hashAhex = await client.eth_sendTransaction(unsignedTx) + let hashBhex = await client.eth_sendRawTransaction(signedTxHex) + check hashAhex.string == hashBhex.string + + test "eth_call": + var ec = EthCall( + source: ethAddressStr(signer).some, + to: ethAddressStr(ks2).some, + gas: encodeQuantity(100000'u).some, + gasPrice: none(HexQuantityStr), + value: encodeQuantity(100'u).some, + data: HexDataStr("0x").some, + ) + + let res = await client.eth_call(ec, "latest") + #test "eth_estimateGas": # let # call = EthCall()