diff --git a/docs.json b/docs.json index 7af88544..46e8046d 100644 --- a/docs.json +++ b/docs.json @@ -368,6 +368,7 @@ "pages": [ "techniques/carry-value", "techniques/contract-sharding", + "techniques/signing", "techniques/security", "techniques/gas", "techniques/using-onchain-libraries", diff --git a/resources/dictionaries/custom.txt b/resources/dictionaries/custom.txt index ad4a8da2..5fc8098c 100644 --- a/resources/dictionaries/custom.txt +++ b/resources/dictionaries/custom.txt @@ -604,6 +604,7 @@ savelists sbt scannability sdks +secp semver seqno seqnos @@ -662,6 +663,7 @@ systemd tApp tApps tagless +telemint testnet tock tokenomics diff --git a/resources/dictionaries/tvm-instructions.txt b/resources/dictionaries/tvm-instructions.txt index 202e8644..bc4d44ce 100644 --- a/resources/dictionaries/tvm-instructions.txt +++ b/resources/dictionaries/tvm-instructions.txt @@ -275,6 +275,7 @@ GETSTORAGEFEE GLOBALID GREATER GTINT +HASHBU HASHCU HASHEXT HASHEXTAR_BLAKE diff --git a/techniques/signing.mdx b/techniques/signing.mdx new file mode 100644 index 00000000..2b4b5670 --- /dev/null +++ b/techniques/signing.mdx @@ -0,0 +1,361 @@ +--- +title: "Signing messages in TON" +sidebarTitle: "Signing" +--- + +import { Aside } from '/snippets/aside.jsx'; + +## Overview + +Cryptographic signatures are the foundation of access control in TON. Contracts verify signatures on-chain and implement their own policies, enabling flexible designs such as wallet operations, server-authorized actions, gasless flows, multisig, and delegation. + +### Ed25519 in TON + +TON uses Ed25519 as the standard signature scheme. All wallets (v1–v5) and highload wallets rely on Ed25519. + +**Specification:** + +- **Public key:** 256 bits +- **Signature:** 512 bits +- **Curve:** Edwards form over Curve25519 + +See [TON Cryptography — Ed25519](/ton/whitepapers/tblkch#a-3-10-cryptographic-ed25519-signatures) for cryptographic details. + + + + + +### What gets signed + +Ed25519 signatures in TON work with **hashes**, not raw data: + +1. **Off-chain:** Serialize message data into a cell → compute its hash (256 bits) → sign the hash with private key → signature (512 bits) +1. **On-chain:** Contract receives signature and data → recomputes the hash → verifies signature matches the hash and public key + +## Common signing patterns + +Signatures are used in different ways depending on who signs the message, who sends it, and who pays for execution. Here are three real-world examples. + +### Example 1: Standard wallets (v1–v5) + +**How it works:** + +1. User signs a message off-chain (includes replay protection data and transfer details) +1. User sends external message to blockchain +1. Wallet contract verifies the signature +1. Wallet contract checks seqno for replay protection +1. Wallet contract accepts message (pays gas from wallet balance) +1. Wallet contract increments seqno +1. Wallet contract executes the transfer + +**Key characteristics:** + +- **Who signs:** User +- **Who sends:** User (external message) +- **Who pays gas:** Wallet contract + +This is the most common pattern. + +### Example 2: Gasless transactions (Wallet v5) + +**How it works:** + +1. User signs a message off-chain that includes two transfers: one to recipient, one to service as payment +1. User sends signed message to service via API +1. Service verifies the signature +1. Service wraps signed message in internal message +1. Service sends internal message to user's wallet (pays gas in TON) +1. Wallet contract verifies user's signature +1. Wallet contract checks seqno for replay protection +1. Wallet contract increments seqno +1. Wallet contract executes both transfers (to recipient and to service) + +**Key characteristics:** + +- **Who signs:** User +- **Who sends:** Service (internal message) +- **Who pays gas:** Service (in TON), gets compensated in Jettons + +This pattern enables users to pay gas in Jettons instead of TON. + +### Example 3: Server-controlled operations + +**How it works:** + +1. User requests authorization from server +1. Server validates request and signs authorization message (includes validity period and operation parameters) +1. User sends server-signed message to contract (with payment) +1. Contract verifies server's signature +1. Contract checks validity period +1. Contract performs authorized action (deploy, mint, claim) +1. If user tries to send same message again, contract ignores it (state already changed) + +**Key characteristics:** + +- **Who signs:** Server +- **Who sends:** User (internal message with payment) +- **Who pays gas:** User + +This pattern is useful when backend needs to authorize specific operations (auctions, mints, claims) without managing private keys for each user. + +**Real-world example:** [telemint contract](https://github.com/TelegramMessenger/telemint) uses server-signed messages to authorize NFT deployments. + +## Message structure for signing + +When designing a signed message, you choose how to organize the signed data — the message fields that will be hashed and verified. The key question: is the signed data a **slice** (part of a cell) or a **cell** (separate cell)? This affects gas consumption during signature verification. + +### Approach 1: Signed data as slice + +After loading the signature from the message body, the signed data remains as a **slice** — a part of the cell that may contain additional data and references. + +**Used in:** Wallet v1-v5 + +**Schema — Wallet v3r2:** + +```tlb +msg_body$_ signature:bits512 subwallet_id:uint32 + seqno:uint32 valid_until:uint32 + mode:uint8 message_to_send:^Cell + = ExternalInMessage; +``` + +```mermaid +graph + subgraph Cell["Message body"] + direction LR + A[**signature**] + subgraph Signed["Signed data"] + direction LR + B[subwallet_id] + C[seqno] + D[valid_until] + E[mode] + F((message
to send)) + end + end + + A ~~~ B + B ~~~ C + C ~~~ D + D ~~~ E + E ~~~ F + +``` + +**Verification in FunC:** + +```func +slice signature = in_msg_body~load_bits(512); +slice signed_data = in_msg_body; // Remaining data + +int hash = slice_hash(signed_data); // 526 gas +throw_unless(35, check_signature(hash, signature, public_key)); +``` + +**Gas analysis:** + +After loading the signature, the remaining data is a **slice**. To verify the signature, the contract needs to hash this slice. In TVM, the method for hashing a slice is `slice_hash()`, which costs 526 gas. + +**Why expensive?**\ +`slice_hash()` internally rebuilds a cell from the slice, copying all data and references. + + + +### Approach 2: Signed data as cell + +The signed data is stored in a **separate cell**, placed as a reference in the message body. + +**Used in:** Preprocessed Wallet v2, Highload Wallet v3 + +**Schema — Preprocessed Wallet v2:** + +```tlb +_ valid_until:uint64 seqno:uint16 actions:^Cell = MsgInner; + +msg_body$_ signature:bits512 msg_inner:^MsgInner = ExternalInMessage; +``` + +```mermaid +graph LR + subgraph Root["Message body"] + direction LR + A[**signature**] + B((msg_inner)) + end + + subgraph Ref["Signed data (next cell)"] + direction LR + C[valid_until] + D[seqno] + E((actions)) + end + + A ~~~ B + B -.-> Ref + C ~~~ D + D ~~~ E + +``` + +**Verification in FunC:** + +```func +slice signature = in_msg_body~load_bits(512); +cell signed_data = in_msg_body~load_ref(); // Signed data as cell + +int hash = cell_hash(signed_data); // 26 gas +throw_unless(35, check_signature(hash, signature, public_key)); +``` + +**Gas analysis:** + +The signed data is loaded as a **cell** from the reference. To get its hash, the contract uses `cell_hash()`, which costs only 26 gas. + +**Why efficient?**\ +Every cell in TON stores its hash as metadata. `cell_hash()` reads this precomputed value directly — no rebuilding, no copying. + +**Trade-off:**\ +This approach adds one extra cell to the message, slightly increasing the forward fee. However, the gas savings (\~500 gas) outweigh the forward fee increase. + +### TVM v12 improvement + +TVM version 12 introduced efficient builder hashing (`HASHBU` instruction), which makes **signed data as slice** approach much more gas-efficient. + +**Verification in FunC (TVM v12+):** + +```func +slice signature = in_msg_body~load_bits(512); +slice signed_data = in_msg_body; + +builder b = begin_cell().store_slice(signed_data); +int hash = b.builder_hash(); +throw_unless(35, check_signature(hash, signature, public_key)); +``` + +**Gas comparison:** + +| Method | Gas cost | Notes | +| ----------------------- | --------- | -------------------------- | +| `slice_hash()` | 526 gas | Rebuilds cell from slice | +| Builder hashing (slice) | \<100 gas | v12+: cheap, uses HASHBU | +| `cell_hash()` (cell) | 26 gas | Uses precomputed cell hash | + +**Conclusion:**\ +In TVM v12+, both approaches are gas-efficient. New contracts can choose based on code simplicity and forward fee considerations. + +Reference: [GlobalVersions.md — TVM v12 updates](https://github.com/ton-blockchain/ton/blob/master/doc/GlobalVersions.md#new-tvm-instructions-4) + +## How to sign messages in TypeScript + +### Prerequisites + +- Node.js 18+ or TypeScript environment +- `@ton/core`, `@ton/crypto` packages installed + +Install required packages: + +```bash +npm install @ton/core @ton/crypto +``` + +### Step 1: Generate or load a mnemonic + +A mnemonic is your wallet's master secret. It derives the private key used to sign messages. + +**Generate a new mnemonic:** + +```typescript +import { mnemonicNew } from '@ton/crypto'; + +const mnemonic = await mnemonicNew(24); // Array of 24 words +``` + +**Load an existing mnemonic:** + +```typescript +const mnemonic = 'word1 word2 word3 ... word24'.split(' '); +``` + + + +### Step 2: Derive the keypair + +Convert the mnemonic to an Ed25519 keypair: + +```typescript +import { mnemonicToPrivateKey } from '@ton/crypto'; + +const keyPair = await mnemonicToPrivateKey(mnemonic); +// keyPair.publicKey — stored in contract +// keyPair.secretKey — used to sign messages +``` + +### Step 3: Build the signed data + +Build the message data that will be signed: + +```typescript +import { beginCell } from '@ton/core'; + +// Build signed data (example: wallet message) +const seqno = 5; +const validUntil = Math.floor(Date.now() / 1000) + 60; // 60 seconds from now + +const signedData = beginCell() + .storeUint(seqno, 32) + .storeUint(validUntil, 32) + // ... other fields (subwallet_id, actions, etc.) + .endCell(); +``` + +### Step 4: Create the signature + +Sign the hash of the signed data: + +```typescript +import { sign } from '@ton/crypto'; + +const signature = sign(signedData.hash(), keyPair.secretKey); +// signature is a Buffer with 512 bits +``` + +### Step 5: Build the message body + +Choose the structure based on your contract (see [Message structure for signing](#message-structure-for-signing) above): + +```typescript +// Approach 1: Signed data as slice +const messageBodyInline = beginCell() + .storeBuffer(signature) // 512 bits + .storeSlice(signedData.asSlice()) // Signed data as slice + .endCell(); + +// Approach 2: Signed data as cell +const messageBodySeparate = beginCell() + .storeBuffer(signature) // 512 bits + .storeRef(signedData) // Signed data as cell + .endCell(); +```