Problems with signature verification on chain

I have this contract and I am trying to do signature verification on chain.
I have this contract:

contract;

use std::{b512::B512, ecr::{ec_recover, ec_recover_address, EcRecoverError}};
use std::hash::*;

abi MyContract {

    #[storage(read)]
    fn generate_msg_hash(txt: str) -> b256;

    #[storage(read)]
    fn recover_signer(
        signature: B512,
        msg_hash: b256
    ) -> Address;
}

storage {
    nonce: u256 = 0
}

impl MyContract for Contract {

    #[storage(read)]
    fn generate_msg_hash(txt: str) -> b256 {
        let nonce = storage.nonce.read();
        sha256((txt, nonce))
    }

    #[storage(read)]
    fn recover_signer(
        signature: B512,
        msg_hash: b256
    ) -> Address {
        // A recovered Fuel address.
        let result_address: Result<Address, EcRecoverError> = ec_recover_address(signature, msg_hash);
        if let Ok(address) = result_address {
            return address;
        } else {
            revert(0);
        }
    }

}

I did types generation and this is some ts code I am using to check if signature verification works on chain:

import { Provider, WalletUnlocked } from "fuels";
import { SignatureValidationAbi__factory } from "./types";

const FUEL_RPC_URL = "https://testnet.fuel.network/v1/graphql";
const CONTRACT_ID =
  "0xa1286882be906caa819dfde46879f5cb80874210bd2328e139476a4437928703";

(async () => {
  const provider = await Provider.create(FUEL_RPC_URL);
  const mnemonic =
    "---------- YOUR MNEMONIC HERE ----------";

  const wallet = WalletUnlocked.fromMnemonic(
    mnemonic,
    undefined,
    undefined,
    provider,
  );

  console.log("Address: ", wallet.address);
  console.log("Address b256: ", wallet.address.toB256());

  const contract = SignatureValidationAbi__factory.connect(CONTRACT_ID, wallet);

  const msg_hash = await contract.functions.generate_msg_hash("abc").get();
  console.log("msg_hash: ", msg_hash.value);

  const signature = await wallet.signMessage(msg_hash.value);
  console.log("signature: ", signature);

  const recover = await contract.functions
    .recover_signer(signature, msg_hash.value)
    .get();

  console.log("recover: ", recover.value);
})();

This is what I am getting as output if I pass in my mnemonic:

Address:  Address {
  bech32Address: 'fuel1yyvjlxdss79nklvt9cfuzthctr9fqj4ph5gu2hpwptcp7mjzex4skvqlsf'
}
Address b256:  0x21192f99b0878b3b7d8b2e13c12ef858ca904aa1bd11c55c2e0af01f6e42c9ab
msg_hash:  0x1b23f860db9bd9eccc26b36eaa3b6c223ef34539cdef01b240ad76ee45e176c1
signature:  0x39f13fa3bbe334d2925666fffeebefafdaa5ec96093f2179135ded28038d8c07943043cd57ff099625df2c5c3d53546d4b0139a02543fcb9072b9bacf78ff073
recover:  {
  bits: '0xefe9068f319a0704531fa7e20fe8322af4b66ed2c13a88ace32136444eafad01'
}

As you can see I am not getting correct recovered address. What am I doing wrong here?

2 Likes

will look into it and get back to you.

1 Like

Any update @Nazeeh21

I am working on it and i will update you on this by tonight

Hey @shivamlync @lokesh-lync

The contract looks correct to me the problem seems to be how you are doing verifications.

Take a look at this portion of the documentation. You skipped using the hashMessage method to get the hash of the original message after signing.

