How to get the icp TX hash from canister

I read the ledger canister’s did, but it only returns the hash of the previous block.
How does a canister get the hash of a transaction (without using the Rosetta API)?

For example this transaction, I want to get the hash: efa2e7e9e1a0f4faf7bdd0a771e72309db28b814e6d2f5711fcbeabbe39543d8

3 Likes

Hi @E.SO, you can calculate the hash of a transaction as following: 1) serialize the transaction with cbor and 2) calculate the sha256 over the result. This is the code we use to do it.

3 Likes

Hello Mariop,

I hope this message finds you well. I have been working on a project involving ICP (Internet Computer Protocol) transactions, and I’ve encountered some challenges related to transaction hash calculation. I’m reaching out for your expertise and guidance on these matters.

I am trying to calculate the transaction hash for ICP transactions in Java. Specifically, I need to:

  1. Encode ICP transaction data in CBOR format.
  2. Calculate the SHA-256 hash of the CBOR-encoded data.

I’m looking for a code example and sample data to assist with debugging. This will help ensure that the transaction hash obtained from my Java implementation matches what’s displayed on the official ICP dashboard and the Rosette client for the same transaction. Any insights or assistance you can provide on this topic would be greatly appreciated.
I have received the transaction data itself from a canister, including the data without the hash function applied. To calculate the hash of this transaction data, I’ve written a snippet. However, I’ve noticed that the hash function’s result is different from what is expected.

Here is the snippet I’ve written for reference:

javaCopy code

use sha2::{Sha256, Digest};
use serde::{Serialize, Deserialize};
use serde_cbor;


