Hash-based tracking
This section shows how to use hashes to track messages and transactions across the blockchain. You'll learn the differences between hash types and how to compute and apply them to verify on-chain activity.
Hashes
Hashes play a vital role, enabling data identification and verification at all layers of the blockchain. Understanding how hashing works will help you effectively track transactions, validate messages, and integrate your applications with TON.
What is a hash?
In TON, a hash is a cryptographic fingerprint of data based on the SHA-256 algorithm. However, the hashing process is not a direct application of SHA-256 to raw data; it involves serialization and internal formatting steps, and in some cases, may rely on alternative algorithms.
Each cell, message, or transaction has its unique hash, which acts as a digital signature.
Hashes in TON are irreversible — the original data cannot be reconstructed from the hash. However, they are deterministic — the same input always produces the same hash.
TON uses the Merkle–Damgård construction for hashing, a method of building collision-resistant hash functions from one-way compression functions. This design makes it vulnerable to length extension attacks.
If you use hashes for authentication or message integrity, be aware that the Merkle–Damgård construction is susceptible to length extension attacks.
Avoid relying solely on raw hashes in security-critical contexts.
Hashes of structures
Message body hash
The message body hash is a unique identifier of a message’s content. It is calculated from the cell containing the message data.
The body refers to the field body: (Either X ^X)
:
message$_ {X:Type} info:CommonMsgInfo
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = Message X;
Let’s walk through an example using a message body with the comment "Hello, TON!" and compute its hash.
import { beginCell } from '@ton/core';
// Create the message body to increment the counter
const messageBody = beginCell()
.storeUint(0, 32) // op code
.storeStringTail('Hello TON!')
.endCell();
console.log('Message body hash:', messageBody.hash().toString('hex'));
Expected output
Message body hash: d989794fa90c9817a63a22e00554574f3c4a347dcfd2e2886980a294de15f2af
When to use a message body hash:
- to verify data integrity
- for debugging and logging operations
- to deduplicate messages in your application
Full message hash
A full message includes not just the body but also headers with metadata such as the sender, recipient, amount, and fees. Its hash uniquely identifies the entire message.
For internal messages, this corresponds to the Message X
structure defined in the following TL-B schema:
message$_ {X:Type} info:CommonMsgInfo
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = Message X;
int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
src:MsgAddressInt dest:MsgAddressInt
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
created_lt:uint64 created_at:uint32 = CommonMsgInfo;
Example:
import { storeMessage, beginCell, Address, Message } from '@ton/ton';
import { toNano } from '@ton/core';
const messageBody = beginCell()
.storeUint(0, 32) // op code
.storeStringTail('Hello TON!')
.endCell();
// Create a complete message object
const message: Message = {
info: {
type: 'internal',
dest: Address.parse('UQAi....'), // Insert recipient address
value: { coins: toNano('0.1') },
bounce: true,
src: Address.parse('UQA1....'), // Insert sender address
ihrDisabled: true,
bounced: false,
ihrFee: 0n,
forwardFee: 0n,
createdAt: 0,
createdLt: 0n,
},
body: messageBody,
};
// Get the complete message hash
const messageCell = beginCell().store(storeMessage(message)).endCell();
const fullMessageHash = messageCell.hash();
console.log('Full message hash:', fullMessageHash.toString('hex'));
Expected output
Full message hash: 40b1477f90d702af223f15194724e0c12b51028bc622444959e155e77903b12c
When to use the complete message hash:
- to track specific messages on the blockchain
- when using the TON Center API to search for transactions
- to verify that no one modified the message
Transaction hash
A transaction hash is a unique identifier for an entire transaction. It derives from all the data involved in the transaction, including incoming and outgoing messages, state changes, and fees. This data becomes available only after the blockchain executes the transaction, so the hash is calculated once the transaction is recorded.
Obtaining a transaction hash:
import { TonClient, beginCell, storeMessage } from '@ton/ton';
import { Address } from '@ton/core';
async function main() {
const client = new TonClient({
// Testnet endpoint; if you're working on Mainnet, don't forget to update this
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
apiKey: 'insert your api key', // Get your API key via the toncenter.com bot
});
const transactions = await client.getTransactions(
Address.parse('0QD3o...'), // Insert your wallet address here
{ archival: true, limit: 10 },
);
for (const tx of transactions) {
const txHash = tx.hash();
console.log('Transaction hash:', txHash.toString('hex'));
// You can also get the hash of the incoming message
if (tx.inMessage) {
const inMsgCell = beginCell().store(storeMessage(tx.inMessage)).endCell();
const inMsgHash = inMsgCell.hash();
console.log('Incoming message hash:', inMsgHash.toString('hex'));
}
}
}
main().catch(console.error);
Expected output
Displays the last 10 transactions for the specified address. For each transaction, it prints the transaction hash. If an incoming message exists, it also prints the hash of that message.
Transaction hash: 79a6406e5544e95bbe5db8c7e8189daaca240b2d6b4f21cc479c3082dd4c5cce
Incoming message hash: 4557aa15e21cf0501f3d82eaa26a23ab61c08a544490154bd12a1814d492db42
Transaction hash: 1a86894bbe2f5af090d97f761f0367a16b713e93247056003e6d8b6a68a5165e
Incoming message hash: dfa0916ceee9dd26c6966d55446b23f88950e118432f870d092cfcde83a3b864
:
:
Normalized message hash
Normalization is a standardization process that converts different representations into a consistent format. While messages across interfaces follow the TL-B scheme, structural differences in implementation sometimes lead to collisions.
To address this, the ecosystem defines a standard that ensures consistent hash calculation. The normalization rules are specified in detail in TEP-467.
The problem normalization solves:
Functionally identical messages may differ in how they represent the src
, import_fee
, and init
fields. These variations result in different hashes for messages with equivalent content, which complicates transaction tracking and deduplication.
How normalization works:
The normalized hash is computed by applying the following standardization rules to an external-in message:
- Source Address (
src
): set toaddr_none$00
- Import Fee (
import_fee
): set to0
- InitState (
init
): set to an empty value - Body: always stored as a reference
Practical example of calculating a normalized hash:
import { beginCell, Cell, Address } from '@ton/core';
function normalizeExternalMessage(destAddress: Address, bodyCell: Cell): Cell {
try {
const normalizedExtMessage = beginCell()
.storeUint(0b10, 2) // Set external message prefix (10 in binary)
.storeUint(0, 2) // Set src to addr_none (normalization)
.storeAddress(destAddress) // Set destination address
.storeCoins(0) // Set import_fee to 0 (normalization)
.storeBit(false) // Set init to nothing$0 (normalization)
.storeBit(true) // Use right$1 to store body by reference (normalization)
.storeRef(bodyCell) // Store body as a reference (normalization)
.endCell();
return normalizedExtMessage;
} catch (error: any) {
console.error('Error in normalization:', error.message);
throw error;
}
}
const messageBody = beginCell()
.storeUint(0, 32) // Set opcode for a simple transfer
.storeStringTail('Normalized hash example')
.endCell();
const destinationAddress = Address.parse('EQB...');
const normalizedExternalMessage = normalizeExternalMessage(destinationAddress, messageBody);
const normalizedHash = normalizedExternalMessage.hash().toString('hex');
console.log('Normalized message hash:', normalizedHash);
Expected output
Normalized message hash: ee53ccd1224c315597bdbafb2e722316d9471490b13dd9e1834903b094eb5964