Typescript

   // Some message
    const msg_hash = await contract.functions.generate_msg_hash("abc").get();
    console.log("msg_hash: ", msg_hash.value);

    // Signing message
    const signedMessage = await wallet?.signMessage(msg_hash);
    console.log("signed message: ", signedMessage);

    // Hash of some message
    const hashedMessage = hashMessage(msg_hash);
    console.log("hashed message: ", hashedMessage);


    // TS SDK ec recover for sanity
    const recoveredAddress = Signer.recoverAddress(hashedMessage, signedMessage);
    console.log("TS SDK EC Recover: ", recoveredAddress.toB256())

    // Smart Contract ec recover
    const recover = await contract.functions
    .recover_signer(signedMessage, hashedMessage)
    .get();
    console.log("Smart Contract EC Recover: ", recover.value);

Console output

Like the documentation says wallet.signMessage uses sha256 again on the message automatically so essentially you are doing sha256(sha256(“abc”)). This is why you cannot use the msg_hash directly from the contract to recover after since it is only hashed once.

Next time I would suggest simplifying your contract example to help debug. It was very helpful to only hashing the string in the contract to rule out any misunderstandings.

  fn generate_msg_hash(txt: str) -> b256 {
      sha256(txt)
  }

I hope this helps! :slight_smile:

2 Likes

What if I have to create the hash before recovering on chain? Something like this:

    #[storage(read)]
    fn recover_signer(
        signature: B512
    ) -> Address {
        let nonce = storage.nonce.read();
        let msg_hash = sha256((txt, nonce));
        // A recovered Fuel address.
        let result_address: Result<Address, EcRecoverError> = ec_recover_address(signature, msg_hash);
        if let Ok(address) = result_address {
            return address;
        } else {
            revert(0);
        }
    }

I am not able to create the hash on chain. I tried to do sha256 again and it did not work. I tried this:

    #[storage(read)]
    fn recover_signer(
        signature: B512
    ) -> Address {
        let nonce = storage.nonce.read();
        let msg_hash = sha256(sha256((txt, nonce)));
        // A recovered Fuel address.
        let result_address: Result<Address, EcRecoverError> = ec_recover_address(signature, msg_hash);
        if let Ok(address) = result_address {
            return address;
        } else {
            revert(0);
        }
    }

Let me know if you need more details I can create an end to end example script to test it out.

Hey @lokesh-lync I think you’re misunderstanding here. It is not a matter of hashing the message again. The following will still work.

    fn recover_signer(
        signature: B512
    ) -> Address {
        let nonce = storage.nonce.read();
        let msg_hash = sha256((txt, nonce));
        // A recovered Fuel address.
        let result_address: Result<Address, EcRecoverError> = ec_recover_address(signature, msg_hash);
        if let Ok(address) = result_address {
            return address;
        } else {
            revert(0);
        }
    }

Lets break down this line

let result_address: Result<Address, EcRecoverError> = ec_recover_address(signature, msg_hash);

we know that msg_hash is sha256((txt, nonce)) and thats fine

the problem arises when you have to pass in the correct signature over that msg_hash in order to recover the correct address.

If you are using the TS SDK
Instead signing msg_hash you need to ONLY sign the tuple (txt, nonce) to avoid double hashing.

Typescript

const signedMessage = await wallet?.signMessage(msg_hash)

This is what youre passing in the as the signature sig(sha256(sha256(txt, nonce))) which is wrong

should actually be

const signedMessage = await wallet?.signMessage((txt, nonce))

to get this as the signature sig(sha256(txt, nonce)))

1 Like

got it.
But how do I sign a tuple using the ts sdk. If I try

const signedMessage = await wallet?.signMessage((txt, nonce))

I get type errors. I guess because signMessage takes a single string. So how do I pass in a tuple here?

Hey looks like you’re right. signMessage only takes a single string. Fuel doesn’t have an EIP 712 or equivalent yet. Let me talk to the SDK team and get back to you. You might need to find a workaround.

1 Like

We don’t have a limitation on the size of the payload to be signed therefore you can combine both things into a string:

${txt} ${nonce}

Make sure this is the same way you are doing it onchain as well.

You might find this helpful