From 195c8c60b43d937bc005c6b3ccda1e512b937e32 Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Wed, 13 Dec 2023 08:23:31 +0200 Subject: [PATCH] Fix send() transaction to be working again with EIP-155. (#105) * Fix send() transaction to be working again with EIP-155. Add tests. --------- Co-authored-by: jangko --- tests/test_signed_tx.nim | 31 ++++++++++++++++++++++++++----- web3.nim | 27 ++++++++++++++++++++++++--- web3/transaction_signing.nim | 31 ++++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/tests/test_signed_tx.nim b/tests/test_signed_tx.nim index fbc6a44..dbc1a0b 100644 --- a/tests/test_signed_tx.nim +++ b/tests/test_signed_tx.nim @@ -10,8 +10,12 @@ import std/[options, json], pkg/unittest2, - chronos, stint, eth/keys, + chronos, stint, + eth/keys, + eth/common/eth_types, + stew/byteutils, ../web3, + ../web3/transaction_signing, ./helpers/utils #[ Contract NumberStorage @@ -36,6 +40,23 @@ contract(NumberStorage): const NumberStorageCode = "6060604052341561000f57600080fd5b60bb8061001d6000396000f30060606040526004361060485763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416633fb5c1cb8114604d578063f2c9ecd8146062575b600080fd5b3415605757600080fd5b60606004356084565b005b3415606c57600080fd5b60726089565b60405190815260200160405180910390f35b600055565b600054905600a165627a7a7230582023e722f35009f12d5698a4ab22fb9d55a6c0f479fc43875c65be46fbdd8db4310029" suite "Signed transactions": + test "encodeTransaction(Transaction, PrivateKey, ChainId) EIP-155 test vector": + let + privateKey = PrivateKey.fromHex("0x4646464646464646464646464646464646464646464646464646464646464646").tryGet() + publicKey = privateKey.toPublicKey() + address = publicKey.toCanonicalAddress() + var tx: EthSend + tx.nonce = some(Quantity(9)) + tx.`from` = Address(address) + tx.value = some(1000000000000000000.u256) + tx.to = some(Address(hexToByteArray[20]("0x3535353535353535353535353535353535353535"))) + tx.gas = some(Quantity(21000'u64)) + tx.gasPrice = some(Quantity(20000000000'i64)) + tx.data = @[] + + let txBytes = encodeTransaction(tx, privateKey, ChainId(1)) + let txHex = "0x" & txBytes.toHex + check txHex == "0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83" test "contract creation and method invocation": proc test() {.async.} = @@ -43,7 +64,7 @@ suite "Signed transactions": let web3 = await newWeb3("ws://127.0.0.1:8545/") let accounts = await web3.provider.eth_accounts() - let gasPrice = int(await web3.provider.eth_gasPrice()) + let gasPrice = await web3.provider.eth_gasPrice() web3.defaultAccount = accounts[0] let pk = PrivateKey.random(theRNG[]) @@ -53,7 +74,7 @@ suite "Signed transactions": tx.`from` = accounts[0] tx.value = some(ethToWei(10.u256)) tx.to = some(acc) - tx.gasPrice = some(gasPrice.Quantity) + tx.gasPrice = some(gasPrice) # Send 10 eth to acc discard await web3.send(tx) @@ -71,14 +92,14 @@ suite "Signed transactions": assert(balance in ethToWei(4.u256)..ethToWei(5.u256)) # 5 minus gas costs # Creating the contract with a signed tx - let receipt = await web3.deployContract(NumberStorageCode, gasPrice = gasPrice) + let receipt = await web3.deployContract(NumberStorageCode, gasPrice = gasPrice.int) let contractAddress = receipt.contractAddress.get balance = await web3.provider.eth_getBalance(acc, "latest") assert(balance < ethToWei(5.u256)) let c = web3.contractSender(NumberStorage, contractAddress) # Calling a methof with a signed tx - discard await c.setNumber(5.u256).send(gasPrice = gasPrice) + discard await c.setNumber(5.u256).send(gasPrice = gasPrice.int) let n = await c.getNumber().call() assert(n == 5.u256) diff --git a/web3.nim b/web3.nim index 6ab0f75..80e0bd2 100644 --- a/web3.nim +++ b/web3.nim @@ -11,10 +11,12 @@ import std/[options, math, json, tables, uri, strformat] from os import DirSep, AltSep +from eth/common/eth_types import ChainId import stint, httputils, chronicles, chronos, nimcrypto/keccak, json_rpc/[rpcclient, jsonmarshal], stew/byteutils, eth/keys, + chronos/apps/http/httpclient, web3/[eth_api_types, conversions, ethhexstrings, transaction_signing, encoding, contract_dsl] @@ -23,7 +25,7 @@ template sourceDir: string = currentSourcePath.rsplit({DirSep, AltSep}, 1)[0] ## Generate client convenience marshalling wrappers from forward declarations createRpcSigs(RpcClient, sourceDir & "/web3/eth_api_callsigs.nim") -export UInt256, Int256, Uint128, Int128 +export UInt256, Int256, Uint128, Int128, ChainId export eth_api_types, conversions, encoding, contract_dsl, HttpClientFlag, HttpClientFlags type @@ -239,11 +241,20 @@ proc send*(web3: Web3, c: EthSend): Future[TxHash] {.async.} = else: return await web3.provider.eth_sendTransaction(c) +proc send*(web3: Web3, c: EthSend, chainId: ChainId): Future[TxHash] {.async.} = + doAssert(web3.privateKey.isSome()) + var cc = c + if cc.nonce.isNone: + cc.nonce = some(await web3.nextNonce()) + let t = encodeTransaction(cc, web3.privateKey.get(), chainId) + return await web3.provider.eth_sendRawTransaction(t) + proc sendData(sender: Web3SenderImpl, data: seq[byte], value: UInt256, gas: uint64, - gasPrice: int): Future[TxHash] {.async.} = + gasPrice: int, + chainId = none(ChainId)): Future[TxHash] {.async.} = let web3 = sender.web3 gasPrice = if web3.privateKey.isSome() or gasPrice != 0: some(gasPrice.Quantity) @@ -261,7 +272,10 @@ proc sendData(sender: Web3SenderImpl, gasPrice: gasPrice, ) - return await web3.send(cc) + if chainId.isNone: + return await web3.send(cc) + else: + return await web3.send(cc, chainId.get) proc send*[T](c: ContractInvocation[T, Web3SenderImpl], value = 0.u256, @@ -269,6 +283,13 @@ proc send*[T](c: ContractInvocation[T, Web3SenderImpl], gasPrice = 0): Future[TxHash] = sendData(c.sender, c.data, value, gas, gasPrice) +proc send*[T](c: ContractInvocation[T, Web3SenderImpl], + chainId: ChainId, + value = 0.u256, + gas = 3000000'u64, + gasPrice = 0): Future[TxHash] = + sendData(c.sender, c.data, value, gas, gasPrice, some(chainId)) + proc call*[T](c: ContractInvocation[T, Web3SenderImpl], value = 0.u256, gas = 3000000'u64, diff --git a/web3/transaction_signing.nim b/web3/transaction_signing.nim index dbb7b87..07f8f6a 100644 --- a/web3/transaction_signing.nim +++ b/web3/transaction_signing.nim @@ -24,9 +24,24 @@ func signTransaction(tr: var Transaction, pk: PrivateKey) = tr.V = int64(v) + 27 # TODO! Complete this +func signTransactionEip155(tr: var Transaction, pk: PrivateKey) = + let chainId = tr.chainId + tr.V = int64(chainId) * 2 + 35 + + let h = tr.txHashNoSignature + let s = sign(pk, SkMessage(h.data)) + + var r = toRaw(s) + let v = r[64] + + tr.R = fromBytesBE(UInt256, r.toOpenArray(0, 31)) + tr.S = fromBytesBE(UInt256, r.toOpenArray(32, 63)) + + tr.V = int64(v) + int64(chainId) * 2 + 35 + func encodeTransaction*(s: EthSend, pk: PrivateKey): seq[byte] = var tr = Transaction(txType: TxLegacy) - tr.gasLimit = GasInt(s.gas.get.uint64) + tr.gasLimit = s.gas.get.GasInt tr.gasPrice = s.gasPrice.get.GasInt if s.to.isSome: tr.to = some(EthAddress(s.to.get)) @@ -37,3 +52,17 @@ func encodeTransaction*(s: EthSend, pk: PrivateKey): seq[byte] = tr.payload = s.data signTransaction(tr, pk) return rlp.encode(tr) + +func encodeTransaction*(s: EthSend, pk: PrivateKey, chainId: ChainId): seq[byte] = + var tr = Transaction(txType: TxLegacy, chainId: chainId) + tr.gasLimit = s.gas.get.GasInt + tr.gasPrice = s.gasPrice.get.GasInt + if s.to.isSome: + tr.to = some(EthAddress(s.to.get)) + + if s.value.isSome: + tr.value = s.value.get + tr.nonce = uint64(s.nonce.get) + tr.payload = s.data + signTransactionEip155(tr, pk) + return rlp.encode(tr)