[Go to site: main page, start]

TON DocsTON Docs

Highload wallet v2: Specification

Deprecated: Highload Wallet v2 is a legacy contract. Use Highload Wallet v3 for new deployments.

This specification is maintained for reference to support existing legacy systems.

This page provides a complete technical specification for Highload Wallet v2, covering storage structure, message formats, replay protection, and limitations.

What is Highload Wallet v2?

Highload Wallet v2 is a specialized wallet contract designed for services that need to send many transactions in a short time. It uses dictionary-based replay protection to enable parallel transaction submission.

Key difference from standard wallets:
Unlike seqno-based wallets that require sequential transaction processing, Highload v2 stores processed request identifiers in a dictionary, enabling parallel submissions.

Replaced by: Highload Wallet v3

TL-B schema

TL-B (Type Language - Binary) is a domain-specific language designed to describe data structures in TON. The schemas below define the binary layout of the contract's storage and external messages.

Storage structure

storage$_ subwallet_id:uint32 last_cleaned:uint64 public_key:bits256
          queries:(HashmapE 64 Cell) = Storage;

Query ID structure

_ query_id:uint64 = QueryId;

External message structure

_ mode:uint8 message:^Cell = OutListNode;

msg_body$_ signature:bits512 subwallet_id:uint32 query_id:uint64
          messages:(HashmapE 16 Cell) = ExternalInMsgBody;

Storage structure

The Highload Wallet v2 contract stores four persistent fields (in this order):

subwallet_id (32 bits)

Purpose:
Allows a single keypair to control multiple wallets with different addresses.

How it works:
The subwallet_id is part of the contract's initial state. Changing it produces a different contract address. Each external message must include the correct subwallet_id; mismatches result in transaction failure.

last_cleaned (64 bits)

Purpose:
The greatest query_id removed by cleanup. The getter uses this watermark to report IDs that may have been forgotten.

How it works:
When an external message runs, the contract removes stored IDs whose timestamp portion is strictly more than 64 seconds old. Cleanup runs only when the wallet processes a later external message, so an idle wallet can retain older entries longer. last_cleaned records the last key removed during that cleanup.

Cleanup logic:

bound -= (64 << 32);  // Clean up records expired more than 64 seconds ago

Queries with query_id < (now() - 64) << 32 are removed from storage.

Gas costs: Cleanup operations consume gas proportional to the number of expired queries. With many expired queries, cleanup can exceed the 1,000,000 gas limit, causing the transaction to fail.

public_key (256 bits)

Purpose:
The Ed25519 public key is used to verify signatures on incoming external messages.

How it works:
When the wallet receives an external message, it verifies that the 512-bit signature was created by the holder of the private key corresponding to this public key.

queries (HashmapE 64 Cell)

Purpose:
Stores processed query_id values for replay protection.

Structure:

  • Key: 64-bit query_id
  • Value: An empty cell marker. The key's presence means the ID was processed.

How it works:
Before processing a message, the contract checks if query_id exists in queries. If found, the message is rejected (replay attack). If not found, the query_id is added to queries, and the message is processed.

Storage limit: The queries dictionary cannot exceed 65,535 cells. If this limit is reached, the contract will fail during the action phase.

External message structure

Message layout

signature:bits512
subwallet_id:uint32
query_id:uint64
messages:(HashmapE 16 Cell)

Key point:
Unlike v3, in v2 the signature is in the same cell as the message body, not in a separate reference cell.

signature (512 bits)

Type:
Ed25519 signature (512 bits).

What is signed:
The hash of the remaining slice after the signature, containing subwallet_id, query_id, and messages.

From source code:

throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));

The contract uses slice_hash() on the message body after loading the signature.

subwallet_id (32 bits)

Purpose:
Identifies which subwallet this message targets.

Validation:
Must match the subwallet_id stored in contract storage.

query_id (64 bits)

Purpose:
Unique identifier for replay protection and timestamp validation.

Structure:
The 64-bit value is internally interpreted as a timestamped identifier:

  • High 32 bits: Unix timestamp (seconds)
  • Low 32 bits: application-chosen counter or differentiator

