Hashing complex structs off-chain

I have a complex struct that I hash on-chain for certain computation. Is there any off-chain way via the TS SDK to hash that struct? I see keccak256 in the TS SDK, but this doesn’t serve my purpose (takes in a Uint8Array-like object).

“Complex” struct:

struct Key {
    one: u256,
    two: u64,
    three: bool,
}

impl Hash for Key {
    fn hash(self, ref mut state: Hasher) {
        self.one.hash(state);
        self.two.hash(state);
        self.three.hash(state);
    }
}

// hashed on-chain via:
keccak256(Key {
    one: 69,
    two: 420,
    three: true
})

Minimal contract reproduction:

contract;

use std::hash::*;

abi TestHash {
    fn hash_key() -> b256;
}

struct Key {
    one: u256,
    two: u64,
    three: bool,
}

impl Hash for Key {
    fn hash(self, ref mut state: Hasher) {
        self.one.hash(state);
        self.two.hash(state);
        self.three.hash(state);
    }
}

impl TestHash for Contract {
    fn hash_key() -> b256 {
        keccak256(Key {
            one: 69,
            two: 420,
            three: true
        })
    }
}

#[test]
fn test_from() {
    let contr = abi(TestHash, CONTRACT_ID);
    log(contr.hash_key());
}

running with forc test --logs --decode yield:

Decoded log value: Bits256([29, 85, 43, 160, 236, 142, 226, 39, 227, 220, 79, 104, 17, 212, 102, 24, 162, 131, 124, 238, 200, 233, 17, 235, 223, 27, 251, 89, 44, 57, 100, 246]), log rb: 8961848586872524460
Raw logs:
[{"LogData":{"data":"1d552ba0ec8ee227e3dc4f6811d46618a2837ceec8e911ebdf1bfb592c3964f6","digest":"6b06c13e935681097361fd4505c7256fc8cf4ad603a35546b875958da41fd1ff","id":"0000000000000000000000000000000000000000000000000000000000000000","is":10368,"len":32,"pc":12632,"ptr":67103622,"ra":0,"rb":8961848586872524460}}]
1 Like

cc @danielbate @IGI-111 @calldelegation

You could use the StructCoder directly if you know the shape of the struct. Based on your above example you could use:

import { StructCoder, BigNumberCoder, BooleanCoder, keccak256 } from 'fuels';

const structCoder = new StructCoder('Key', {
  one: new BigNumberCoder('u256'),
  two: new BigNumberCoder('u64'),
  three: new BooleanCoder,
});

const encodedStruct: UInt8Array = structCoder.encode(myStruct);
const hashedStruct = keccak256(encodedStruct);

Or do you need something like EIP-712?

2 Likes

oh super. i briefly glanced over this yesterday, but it didn’t click for me.

Trivial question, but haven’t been unable to find a solution for: hashedStruct is a Uint8Array of 32 elements. How can I convert that to a string? Tried Buffer.from(hashedStruct.bufffer).toString() but I get some weird value:

const hashedStruct: Uint8Array= [
     29,  85,  43, 160, 236, 142, 226,  39,
    227, 220,  79, 104,  17, 212, 102,  24,
    162, 131, 124, 238, 200, 233,  17, 235,
    223,  27, 251,  89,  44,  57, 100, 246
  ]

// expected value: 0x1d552ba0ec8ee227e3dc4f6811d46618a2837ceec8e911ebdf1bfb592c3964f6

Check out hexlify, it’s a utility function for hex conversions, so call that on your encoded byte array. Conversely, we also offer arrayify for conversions backto UInt8Array.

Also potentially redundant, but if you don’t know the shape of the struct, you can also use the AbiCoder as long as the struct is present in the ABI (a function input/output or log type) and the coder will build out the custom struct for you.

import { AbiCoder, JsonAbiArgument, hexlify } from 'fuels';

// Picking off the my_struct input from a main function in a script ABI
const argument: JsonAbiArgument = abi.functions
  .find((f) => f.name === 'main')
  ?.inputs.find((i) => i.name === 'my_struct') as JsonAbiArgument;

const myStruct = {
  one: 1, 
  two: 2,
  three: true
};

const encoded = AbiCoder.encode(abi, argument, myStruct);
const hexed = hexlify(encoded);
// 0x123....

this worked. many thanks.

to boot off on your EIP-712 comment, would the process be entirely similar for encoding such structs (EIP-712 structs) for message signing?

So we currently don’t have a sway standard for EIP-712 yet but I believe it’s on the roadmap, so I can’t comment specifically.

But yes I would expect it to work exactly as above and you’d append it to signatures on a transaction. We’d likely augment the signMessage functionality to accept a struct and then we do the encoding/signing rather than this being implemented by the consumer, like signTransaction

1 Like