0

I'm trying to reproduce a signature that a contract verifies but I'm unable to do it on web3py. To verify I'm producing a signature correctly I'm going to look at an already produced signature and verify that the address is correct. First here's my code:

hash = Web3.solidity_keccak(['address', 'uint256', 'uint256', 'uint256'], ["0xf7FA3C8C9Ba3FfCFA1d5f0ca5C9294Afd5970aE7", 1, 59144, 1])
message = defunct_hash_message(hash)
address = w3.eth.account._recover_hash(message, signature=HexBytes("0x599f3172abf3bf5029ee5ea53b5119fa2c9d5de3fa434fd81d46209fb0ab1d230a6c5a0946b59942f94e1b352d325db1bf938a608eb13cbc89ce7184e2d6112b1c"))
print(address)

The output is 0x784aD8676Ade4DEE288fd3f36e2F2EbDfdF70765 but should be 0xf7FA3C8C9Ba3FfCFA1d5f0ca5C9294Afd5970aE7

This is the contract and specifically, I'm looking at newTicket function. Specifically if you look at the code here, LotteryMaster.sol#126you can see this is being done in solidity:

keccak256(
                abi.encodePacked(to, txHashes.length, block.chainid, sessionIdx))
        );  // include session index so the signature only affect only for this session
        require(isValidSignature(toAddressHash, _signature), "Mismatch signature.");

If you look at this successful transaction here it has the full signature. Now I want to be able to recover the address like they do in the code. But for that I need to hash the message correctly. If you look at their code you can determine the message parameters by looking at the transaction itself, and so should be able to create the hash that would return the address using the signature. But I can't seem to do that. What am I doing wrong?

EDIT

Another example using an open private key: Take a look at this TX. The code if you go according to the example would be:

private_key = b"\xb2\\}\xb3\x1f\xee\xd9\x12''\xbf\t9\xdcv\x9a\x96VK-\xe4\xc4rm\x03[6\xec\xf1\xe5\xb3d"
wallet_address = '0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E'
_hash = w3.keccak(encode_packed(['address', 'uint256', 'uint256'], [wallet_address, 113, 200]))
signer = w3.eth.account.from_key(private_key)
signed = signer.signHash(_hash.hex())
print(signed)

r = requests.post('https://ape.ankr.com/api/v2/linea/getSign', json={'address': wallet_address, 'boxId': 113, 'continue': True}).json()
print(r['data']['sign'])

The above two don't match. The latter api is just a helper function that produces the same signature that is verified on the smart contract. As you can see I don't pass a private key, so the API can't sign it, but it is verified on the smart contract. I'm confused as to how it works.

1 Answer 1

1

It says it is toEthSignedMessageHash are you encoding the message before signing?

From the contract you link:

  function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) {
    // 32 is the length in bytes of hash,
    // enforced by the type signature above
    /// @solidity memory-safe-assembly
    assembly {
      mstore(0x00, "\x19Ethereum Signed Message:\n32")
      mstore(0x1c, hash)
      message := keccak256(0x00, 0x3c)
    }
  }

If I use the key from the example here:

>>> private_key = b"\xb2\\}\xb3\x1f\xee\xd9\x12''\xbf\t9\xdcv\x9a\x96VK-\xe4\xc4rm\x03[6\xec\xf1\xe5\xb3d"
>>> w3.eth.account.from_key(private_key).address
'0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E'
>>>
>>> _hash = w3.keccak(encode_packed(['address', 'uint256', 'uint256', 'uint256'], ["0xf7FA3C8C9Ba3FfCFA1d5f0ca5C9294Afd5970aE7", 1, 59144, 1])) #same as solidity_keccak
>>> encoded_msg = encode_defunct(hexstr=_hash.hex())
>>>
>>> encoded_msg
SignableMessage(version=b'E', header=b'thereum Signed Message:\n32', body=b"@\xedDx\x1bI\xa8\\U\x04\xe7\r4\xd8\xe4\xf7\xda@y;\x82\xb5mA'\x82\xf5\xf8\x06\x87I\x18")
>>> signed_message = w3.eth.account.sign_message(encoded_msg, private_key=private_key)
>>>
>>> w3.eth.account.recover_message(encoded_msg, signature=signed_message.signature)
'0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E'

edit:

Please try like this (it's as with the xSwap example I posted below):

>>> private_key = b"\xb2\\}\xb3\x1f\xee\xd9\x12''\xbf\t9\xdcv\x9a\x96VK-\xe4\xc4rm\x03[6\xec\xf1\xe5\xb3d"
>>> _hash = w3.keccak(encode_packed(['address', 'uint256', 'uint256', 'uint256'], ["0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E", 1, 59144, 1]))
>>> signer = w3.eth.account.from_key(private_key)
>>> signed = signer.signHash(_hash.hex())
>>>
>>> signed.signature
HexBytes('0x805722a36cdc140e5bad965e9511c2479d03a5e1540fb8a4be61390f35d303d5784ba8eeae759b4600e034a9721e7e32549913d0cb28c56aa24bff0ca35f356a1c')
>>>

Lifted functions for testing and included an example call

pragma solidity ^0.8.7; 
//SPDX-License-Identifier: UNLICENSED

contract c {
  function get_hash() public pure returns (bytes32) {
    address addr = 0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E;
    uint256 hashes = 1;
    uint256 blockid = 59144;
    uint256 seshid = 1;
    return keccak256(abi.encodePacked(addr, hashes, blockid, seshid));
  }

  function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32 message) {
    assembly {
      mstore(0x00, "\x19Ethereum Signed Message:\n32")
      mstore(0x1c, get_hash())
      message := keccak256(0x00, 0x3c)
    }
  }

  function verify(bytes memory sig) public pure returns (address) {
    return recover(get_hash(), sig);
  }

  function recover(bytes32 hash, bytes memory signature) internal pure returns (address signer) {
    if (signature.length == 65) {
      bytes32 r;
      bytes32 s;
      uint8 v;
      assembly {
        r := mload(add(signature, 0x20))
        s := mload(add(signature, 0x40))
        v := byte(0, mload(add(signature, 0x60)))
      }
      return ecrecover(hash, v, r, s);
    }
  }
}

enter image description here

edit:

What I think, is that api method is some off chain function returning a signature from a key it has control of, signing the data you pass it. That's the reason you can't recreate it, and the reason it returns that address that isn't yours.

Because it's the address that actually signed the data.

11
  • Please note (did comment within the example) that keccak(encode_packed()) is the same as solidity_keccak and can be imported from eth_abi which comes with web3py. encode is keccak on the padded encoding, so where you may see keccak256(encode()) in solidity, there you must explicitly use keccak(encode()) rather than solidty_keccak or keccak(encode_packed()).
    – Maka
    Commented Mar 31 at 20:06
  • Hey thanks for the quick response, I made an edit above. Still not matching, :/
    – userinev
    Commented Apr 1 at 1:26
  • What did you test, as it looks like the edit you made uses the private key from my example (from the docs). Something signed by that keys address, wouldn't match a signature from the address in the transaction you linked. Some additional examples: github.com/0xMaka/w3py/blob/main/permit/pysign.py / github.com/0xMaka/w3py/blob/main/xSwap.py
    – Maka
    Commented Apr 1 at 1:51
  • Please see the updated answer
    – Maka
    Commented Apr 1 at 3:15
  • 1
    Ohh I see! Thanks for pointing it out and that makes a lot more sense. I was confused by the .from and thought it meant the transaction sender, not the contract itself. Thank you for taking the time and helping me with this!
    – userinev
    Commented Apr 3 at 16:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.