Validation:
The contract checks query_id >= now() << 32, ensuring the query ID is not from the past (based on the current time shifted left by 32 bits).

Capacity:
The contract does not define a fixed limit of 32,000 query IDs. The practical limit depends on storage size and the gas needed to remove expired entries. The source recommends no more than 1,000 queries in an expiration interval and fewer than 100 removals in one transaction.

messages (HashmapE 16)

Purpose:
Dictionary of messages to send in this transaction.

Structure:

  • Key: uint16 (message index, 0 to 65,535)
  • Value: mode:uint8 + ^Cell (reference to internal message)

How it works:
The contract iterates through the dictionary and sends each message with its corresponding send mode:

int i = -1;
do {
  (i, var cs, var f) = dict.idict_get_next?(16, i);
  if (f) {
    var mode = cs~load_uint(8);
    send_raw_message(cs~load_ref(), mode);
  }
} until (~ f);

Max batch size:
Up to 254 messages. The limit comes from TVM action list, not the 16-bit dictionary key space.

Replay protection mechanism

Validation sequence

  1. Check query_id timestamp: query_id >= now() << 32 (exit code 35 if too old)
  2. Check replay: query_id must not be in queries (exit code 32 if already processed)
  3. Check subwallet: subwallet_id == stored_subwallet (exit code 34 if mismatch)
  4. Verify signature: Ed25519 signature verification (exit code 35 if invalid)
  5. Mark as processed: Add query_id to queries
  6. Send messages: Iterate through the message dictionary and send each message
  7. Cleanup: Remove queries older than 64 seconds

Rollback issue: Highload Wallet v2 does not use commit() to persist storage changes. If the compute phase fails after accept_message() (e.g., gas limit exceeded during cleanup) or if the action phase fails, all changes roll back, including replay protection. The query_id is not marked as processed, and lite-servers will retry the same message, burning gas repeatedly.

Highload Wallet v3 calls commit() before validating the outgoing message and processes batch actions in a separate internal transaction. See Why internal messages to self? for the batch flow.

Exit codes

Exit codeNameDescriptionHow to fix
0SuccessMessage processed successfully—
32Query already executedThe query_id was already processed (found in queries)Use a new, unique query_id
34Subwallet ID mismatchThe subwallet_id in the message does not match storageVerify you are using the correct subwallet_id for this wallet
35Invalid signature or query_idEd25519 signature verification failed, or query_id is too oldCheck the private key and ensure query_id >= now() << 32

Limitations and constraints

Storage size limit

Limit:
The queries dictionary cannot exceed 65,535 cells.

What happens if exceeded:
An exception is thrown during the action phase, and the transaction fails. The failed transaction may be replayed, potentially locking funds.

Gas limit for cleanup

Limit:
Transaction gas limit is 1,000,000 gas.

What happens if exceeded:
Cleanup operations that exceed this limit will fail, preventing the contract from processing new transactions.

Recommended limits:

  • Queries within expiration window: ≤ 1,000
  • Queries cleaned per transaction: ≤ 100

Query ID expiration

Expiration time:
Cleanup removes query IDs whose timestamp portion is strictly older than now() - 64 seconds. It runs when a later external message is processed, not on a timer.

Effective limit:
The source recommends no more than 1,000 queries in an expiration interval and fewer than 100 expired entries removed per transaction. These are operating limits for storage and cleanup gas, not a fixed query ID count.

Get methods

MethodReturnsDescription
processed?(query_id)intReturns -1 if processed, 0 if not processed, 1 if unknown (forgotten after cleanup)
get_public_key()int (256 bits)Returns the Ed25519 public key

processed? method details

Returns:

  • -1 (true) — the query_id was processed and is still stored in queries
  • 0 (false) — the query_id has not been processed yet
  • 1 (unknown) — query_id <= last_cleaned; cleanup removed that ID or an earlier range of IDs

Implementation

Source code:
ton-blockchain/ton (highload-wallet-v2-code.fc)

SDK wrappers:

  • Go: tonutils-go — includes Highload v2 wrapper
  • Python: pytoniq — includes Highload v2 wrapper

For new projects, consider using Highload Wallet v3 instead.

See also