How to Sign a Message in TypeScript for Sway Smart Contract Verification?

I’m working on a Sway contract that hashes a message with sha256 for verification. The message is structured like this:

rust

Copy code

let message: AddLockMessage = AddLockMessage {
    Id: Id,
    hashlock: hashlock,
    timelock: timelock,
};
let messageHash = sha256(message);

I need to sign this message off-chain using the Fuel TypeScript SDK and send the signature with the data (Id, hashlock, timelock) to pass contract verification.

Questions:

  1. How should I structure and serialize the message in TypeScript to match the sha256 hash generated by the contract?
  2. How can I correctly sign the message using the Fuel TypeScript SDK to ensure verification passes?

Any help or examples would be great!

1 Like

Hi @nerses, you can find the relevant documentation with examples in the documentation here. Be aware that the message gets hashed before being signed (see ref).

4 Likes

Thanks @nedsalk but I have already seen that. Just I need an example where signed message is struct. I have tested that example of docs , something goes wrong in struct case and cant figure out what

1 Like

The SDK only supports signing of string messages, primarily for the purposes of signing over transactions; it doesn’t have capabilities to sign arbitrary objects/structs.

2 Likes

@nedsalk then what is correct way to represent the struct as string so the verification will pass ?

Hi @nerses the struct must be serialized as a sha256 hash in order to be signed as @nedsalk mentioned earlier.

1 Like

Hi @maschad . Do you mean something like this

impl Hash for AddLockMessage {
    fn hash(self, ref mut state: Hasher) {
        self.Id.hash(state);
        self.hashlock.hash(state);
        self.timelock.hash(state);
    }
}

?

1 Like

What you had in the first example seemed correct although this looks like Rust code, are you sure this isn’t a question meant for here?

1 Like

@maschad yup I think it is correct topic

this works, but I need to verify the signature onchain, and this is where it fails.

this is my contract:

// SPDX-License-Identifier: Apache-2.0
contract;
 
use std::{
	context::*, 
	asset::*,
	b512::B512,
	ecr::ec_recover_address,
};
use std::hash::*;
use helpers::{
	utils::*,
	context::Account
};

abi Signature {
	fn verify_signature(
		account: Account,
		balance: u64,
		signature: B512,
	) -> (b256, b256, bool);
}

struct Message {
	account: Account,
	balance: u64,
}

impl Hash for Message {
    fn hash(self, ref mut state: Hasher) {
        self.account.hash(state);
        self.balance.hash(state);
    }
}

impl Signature for Contract {
	fn verify_signature(
		account: Account,
		balance: u64,
		signature: B512,
	) -> (b256, b256, bool) {
		let msg_hash = sha256(Message { account, balance });
		let recovered_address = ec_recover_address(signature, msg_hash).unwrap().bits();

		(msg_hash, recovered_address, account.value == recovered_address)
	}
}

in Typescript (cc @nerses, this is how you “hash” structs)

const myStruct = {
    account: {
        // "deployer" address bits: 0xe373620c9fdae7e928ee42001314bf8ab9638cd82a61f4e19a4e27133a419f7b
        value: deployer.address.toB256(),
        is_contract: false
    },
    balance: 100_000_000,
}

const structCoder = new StructCoder("MessageStruct", {
    account: new StructCoder("Account", {
        value: new BigNumberCoder("u256"),
        is_contract: new BooleanCoder(),
    }),
    balance: new BigNumberCoder("u64"),
})
const encodedStruct: Uint8Array = structCoder.encode(myStruct)
// 0xa4057e6a07b141149a0e94c2254f06c5bc31dc4ef36689d8e895df3ccf91f109
const message = hexlify(sha256(encodedStruct))

const signatureContract = await deploy("Signature", deployer)

// ---------- this works --------------
const deployerSigner = new Signer("priv key")
const signedMessage = await deployer.signMessage(message)
const hashedMessage = hashMessage(message)
const localRecoveredSigner = Signer.recoverAddress(hashedMessage, signedMessage).toB256() // 0xe373620c9fdae7e928ee42001314bf8ab9638cd82a61f4e19a4e27133a419f7b

// ---------- this FAILS to yield the right result --------------
const recoveredSigner = await signatureContract.functions
    .verify_signature(myStruct.account, myStruct.balance, signedMessage)
    .get()

// 0xe373620c9fdae7e928ee42001314bf8ab9638cd82a61f4e19a4e27133a419f7b
const deployerAddress = myStruct.account.value 
// 0xf64013bdbbf68430d24115ac8be5744ae8d6b7f032bff54bc6141a0681e58a8b
const contractRecoveredSigner = recoveredSigner.value[1]

expect(message === recoveredSigner.value[0]).to.be.true
expect(recoveredSigner.value[2]).to.be.true // this doesn't work

Given that “deployMessage” hashes the message before signing, I’ve even gone as far as re-sha256ing the message onchain like so:

let msg_hash = sha256(Message { account, balance });
let recovered_address = ec_recover_address(signature, sha256(msg_hash)).unwrap().bits();

but this doesn’t help either

1 Like

cc @nedsalk @maschad

What is the Hash implementation on Account I.e. self.account.hash(state);?

1 Like

Account is a struct like so:

pub struct Account {
    pub value: b256,
    pub is_contract: bool
}

and the “Hash” implementation is:

impl Hash for Account {
    fn hash(self, ref mut state: Hasher) {
        self.value.hash(state);
        self.is_contract.hash(state);
    }
}

also, just fyi: hashing is not the issue. I’ve verified that the hash outputs are exactly identical; there just seems to be an issue with the way I’m approaching the signature verifiaction, but I cannot wrap my head around it

@theAusicist

I have built the contract you provided and wrote a test that passes. The essential difference between what you did and what I did is that I used the deployer.signer().sign(message) approach which doesn’t hash the already hashed message. Please test it out and get back to me.

test('signature recovery works', async () => {
    using launched = await launchTestNode();

    const {
      wallets: [deployer],
    } = launched;

    // should deploy on the test node launched with `launchTestNode`
    const signatureContract = await deploy('Signature', deployer);

    const structCoder = new StructCoder('MessageStruct', {
      account: new StructCoder('Account', {
        value: new BigNumberCoder('u256'),
        is_contract: new BooleanCoder(),
      }),
      balance: new BigNumberCoder('u64'),
    });

    const messageStruct = {
      account: {
        value: deployer.address.toB256(),
        is_contract: false,
      },
      balance: 100_000_000,
    };

    const encodedStruct = structCoder.encode(messageStruct);

    const message = sha256(encodedStruct);

    /**
     * The workaround is to use the wallet's signer to sign the message.
     * We'll evaluate the wallet's signMessage method in another issue.
     */
    const signedMessage = deployer.signer().sign(message);

    const {
      value: [msgHash, recoveredAddress],
    } = await signatureContract.functions
      .verify_signature(messageStruct.account, messageStruct.balance, signedMessage)
      .get();

    expect(msgHash).toEqual(message);
    expect(recoveredAddress).toEqual(messageStruct.account.value);
  });
3 Likes

thanks @nedsalk. This did the trick. Though, was aware that Wallet hashed the message, so I actually did use Signer, but I must have had a lot of moving parts in the tests, so I may have botched some configuration here or there.

2 Likes

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.