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