T-ECDSA - Local Testing. Am I doing something wrong

I’m trying to test my ecdsa singing locally.

I’m generating my public key using ecdsa_public_key with a Hash that I know is stable that that I use as my derivation path later when I try to sign.

           let address = await server.ecdsa_public_key({
                canister_id = ?canister;
                derivation_path = [sha];
                key_id = {curve = #secp256k1; name = state.settings.tecdsaKeyName};
              });

            let #ok(evm_address) = EVMAddress.fromPublicKey(encodedPk) else {
                return null;
              };

I’ve checked that this key to address verification works using Ethereum Public Key To Address Online

It seems to be working.

Later when I want to sign I derive the proper hash and do the following:

     let inputList = Buffer.Buffer<RLPTypes.Input>(1);
     inputList.add(#number(thisNonce));
      inputList.add(#number(gasPrice));
      inputList.add(#number(gasLimit));
      inputList.add(#string(Text.toLowercase(pointer.contract)));
      inputList.add(#number(0));
      inputList.add(#Uint8Array(abi));
      inputList.add(#number(chainId));
      inputList.add(#number(0));
      inputList.add(#number(0));

I take a a keccak of this abi set up and sign it with my tecdsa key with the same derivation path(I’ve checked and double checked this).

let { signature } = await server.sign_with_ecdsa({
        message_hash = Blob.fromArray(msgToSign);
        derivation_path = [sha];
        key_id = {curve = #secp256k1; name = state.settings.tecdsaKeyName};
      });

I get a signature which I assume are r and s.

I wasted a day trying to derive v (no clue why we don’t get this as part of the key) and I’m just firing both options for v off at the final rpc. Something along these lines:

let r = Array.take<Nat8>(sigBytes, 32);
      let s = Array.take<Nat8>(sigBytes, -32);
      var v = if(recoveryId == 0){
        (chainId *2) + 35;
      } else {
        (chainId *2) + 36;
      };

      debug if(debug_channel.announce) D.print(debug_show(("Ethereum Sig", v, r, s)));

      let inputList2 = Buffer.Buffer<RLPTypes.Input>(1);
      inputList2.add(#number(thisNonce));

      inputList2.add(#number(gasPrice));
      inputList2.add(#number(gasLimit));
      inputList2.add(#string(Text.toLowercase(pointer.contract)));
      inputList2.add(#number(0));
      inputList2.add(#Uint8Array(abi));
      inputList2.add(#number(v));
      inputList2.add(#Uint8Array(Buffer.fromArray<Nat8>(r)));
      inputList2.add(#Uint8Array(Buffer.fromArray<Nat8>(s)));

      let #ok(rlpPost) = RLP.encode(#List(inputList2)) else {
        return (#err(#GenericError("Failed to encode RLP")), state.cycleSettings.baseCharge);
      };

      let rlpText = Hex.encode(Buffer.toArray(rlpPost));

Then I ship this off to the rpc hoping that all will be well, but I get an error that my address has no eth although I’ve sent it much eth.

I’m pretty sure I have the contract set up correctly because I can plug

0xf8cc8083140eca85e8d4a51000949fe46736679d2d9a65f0992f2272de9f3c7fa6e080b86442842e0e00000000000000000000000065315b4df3ebaf8738507b1ce295bf473e31fcdf000000000000000000000000974a5c78a5cd3b9ad38883f0a8301bb5968b5efd000000000000000000000000000000000000000000000000000000000000001f82f4f6a0b53364bd58b72b3117b87010651edf6c118108025c0b40a04c3e18fc7e9f2905a03315f551aaaf5392470126d3b60536dee038f088f1015b0e894a5cc65cc9e2d3

into MyCrypto - Ethereum Wallet Manager and I see a pretty valid transaction. The only thing that is wrong is the from address. This is not the address I sent the eth to and it is not the address derived by the key I pulled from ecdsa_public_key.

Maybe this just doesn’t work locally? I don’t think that is the case? Have I made a poor assumption about the format of the signature that comes back(32 bytes of r then 32 bytse of s?) Is it reverse-encoded or something like that?

It SEEMS the signature is working, it just isn’t signing with the right key because the MyCrypto took finds an address for a signature and that seems far from random.

A day wasted banging my head against a wall. Any help or random guesses would be appreciated.

[Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] ("Ethereum raw trx result", #Consistent(#Err(#JsonRpcError({code = -32_000; message = "sender doesn't have enough funds to send tx. The max upfront cost is: 1314506000000000000 and the sender's account only has: 0"}))))

After Inspecting the ckERC20 code I see that I’m not using the correct EIP 1559 structure. I’ve updated my code to create a trx to sign by doing this:


inputList.add(#number(chainId));
      inputList.add(#number(thisNonce));
      inputList.add(#number(1000000000)); //gas tip
      
      inputList.add(#number(gasPrice));
      inputList.add(#number(gasLimit));
      inputList.add(#string(Text.toLowercase(pointer.contract)));
      inputList.add(#number(0));
      let accessList = Buffer.Buffer<RLPTypes.Input>(1);
      accessList.add(#string(Text.toLowercase(pointer.contract)));
      inputList.add(#List(accessList));
      inputList.add(#Uint8Array(abi));

      let #ok(rlpPre) = RLP.encode(#List(inputList)) else {
        return (#err(#GenericError("Failed to encode RLP")), state.cycleSettings.baseCharge);
      };
      
      debug if(debug_channel.announce) D.print(debug_show(("RLP Pre", Hex.encode(Buffer.toArray(rlpPre)))));

      let sha33 = SHA3.Keccak(256);
      sha33.update(Array.append<Nat8>([2], Buffer.toArray(rlpPre)));
      let rlpHash = sha3.finalize();

This gets me the hash to sign and I sign it as before.

EIP 1559 removes the crazy v stuff and lets you just use the y parity from your public key, so this should get easier for the final transaction:

let r = Array.take<Nat8>(sigBytes, 32);
      let s = Array.take<Nat8>(sigBytes, -32);
      
      debug if(debug_channel.announce) D.print(debug_show(("Ethereum Sig", v, r, s)));

      let inputList2 = Buffer.Buffer<RLPTypes.Input>(1);
      inputList2.add(#number(chainId));
      inputList2.add(#number(thisNonce));
      inputList2.add(#number(1000000000)); //gas tip
      inputList2.add(#number(gasPrice));
      inputList2.add(#number(gasLimit));
      inputList2.add(#string(Text.toLowercase(pointer.contract)));
      inputList2.add(#number(0));
      inputList2.add(#Uint8Array(abi));

      inputList2.add(#List(accessList));
      inputList2.add(#number(Nat8.toNat(publicKey[0])));
      inputList2.add(#Uint8Array(Buffer.fromArray<Nat8>(r)));
      inputList2.add(#Uint8Array(Buffer.fromArray<Nat8>(s)));

      let #ok(rlpPost) = RLP.encode(#List(inputList2)) else {
        return (#err(#GenericError("Failed to encode RLP")), state.cycleSettings.baseCharge);
      };

      let rlpText = Hex.encode(Array.append<Nat8>([2],Buffer.toArray(rlpPost)));

      
      let result = await* ethSendTrx(canisterId, ?chainId, rpcs, rlpText, state.cycleSettings.bytesPerEthTransferRequest);

I’m getting an error or these from the rpc that length is wrong somewhere…it isn’t a very descriptive error

{\"jsonrpc\":\"2.0\",\"id\":22,\"error\":{\"code\":-32603,\"message\":\"TypeError: Cannot read properties of undefined (reading 'length')\",\"data\":{\"message\":\"TypeError: Cannot read properties of undefined (reading 'length')\"}}}

My current suspicions are that I’ve mis encoded the Access list(In only have the contract address in an list.) Or I’ve put something in as a number that needs to be hex. Anyone see anything clearly off? My outputs no longer parse in then mycrypto tool and they don’t give an error.

Full signed trx:

0x02f8e8827a6980843b9aca0083140eca85e8d4a5100094e7f1725e7734ce288f8367e1bb143e90bb3f051280b86442842e0e0000000000000000000000002a99cbac835c2727f71b809a75ea0e10bf84bb9a000000000000000000000000974a5c78a5cd3b9ad38883f0a8301bb5968b5efd0000000000000000000000000000000000000000000000000000000000000002d594e7f1725e7734ce288f8367e1bb143e90bb3f051202a0278c9615878e1d30bad9826fbce21ceed6249315c2cef04801a240df2551de54a00e687162a37372ec9fce94fa168d62ab9f81886d1549ce1660d498e7e2a8e767

Hi @skilesare

I don’t know about any Motoko examples, but for Rust there are several resources that could maybe help you:

  1. basic_ethereum example
  2. ICP alloy from @kristofer

Regarding the RLP encoding, could it be that the length of the list was forgotten in the RLP encoding (just a guess)? For debugging purposes I would suggest to test your implementation against some known test vectors.

I had stupid number in stupid places and with stupid values.

eth_sendRawTransaction
Transaction: 0xf926e47c3ba25e6734c506ae92e54327d95d8b35cfb32cc6ae042ab818ed0655
From: 0x412a62ab923b5a52c55ab971648c7381174e91ef
To: 0xe7f1725e7734ce288f8367e1bb143e90bb3f0512
Value: 0 ETH
Gas used: 58112 of 30000000
Block #48: 0xd02a2b554d3290afbc9c15d7d82687891cc89461f963085424706f4f1c9a547e

Napoleon Dynamite Yes GIFs | Tenor

Lessons learned:

  • The y parity is 0 or 1 not 2 or 3 (but the uncompressed key leading nat8 is useful…just subtract 2.
  • y parity goes AFTER the r and s
  • Max Gas limit is like 300000.
  • Set your priority gas fee and price per gas to the same amount.
  • Access list goes last in the presign. I haven’t been brave enough to put a number in yet.