#[derive(Debug, Serialize, Deserialize)]
pub struct TimeStamp {
    timestamp_nanos: u64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Tokens {
    e8s: u64,
}

type AccountIdentifier = Vec<u8>;

type Memo = u64;

type ByteBuf = Vec<u8>;

#[derive(Debug,Serialize,Deserialize)]
pub enum Operation {
    Transfer {
        from: AccountIdentifier,
        to: AccountIdentifier,
        amount: Tokens,
        fee: Tokens,
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Transaction {
    pub operation: Operation,
    pub memo: Memo,
    pub created_at_time: Option<TimeStamp>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icrc1_memo: Option<ByteBuf>,
}

fn hash_transaction(tx: &Transaction) -> String {
    let serialized = serde_cbor::ser::to_vec_packed(&tx).unwrap();

    // Print the serialized CBOR data in hexadecimal format
    println!("Serialized Transaction (CBOR): {:?}", hex::encode(&serialized));

    let mut state = Sha256::new();
    state.update(&serialized);

    let result = state.finalize();
    format!("{:x}", result)
}

fn main() {
    let tx = Transaction {
        operation: Operation::Transfer {
            from: vec![184, 148, 138, 218, 225, 74, 217, 147, 224, 68, 5, 43, 186, 121, 61, 192, 127, 226, 212, 64, 26, 221, 10, 202, 240, 213, 152, 61, 71, 84, 21, 238],
            to: vec![76, 229, 5, 195, 188, 198, 129, 44, 139, 135, 128, 68, 192, 250, 36, 103, 94, 145, 97, 67, 207, 9, 100, 206, 194, 161, 228, 245, 235, 206, 240, 246],
            amount: Tokens { e8s: 1000000000 },
            fee: Tokens { e8s: 10000 },
        },
        memo: 0,
        created_at_time: Some(TimeStamp {
            timestamp_nanos: 1704818689528096318,
        }),
        icrc1_memo: None,
    };

    let hash = hash_transaction(&tx);
    println!("The SHA-256 hash of transaction {:?} is {}", tx, hash);
}

Transaction’s data used from transaction: Transaction: 015e80cd2db039cac2e0282764960330b709cd0a0656918ddc57d94822bc6965 - ICP Dashboard
expected hash: 015e80cd2db039cac2e0282764960330b709cd0a0656918ddc57d94822bc6965
snippet hash: 616492af0385dd3092e2164ee0c0819d119c8854cc47ae94f25a1827bf9098ed
Snippet debug:

Serialized Transaction (CBOR): "a300a100a400982018b81894188a18da18e1184a18d9189318e0184405182b18ba1879183d18c0187f18e218d41840181a18dd0a18ca18f018d51898183d184718541518ee019820184c18e50518c318bc18c61881182c188b18871880184418c018fa18241867185e18911861184318cf09186418ce18c218a118e418f518eb18ce18f018f602a1001a3b9aca0003a100192710010002a1001b17a8bb90cbc4e63e"
The SHA-256 hash of transaction Transaction { operation: Transfer { from: [184, 148, 138, 218, 225, 74, 217, 147, 224, 68, 5, 43, 186, 121, 61, 192, 127, 226, 212, 64, 26, 221, 10, 202, 240, 213, 152, 61, 71, 84, 21, 238], to: [76, 229, 5, 195, 188, 198, 129, 44, 139, 135, 128, 68, 192, 250, 36, 103, 94, 145, 97, 67, 207, 9, 100, 206, 194, 161, 228, 245, 235, 206, 240, 246], amount: Tokens { e8s: 1000000000 }, fee: Tokens { e8s: 10000 } }, memo: 0, created_at_time: Some(TimeStamp { timestamp_nanos: 1704818689528096318 }), icrc1_memo: None } is 616492af0385dd3092e2164ee0c0819d119c8854cc47ae94f25a1827bf9098ed

If you have any insights into why the calculated hash is differing from the expected result, or if you can provide guidance on how to correctly calculate the hash using the received transaction data, it would be of great assistance to my project.

Thank you very much for your time and expertise. I look forward to your response.

Best regards,
Alexander

1 Like

Hi Alexander, just to confirm: Do you want to calculate the transaction hash and not the block hash? I am asking this because the transaction hash is not unique. There is a good chance that given a transaction there exist multiple transactions with that hash in the ICP ledger.

1 Like

Hi there,

Thank you for your question. Yes, the main objective is to enable linking transactions from the ledger to the official dashboard, such as this example link. The reference in these links is indeed the transaction hash.

Currently, we’re utilizing Rosetta in Docker to read ledger transactions, but we’re planning to switch to the canister ledger. However, a challenge arises as the canister ledger doesn’t provide transaction hashes directly. Therefore, we need to construct a hash from the transaction data in the canister to create links to the dashboard from our application.

To address this, I’ve written a code snippet in Rust aimed at recalculating the hash for debugging purposes, and I plan to replicate this process in Java. However, I’m encountering an issue: the hash generated by my implementation doesn’t match the one on the dashboard. It seems the problem might be related to the AccountIdentifier. Since I’m not very familiar with Rust, this has been a challenging aspect for me.

I would greatly appreciate your assistance in this matter. Here is the code:

use sha2::{Sha256, Digest};
use serde::{Serialize, Deserialize};
use serde_cbor;


#[derive(Debug, Serialize, Deserialize)]
pub struct TimeStamp {
    timestamp_nanos: u64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Tokens {
    /// Number of 10^-8 Tokens.
    /// Named because the equivalent part of a Bitcoin is called a Satoshi
    e8s: u64,
}

#[derive(Debug,Serialize,Deserialize)]
pub struct AccountIdentifier {
    pub hash: [u8; 28],
}

#[derive(Debug,Serialize,Deserialize)]
pub struct Memo(pub u64);

#[derive(Debug,Serialize,Deserialize)]
pub struct ByteBuf {
    bytes: Vec<u8>,
}

#[derive(Debug,Serialize,Deserialize)]
pub enum Operation {
    Burn {
        from: AccountIdentifier,
        amount: Tokens,
        #[serde(skip_serializing_if = "Option::is_none")]
        spender: Option<AccountIdentifier>,
    },
    Mint {
        to: AccountIdentifier,
        amount: Tokens,
    },
    Transfer {
        from: AccountIdentifier,
        to: AccountIdentifier,
        amount: Tokens,
        fee: Tokens,
        #[serde(skip_serializing_if = "Option::is_none")]
        spender: Option<AccountIdentifier>,
    },
    Approve {
        from: AccountIdentifier,
        spender: AccountIdentifier,
        allowance: Tokens,
        expected_allowance: Option<Tokens>,
        expires_at: Option<TimeStamp>,
        fee: Tokens,
    },
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Transaction {
    pub operation: Operation,
    pub memo: Memo,
    /// The time this transaction was created.
    pub created_at_time: Option<TimeStamp>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icrc1_memo: Option<ByteBuf>,
}

fn hash_transaction(tx: &Transaction) -> String {
    // Print the serialized CBOR data in hexadecimal format
    println!("Serialized Transaction (CBOR): {:?}", hex::encode(&serde_cbor::ser::to_vec_packed(&tx).unwrap()));

    let mut state = Sha256::new();
    state.update(&serde_cbor::ser::to_vec_packed(&tx).unwrap());

    let result = state.finalize();
    format!("{:x}", result)
}

fn main() {
    // transaction: https://dashboard.internetcomputer.org/transaction/2e6a1a86ceadae6c36e2f4be16f05bcdb686d73f7c8c5b007eab351f78ac82f1
    // expected hash: 2e6a1a86ceadae6c36e2f4be16f05bcdb686d73f7c8c5b007eab351f78ac82f1
    //let from_account_bytes: [u8; 32] = [34, 12, 58, 51, 249, 6, 1, 137, 110, 38, 247, 111, 166, 25, 254, 40, 135, 66, 223, 31, 167, 84, 38, 237, 250, 247, 89, 211, 159, 36, 85, 165];
    let from_account_bytes: [u8; 28] = [249, 6, 1, 137, 110, 38, 247, 111, 166, 25, 254, 40, 135, 66, 223, 31, 167, 84, 38, 237, 250, 247, 89, 211, 159, 36, 85, 165];
    // Truncate the array to the first 28 bytes
    let from_truncated_bytes = <[u8; 28]>::try_from(&from_account_bytes[..28]).unwrap();

    //let to_account_bytes: [u8; 32] = [6, 151, 172, 14, 101, 6, 130, 101, 214, 212, 21, 79, 226, 17, 192, 244, 205, 133, 97, 115, 232, 71, 168, 69, 20, 120, 119, 0, 181, 227, 116, 195];
    let to_account_bytes: [u8; 28] = [101, 6, 130, 101, 214, 212, 21, 79, 226, 17, 192, 244, 205, 133, 97, 115, 232, 71, 168, 69, 20, 120, 119, 0, 181, 227, 116, 195];
    let to_truncated_bytes = <[u8; 28]>::try_from(&to_account_bytes[..28]).unwrap();

    let tx = Transaction {
        operation: Operation::Transfer {
            from: AccountIdentifier {
                hash: from_truncated_bytes,
            },
            to: AccountIdentifier {
                hash: to_truncated_bytes,
            },
            amount: Tokens { e8s: 155814000 },
            fee: Tokens { e8s: 10000 },
            spender: None,
        },
        memo: Memo(1704894717240),
        created_at_time: Some(TimeStamp {
            timestamp_nanos: 1704894717239479196,
        }),
        //icrc1_memo: None,
         icrc1_memo: Some(ByteBuf {
            bytes: vec![],
        }),
    };

    let hash = hash_transaction(&tx);
    println!("The SHA-256 hash of transaction {:?} is {}", tx, hash);
}

Any guidance or suggestions to resolve this discrepancy would be immensely helpful.

Thank you in advance for your help!

1 Like

Thanks for clarifying. The actual rust implementation of the transaction hash can be found here, the Transaction object can be found here. I would encourage you to try it with the Transaction object that I linked here.

1 Like

As I try to replicate hash() method, it has dependency that I could not reach

use ic_ledger_hash_of::HashOf;

I am trying to replicate this method

fn hash(&self) -> HashOf<Self> {
        let mut state = Sha256::new();
        state.write(&serde_cbor::ser::to_vec_packed(&self).unwrap());
        HashOf::new(state.finish())
    }

I am hoping someone could help me resolve finding / replacing HashOf<>
Thank you in advance

Here you can find ic_ledger_hash_of::HashOf: ic/packages/ic-ledger-hash-of/src/lib.rs at master · dfinity/ic · GitHub

1 Like

Hi @Severin,
Thank you for the previous response, I have borrowed the implementation of ic_ledger_hash_of and used it for a script to generate hash from a manually values-assigned Transaction instance. All the values that I am using is from mainnet and can be verified on explorer from transaction hash b12eee8bca96feeac03bed9bfe3cd9504f7815b6c3307104f85a526cf9cce4d7
but my implementation albeit following faithfully ledger’s implementation, still generates different hash ae001453d3b5439dbae59d227fda70136e5cdbaf65501cf2d71059ce0312365e
I am hoping you can take a look and possibly provide hints to where I may got things incorrectly. Thank you in advance. The following is my implementation:

use ic_ledger_types::{AccountIdentifier, Memo, Transaction, Timestamp, Operation, Tokens};
use serde_cbor;
use sha2::{Sha256, Digest};
use std::fmt;
use std::hash::Hash;
use std::marker::PhantomData;

const HASH_LENGTH: usize = 32;

#[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HashOf<T> {
    hash: [u8; HASH_LENGTH],
    phantom: PhantomData<T>,
}


impl<T> HashOf<T> {
    pub fn new(bs: [u8; HASH_LENGTH]) -> Self {
        HashOf {
            hash: bs,
            phantom: PhantomData,
        }
    }
}

impl<T> fmt::Display for HashOf<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", hex::encode(self.hash))
    }
}

fn hash_transaction(tx: &Transaction) -> HashOf<Transaction> {
    let serialized = serde_cbor::ser::to_vec_packed(&tx).unwrap();

    let mut state = Sha256::new();
    state.update(&serialized);

    let result = state.finalize();
    let fixed_result: [u8; HASH_LENGTH] = result.into();
    HashOf::new(fixed_result)
}

fn main() {
    // Create sample data
    let from_hex = "4ef6b55f55a4bbeb4646b4e433da030662f4d0c097c5b158bf64ec44dfaeda53";
    let to_hex = "5c8aea1a5c6b871125c5b876688f2c28483a37314717750f2175156742fd08d8";

    let from_account = match AccountIdentifier::from_hex(from_hex) {
        Ok(account) => account,
        Err(e) => {
            eprintln!("Failed to create from account-identifier from hex: {}", e);
            return;
        }
    };

    let to_account = match AccountIdentifier::from_hex(to_hex) {
        Ok(account) => account,
        Err(e) => {
            eprintln!("Failed to create to account-identifier from hex: {}", e);
            return;
        }
    };

    let transaction = Transaction {
        memo: Memo(0),
        operation: Some(Operation::Transfer {
            from: from_account,
            to: to_account,
            amount: Tokens::from_e8s(48980000),
            fee: Tokens::from_e8s(10000),
        }),
        created_at_time: Timestamp {
            timestamp_nanos: 1716563439433251852,
        },
        icrc1_memo: None,
    };

    // Hash the transaction
    let hash = hash_transaction(&transaction);

    // Print the hash
    println!("Tx Hash: {}", hash);
}

I believe you are confusing block timestamp with created_at_timestamp. One is the timestamp of when the block was created in which the transaction was recorded and the other is the timestamp set by the user when creating the transaction offline.
Please make sure you set the fields correctly and let me know whether that resolves the issue.

The data was already correct. I have resolved this by other means. Something Dfinity may want to check is that many structs defined with ic_ledger_types are not compatible with the structs defined within your rosetta-api. For example:

//---------- ic_ledger_types definition
pub struct Transaction {
    pub memo: Memo,
    pub operation: Option<Operation>,
    pub created_at_time: Timestamp,
    pub icrc1_memo: Option<ByteBuf>,
} 
pub enum Operation {
    Mint {
        to: AccountIdentifier,
        amount: Tokens,
    },
    Burn {
        from: AccountIdentifier,
        amount: Tokens,
    },
    Transfer {
        from: AccountIdentifier,
        to: AccountIdentifier,
        amount: Tokens,
        fee: Tokens,
    },
    Approve {
        from: AccountIdentifier,
        spender: AccountIdentifier,
        expires_at: Option<Timestamp>,
        fee: Tokens,
    },
    TransferFrom {
        from: AccountIdentifier,
        to: AccountIdentifier,
        spender: AccountIdentifier,
        amount: Tokens,
        fee: Tokens,
    },
}
//-------------- rosetta-api definition
pub struct Transaction {
    pub operation: Operation,
    pub memo: Memo,
    /// The time this transaction was created.
    pub created_at_time: Option<TimeStamp>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icrc1_memo: Option<ByteBuf>,
}
pub enum Operation {
    Burn {
        from: AccountIdentifier,
        amount: Tokens,
        #[serde(skip_serializing_if = "Option::is_none")]
        spender: Option<AccountIdentifier>,
    },
    Mint {
        to: AccountIdentifier,
        amount: Tokens,
    },
    Transfer {
        from: AccountIdentifier,
        to: AccountIdentifier,
        amount: Tokens,
        fee: Tokens,
        #[serde(skip_serializing_if = "Option::is_none")]
        spender: Option<AccountIdentifier>,
    },
    Approve {
        from: AccountIdentifier,
        spender: AccountIdentifier,
        allowance: Tokens,
        expected_allowance: Option<Tokens>,
        expires_at: Option<TimeStamp>,
        fee: Tokens,
    },
}

the minor differences between them made hash generation inconsistent. the only way we could get around this is by copying verbatim all the structs and traits from rosetta-api that are different to that of crates ic_ledger_types. I hope this helps other developers who are using ic_ledger_types and try to implement transaction hashing themselves.

1 Like