diff --git a/docs.json b/docs.json index f067a5821..9f0f9393a 100644 --- a/docs.json +++ b/docs.json @@ -697,7 +697,17 @@ "foundations/shards", "foundations/limits", "foundations/config", - "foundations/services", + { + "group": "Web3 services", + "pages": [ + "foundations/services", + "foundations/web3/ton-dns", + "foundations/web3/ton-storage", + "foundations/web3/ton-proxy", + "foundations/web3/ton-sites", + "foundations/web3/ton-payments" + ] + }, { "group": "Merkle proofs", "pages": [ diff --git a/foundations/services.mdx b/foundations/services.mdx index 0920aff44..e3cff839d 100644 --- a/foundations/services.mdx +++ b/foundations/services.mdx @@ -1,5 +1,5 @@ --- -title: "Web3 services" +title: "Overview" --- import { Image } from '/snippets/image.jsx'; diff --git a/foundations/web3/ton-dns.mdx b/foundations/web3/ton-dns.mdx new file mode 100644 index 000000000..8dc1a2aa9 --- /dev/null +++ b/foundations/web3/ton-dns.mdx @@ -0,0 +1,653 @@ +--- +title: "TON DNS" +description: "On-chain hierarchical domain name service for TON Network" +--- + +import { Aside } from '/snippets/aside.jsx'; + +TON DNS is a service for translating human-readable domain names (such as `example.ton` or `mysite.temp.ton`) into TON smart contract addresses, ADNL addresses used by services on the TON Network, and other network entities. + +## What TON DNS resolves + +TON DNS records can point to several types of addresses: + +- **Wallet addresses**: on-chain smart contract addresses stored under the `wallet` category, allowing users to send cryptocurrency directly to a human-readable domain name instead of a raw address. +- **ADNL addresses**: used to locate TON Sites running on the TON Network. +- **TON Storage Bag IDs**: identifiers for files and data stored via TON Storage. +- **Next resolver references**: delegation of subdomains to another DNS smart contract. +- **Text records**: arbitrary UTF-8 text associated with a domain name. + +## Implementation + +TON DNS is built as a tree of DNS smart contracts. The root contract lives on the masterchain; its address is stored in blockchain configuration parameter #4. `.ton` is the only first-level domain. Each `.ton` domain is an NFT following the TEP-0081 standard: the resolver contract acts as an NFT collection and each registered domain is an NFT item, making domains transferable through any NFT-compatible wallet or marketplace. + +The `.t.me` namespace operates as a delegated sub-resolver on the same infrastructure, implementing the same `dnsresolve` interface and supporting the same record types as `.ton` domains. + +Every DNS smart contract exposes a `dnsresolve` get-method: + +```func +(int, cell) dnsresolve(slice subdomain, int category) +``` + +`subdomain` is the portion of the domain name remaining to be resolved. `category` is the SHA-256 hash of the record category name, or `0` to retrieve all records. The method returns the number of bits consumed and a cell containing the DNS record. If fewer bits were consumed than the full subdomain length, the returned cell is a `dns_next_resolver` pointing to the contract that handles the remainder. + +## DNS record types + +DNS record values are described by TL-B schemas. The following record types are defined. + +**`dns_adnl_address`**: stores a 256-bit ADNL address for network services such as TON Sites. An optional protocol list describes supported protocols. + +```tlb +dns_adnl_address#ad01 adnl_addr:bits256 flags:(## 8) { flags <= 1 } + proto_list:flags . 0?ProtoList = DNSRecord; +``` + +**`dns_smc_address`**: stores any smart contract address on the TON Blockchain, including wallets. An optional capability list describes the capabilities of the contract. Under the standard `wallet` category, this record enables sending funds to a domain name. + +```tlb +dns_smc_address#9fd3 smc_addr:MsgAddressInt flags:(## 8) { flags <= 1 } + cap_list:flags . 0?SmcCapList = DNSRecord; +``` + +**`dns_next_resolver`**: delegates resolution of subdomains to another DNS smart contract at the specified address. + +```tlb +dns_next_resolver#ba93 resolver:MsgAddressInt = DNSRecord; +``` + +**`dns_storage_address`**: stores a 256-bit TON Storage Bag ID, allowing domain names to be assigned to files stored via TON Storage. + +```tlb +dns_storage_address#7473 bag_id:bits256 = DNSRecord; +``` + +**`dns_text`**: stores arbitrary UTF-8 text using the `Text` chunked encoding defined in the block layout specification. + +```tlb +dns_text#1eda _:Text = DNSRecord; +``` + +### Record categories + +When querying a DNS contract, the caller specifies a category to select which record type to retrieve. Categories are identified by the SHA-256 hash of a UTF-8 category name string. The standard categories defined by TEP-0081 are: + +| Category name | Purpose | +|---|---| +| `wallet` | Default wallet address (`dns_smc_address`) | +| `site` | TON Site ADNL address (`dns_adnl_address`) | +| `dns_next_resolver` | Subdomain delegation (`dns_next_resolver`) | +| `storage` | TON Storage Bag ID (`dns_storage_address`) | + +Passing category `0` retrieves all records stored for the domain. + +## Smart contract interactions + +The `.ton` DNS collection contract address is stored in blockchain configuration parameter #4. Each registered domain is an NFT item contract deployed by the collection. All operations (registration, bidding, record updates, and renewal) are performed by sending messages to these contracts. + +### Minimum price by domain length + +The minimum price to register or re-auction a domain depends on the domain name length. + +| Domain length | Minimum price | +|---|---| +| 4 characters | 100 TON | +| 5 characters | 50 TON | +| 6 characters | 40 TON | +| 7 characters | 30 TON | +| 8 characters | 20 TON | +| 9 characters | 10 TON | +| 10 characters | 5 TON | +| 11+ characters | 1 TON | + +### Prerequisites + +- [Node.js](https://nodejs.org/en/download) 22+ +- Packages: [`@ton/ton`](https://github.com/ton-org/ton), [`@ton/core`](https://github.com/ton-org/ton-core), [`@ton/crypto`](https://github.com/ton-org/ton-crypto) +- A funded Testnet wallet mnemonic in the `MNEMONIC` environment variable for write operations +- The sending wallet must be the domain NFT owner for record updates; any account can send a renewal + + + +### Register a domain + +To register an unclaimed `.ton` domain, send a text comment message (op `0`) to the collection contract with the domain name as the comment body. The attached TON value must meet the minimum price for the domain length. The collection contract deploys an NFT item contract and opens a one-hour auction with the sender as the initial bidder. + +```ts expandable +import { Address, beginCell, internal, toNano } from "@ton/core"; +import { TonClient, WalletContractV5R1, SendMode } from "@ton/ton"; +import { mnemonicToPrivateKey } from "@ton/crypto"; + +const collectionAddress = Address.parse(""); +const domainName = ""; + +async function main() { + const client = new TonClient({ + endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", + }); + + // Text comment: op=0 followed by domain name as UTF-8 + const body = beginCell() + .storeUint(0, 32) + .storeBuffer(Buffer.from(domainName)) + .endCell(); + + const msg = internal({ + to: collectionAddress, + value: toNano(""), + bounce: true, + body, + }); + + const mnemonic = process.env.MNEMONIC; + if (!mnemonic) { + throw new Error("Set MNEMONIC"); + } + const keyPair = await mnemonicToPrivateKey(mnemonic.split(" ")); + const walletContract = client.open( + WalletContractV5R1.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }), + ); + + const seqno = await walletContract.getSeqno(); + await walletContract.sendTransfer({ + seqno, + secretKey: keyPair.secretKey, + sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY, + messages: [msg], + }); +} + +void main(); +``` + +Where + +- ``: the `.ton` DNS collection contract address, stored in blockchain configuration parameter #4. +- ``: the bare domain label to register, for example `example` (without the `.ton` suffix). Must be 4–126 characters, lowercase letters, digits, and hyphens only. Hyphens cannot appear at the start or end of the name. +- ``: the initial bid in TON. Must meet the minimum price for the domain length. + +### Bid on an active auction + +To place a bid on a domain with an active auction, send a simple transfer (op `0`) to the domain NFT item contract. The attached TON value must be at least 5% higher than the current highest bid. The previous highest bidder receives a refund via an `outbid_notification` message. If a bid arrives within the last hour of the auction, the end time extends by one hour to allow counter-bids. + +```ts expandable +import { Address, beginCell, internal, toNano } from "@ton/core"; +import { TonClient, WalletContractV5R1, SendMode } from "@ton/ton"; +import { mnemonicToPrivateKey } from "@ton/crypto"; + +const domainNftAddress = Address.parse(""); + +async function main() { + const client = new TonClient({ + endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", + }); + + // Simple transfer with op=0 + const body = beginCell() + .storeUint(0, 32) + .endCell(); + + const msg = internal({ + to: domainNftAddress, + value: toNano(""), + bounce: true, + body, + }); + + const mnemonic = process.env.MNEMONIC; + if (!mnemonic) { + throw new Error("Set MNEMONIC"); + } + const keyPair = await mnemonicToPrivateKey(mnemonic.split(" ")); + const walletContract = client.open( + WalletContractV5R1.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }), + ); + + const seqno = await walletContract.getSeqno(); + await walletContract.sendTransfer({ + seqno, + secretKey: keyPair.secretKey, + sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY, + messages: [msg], + }); +} + +void main(); +``` + +Where + +- ``: the address of the domain NFT item contract with an active auction. Obtain it by calling `get_nft_address_by_index(slice_hash(""))` on the collection contract. +- ``: the bid in TON. Must be at least 5% higher than the current highest bid. + +### Release an expired domain + +A domain that has not been renewed for more than one year can be released by sending a `dns_balance_release` message (op `0x4ed14b65`) to the domain NFT item contract. The attached TON value must meet the minimum price for the domain length. The sender becomes the initial bidder in a new one-week auction. The previous owner receives the remaining contract balance. + +```ts expandable +import { Address, beginCell, internal, toNano } from "@ton/core"; +import { TonClient, WalletContractV5R1, SendMode } from "@ton/ton"; +import { mnemonicToPrivateKey } from "@ton/crypto"; + +const domainNftAddress = Address.parse(""); + +async function main() { + const client = new TonClient({ + endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", + }); + + // dns_balance_release#4ed14b65 + const body = beginCell() + .storeUint(0x4ed14b65, 32) + // query_id + .storeUint(0, 64) + .endCell(); + + const msg = internal({ + to: domainNftAddress, + value: toNano(""), + bounce: true, + body, + }); + + const mnemonic = process.env.MNEMONIC; + if (!mnemonic) { + throw new Error("Set MNEMONIC"); + } + const keyPair = await mnemonicToPrivateKey(mnemonic.split(" ")); + const walletContract = client.open( + WalletContractV5R1.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }), + ); + + const seqno = await walletContract.getSeqno(); + await walletContract.sendTransfer({ + seqno, + secretKey: keyPair.secretKey, + sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY, + messages: [msg], + }); +} + +void main(); +``` + +Where + +- ``: the address of the expired domain NFT item contract. +- ``: the bid in TON. Must meet the minimum price for the domain length. + +### Resolve a domain + +The following example resolves a `.ton` domain to a DNS record by following the `dnsresolve` chain from the root contract. Domain names are encoded by reversing labels and null-terminating each one: `example.ton` becomes `ton\0example\0`. + +```ts expandable +import { Address, beginCell, Cell } from "@ton/core"; +import { TonClient } from "@ton/ton"; +import { sha256_sync } from "@ton/crypto"; + +const rootDnsAddress = Address.parse(""); +const domain = ""; +const category = ""; + +function encodeDomain(name: string): Buffer { + return Buffer.from( + name.split(".").reverse().join("\0") + "\0", + "ascii", + ); +} + +async function main() { + const client = new TonClient({ + endpoint: "https://toncenter.com/api/v2/jsonRPC", + }); + + const categoryKey = BigInt( + "0x" + sha256_sync(Buffer.from(category)).toString("hex"), + ); + + let contractAddress = rootDnsAddress; + let remaining = encodeDomain(domain); + let record: Cell | null = null; + + for (let depth = 0; depth < 10; depth++) { + const { stack } = await client.runMethod( + contractAddress, + "dnsresolve", + [ + { type: "slice", cell: beginCell().storeBuffer(remaining).endCell() }, + { type: "int", value: categoryKey }, + ], + ); + + const bitsConsumed = stack.readNumber(); + const cell = stack.readCellOpt(); + + if (!cell || bitsConsumed === 0) break; + + const bytesConsumed = bitsConsumed / 8; + if (bytesConsumed >= remaining.length) { + // Fully resolved: cell is the final DNS record value + record = cell; + break; + } + + // Partial match: follow dns_next_resolver#ba93 + const cs = cell.beginParse(); + if (cs.loadUint(16) !== 0xba93) break; + contractAddress = cs.loadAddress(); + remaining = remaining.subarray(bytesConsumed); + } + + console.log(record); +} + +void main(); +``` + +Where + +- ``: the root DNS contract address, stored in blockchain configuration parameter #4. +- ``: the domain to resolve, for example `example.ton`. +- ``: the record category name, for example `wallet` or `site`. The returned cell is a raw `DNSRecord` value. Parse it according to the TL-B type for the requested category. + +### Set a wallet address record + +The following example writes a `dns_smc_address` value for the `wallet` category, linking a wallet address to the domain. + +```ts expandable +import { Address, beginCell, internal, toNano } from "@ton/core"; +import { TonClient, WalletContractV5R1, SendMode } from "@ton/ton"; +import { mnemonicToPrivateKey, sha256_sync } from "@ton/crypto"; + +const domainNftAddress = Address.parse(""); +const walletToLink = Address.parse(""); + +async function main() { + const client = new TonClient({ + endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", + }); + + // Category key: SHA-256 of "wallet" + const categoryKey = BigInt( + "0x" + sha256_sync(Buffer.from("wallet")).toString("hex"), + ); + + // DNS record value: dns_smc_address#9fd3 + const recordValue = beginCell() + // dns_smc_address tag + .storeUint(0x9fd3, 16) + .storeAddress(walletToLink) + // flags = 0, no capability list + .storeUint(0, 8) + .endCell(); + + // change_dns_record#4eb1f0f9 + const body = beginCell() + // op::change_dns_record + .storeUint(0x4eb1f0f9, 32) + // query_id + .storeUint(0, 64) + // record key + .storeUint(categoryKey, 256) + // record value cell + .storeRef(recordValue) + .endCell(); + + const msg = internal({ + to: domainNftAddress, + value: toNano("0.05"), + bounce: true, + body, + }); + + const mnemonic = process.env.MNEMONIC; + if (!mnemonic) { + throw new Error("Set MNEMONIC"); + } + const keyPair = await mnemonicToPrivateKey(mnemonic.split(" ")); + const walletContract = client.open( + WalletContractV5R1.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }), + ); + + const seqno = await walletContract.getSeqno(); + await walletContract.sendTransfer({ + seqno, + secretKey: keyPair.secretKey, + sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY, + messages: [msg], + }); +} + +void main(); +``` + +Where + +- ``: the address of the `.ton` domain NFT item contract. Obtain it by calling `get_nft_address_by_index(slice_hash(""))` on the `.ton` collection contract, whose address is stored in blockchain configuration parameter #4. +- ``: the wallet address the domain should resolve to. + +### Set a site ADNL address record + +The following example writes a `dns_adnl_address` value for the `site` category, linking an ADNL address to the domain. + +```ts expandable +import { Address, beginCell, internal, toNano } from "@ton/core"; +import { TonClient, WalletContractV5R1, SendMode } from "@ton/ton"; +import { mnemonicToPrivateKey, sha256_sync } from "@ton/crypto"; + +const domainNftAddress = Address.parse(""); +const adnlAddress = ""; + +async function main() { + const client = new TonClient({ + endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", + }); + + // Category key: SHA-256 of "site" + const categoryKey = BigInt( + "0x" + sha256_sync(Buffer.from("site")).toString("hex"), + ); + + // DNS record value: dns_adnl_address#ad01 + const recordValue = beginCell() + // dns_adnl_address tag + .storeUint(0xad01, 16) + // adnl_addr:bits256 + .storeUint(BigInt("0x" + adnlAddress), 256) + // flags = 0, no proto list + .storeUint(0, 8) + .endCell(); + + // change_dns_record#4eb1f0f9 + const body = beginCell() + .storeUint(0x4eb1f0f9, 32) + .storeUint(0, 64) + .storeUint(categoryKey, 256) + .storeRef(recordValue) + .endCell(); + + const msg = internal({ + to: domainNftAddress, + value: toNano("0.05"), + bounce: true, + body, + }); + + const mnemonic = process.env.MNEMONIC; + if (!mnemonic) { + throw new Error("Set MNEMONIC"); + } + const keyPair = await mnemonicToPrivateKey(mnemonic.split(" ")); + const walletContract = client.open( + WalletContractV5R1.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }), + ); + + const seqno = await walletContract.getSeqno(); + await walletContract.sendTransfer({ + seqno, + secretKey: keyPair.secretKey, + sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY, + messages: [msg], + }); +} + +void main(); +``` + +Where + +- ``: the address of the `.ton` domain NFT item contract. +- ``: a 64-character hex string representing the 256-bit ADNL address of the target TON Site. + +### Delete a DNS record + +To remove a record, send `change_dns_record` without a value cell reference. The contract interprets the absence of the reference as a deletion. + +```ts expandable +import { Address, beginCell, internal, toNano } from "@ton/core"; +import { TonClient, WalletContractV5R1, SendMode } from "@ton/ton"; +import { mnemonicToPrivateKey, sha256_sync } from "@ton/crypto"; + +const domainNftAddress = Address.parse(""); +const categoryName = ""; + +async function main() { + const client = new TonClient({ + endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", + }); + + const categoryKey = BigInt( + "0x" + sha256_sync(Buffer.from(categoryName)).toString("hex"), + ); + + // No storeRef: absence of value cell deletes the record + const body = beginCell() + .storeUint(0x4eb1f0f9, 32) + .storeUint(0, 64) + .storeUint(categoryKey, 256) + .endCell(); + + const msg = internal({ + to: domainNftAddress, + value: toNano("0.05"), + bounce: true, + body, + }); + + const mnemonic = process.env.MNEMONIC; + if (!mnemonic) { + throw new Error("Set MNEMONIC"); + } + const keyPair = await mnemonicToPrivateKey(mnemonic.split(" ")); + const walletContract = client.open( + WalletContractV5R1.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }), + ); + + const seqno = await walletContract.getSeqno(); + await walletContract.sendTransfer({ + seqno, + secretKey: keyPair.secretKey, + sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY, + messages: [msg], + }); +} + +void main(); +``` + +Where + +- ``: the address of the `.ton` domain NFT item contract. +- ``: the category to delete, for example `wallet` or `site`. + +### Renew a domain + +To extend a domain for one year, send a `fill_up` message with 0.015 TON to the domain NFT item contract. Any account can send this message. + +```ts expandable +import { Address, beginCell, internal, toNano } from "@ton/core"; +import { TonClient, WalletContractV5R1, SendMode } from "@ton/ton"; +import { mnemonicToPrivateKey } from "@ton/crypto"; + +const domainNftAddress = Address.parse(""); + +async function main() { + const client = new TonClient({ + endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", + }); + + // fill_up#370fec51 + const body = beginCell() + .storeUint(0x370fec51, 32) + .endCell(); + + const msg = internal({ + to: domainNftAddress, + value: toNano("0.015"), + bounce: true, + body, + }); + + const mnemonic = process.env.MNEMONIC; + if (!mnemonic) { + throw new Error("Set MNEMONIC"); + } + const keyPair = await mnemonicToPrivateKey(mnemonic.split(" ")); + const walletContract = client.open( + WalletContractV5R1.create({ + workchain: 0, + publicKey: keyPair.publicKey, + }), + ); + + const seqno = await walletContract.getSeqno(); + await walletContract.sendTransfer({ + seqno, + secretKey: keyPair.secretKey, + sendMode: SendMode.IGNORE_ERRORS | SendMode.PAY_GAS_SEPARATELY, + messages: [msg], + }); +} + +void main(); +``` + +Where + +- ``: the address of the `.ton` domain NFT item contract to renew. + +## Domain lifecycle + +Registered domains must be renewed annually by sending 0.015 TON to the domain contract. There is no grace period: once a domain has gone more than one year without renewal, anyone can trigger its release by calling `dns_balance_release` on the contract with a minimum payment. The domain then re-enters auction for one week, with the caller as the initial bidder. + +## Ecosystem integration + +TON domain names such as `.ton` domains are recognized by wallet applications and explorers in the TON ecosystem. Users can send cryptocurrency to a TON DNS domain name instead of copying a 256-bit account identifier. + +TON DNS also integrates with other TON services: + +- **[TON Sites](/foundations/web3/ton-sites)**: a `dns_adnl_address` record pointing to an ADNL address gives a site a human-readable `.ton` domain. +- **[TON Storage](/foundations/web3/ton-storage)**: a `dns_storage_address` record maps a domain name to a Bag ID, making stored files addressable by name. +- **[TON Proxy](/foundations/web3/ton-proxy)**: resolves `.ton` domains to ADNL addresses before routing HTTP traffic to the target site. diff --git a/foundations/web3/ton-payments.mdx b/foundations/web3/ton-payments.mdx new file mode 100644 index 000000000..908786c3e --- /dev/null +++ b/foundations/web3/ton-payments.mdx @@ -0,0 +1,84 @@ +--- +title: "TON Payments" +description: "Off-chain payment channels for instant value transfers between two parties" +--- + +import { Aside } from '/snippets/aside.jsx'; + +TON Payments enables instant off-chain value transfers between two parties through payment channels. A payment channel is a smart contract that holds a shared pool of funds. The two parties exchange signed balance updates off-chain, without submitting transactions to the blockchain, and only settle on-chain when the channel closes. + +## Payment channels + +A payment channel involves two parties, A and B, who deploy a shared smart contract and each deposit funds into it. Once funded, the parties can transfer value by exchanging signed messages. These off-chain updates are instant and cost no gas. + +## Channel flow + +### Open + +Both parties deploy the channel contract with a configuration that contains their public keys, payout addresses, and timeout parameters. Each party then sends a deposit transaction to the contract. The contract tracks how much each party deposited and waits for both signatures before activating. + +If only one party deposits and the `init_timeout` expires, either party can reclaim the deposited funds. + +### Transfer + +Transfers happen entirely off-chain through signed promises. + +A promise is a small signed message containing three fields: the `channel_id`, `promise_A` (cumulative total that A owes B), and `promise_B` (cumulative total that B owes A). Both parties keep a copy of the latest signed promise. + +To transfer 5 TON from A to B, party A creates a new promise where `promise_A` is increased by 5, signs it, and sends it to B. Party B verifies the signature and stores the updated promise. To transfer in the other direction, B increases `promise_B` and signs. + +Promises are monotonically increasing. Each new promise can only raise the amounts, never lower them. This prevents either party from rolling back a previous transfer. The net balance shift at any point is `promise_B - promise_A`, clamped to the funds available in the channel. + +### Signing + +Both public keys are stored in `ChanConfig` at deployment (`a_key` and `b_key`, 256 bits each). To sign a promise, a party serializes the promise content into a cell, hashes it with SHA-256 (`slice_hash()`), and signs the hash with its Ed25519 private key. The resulting signature is 512 bits. + +```tlb +chan_signed_promise#_ sig:(Maybe ^bits512) promise:ChanPromise = ChanSignedPromise; +``` + +The `Maybe` field means the signature is optional, which allows unilateral close with only one party's signature. On-chain, the contract calls the TVM primitive `check_signature(hash, sig, key)` to verify Ed25519 signatures natively. Invalid signatures produce error 31 (`wrong_a_signature`) or 32 (`wrong_b_signature`). + +### Close + +When both parties agree to close the channel, they co-sign a close message containing the latest promise. The contract verifies the signatures, computes the final balances, and distributes funds to both parties. + +## Dispute resolution + +If one party attempts to cheat or becomes unresponsive, the contract enforces resolution on-chain. + +### Unilateral close + +If party B stops responding, party A can submit a close message with only its own signature and the latest promise. The contract records the submitted state and starts a countdown of `close_timeout` seconds. + +During this window, party B can submit its own version of the promise. The contract keeps whichever promise is higher for each side, since promises can only increase. + +If party B responds and both signatures are collected, the channel settles immediately with the final balances. + +### Timeout expiry + +If the `close_timeout` expires without a response from party B, either party can send a timeout message. The contract then computes the final balances using whichever promises were submitted and distributes the funds. + +An unresponsive party can delay settlement by at most `close_timeout` seconds, but cannot prevent it. + +### Init timeout + +The same mechanism protects the opening phase. If the channel configuration specifies an `init_timeout` and only one party has deposited by that deadline, a timeout message returns the deposited funds to their owner. + +## Final balance computation + +When the channel settles, the contract computes balances from the accumulated promises: + +``` +diff = promise_B - promise_A +diff = max(diff, -A) +diff = min(diff, B) +final_A = A + diff +final_B = B - diff +``` + +Where `A` and `B` are the on-chain deposited amounts. The clamping ensures that neither party can owe more than what is available in the channel. + + diff --git a/foundations/web3/ton-proxy.mdx b/foundations/web3/ton-proxy.mdx new file mode 100644 index 000000000..d48aed119 --- /dev/null +++ b/foundations/web3/ton-proxy.mdx @@ -0,0 +1,136 @@ +--- +title: "TON Proxy" +description: "HTTP-to-ADNL bridge for accessing and hosting TON Sites from a conventional browser" +--- + +TON Proxy is an HTTP-to-ADNL bridge that allows ordinary web browsers to access TON Sites. It is implemented by the `rldp-http-proxy` binary, which operates as a forward proxy for clients and as a reverse proxy for servers hosting TON Sites. + +## How TON Proxy works + +`rldp-http-proxy` runs locally and listens for HTTP requests from a web browser. When a request arrives for a `.ton` domain, the proxy resolves it via TON DNS to an ADNL address, then forwards the HTTP request over RLDP to the target TON Site. + +By default the forward proxy only intercepts requests for `.ton`, `.adnl`, and `.bag` hostnames; all other requests pass through unchanged. Setting `-P YES` makes the proxy intercept all HTTP requests regardless of suffix. + +### Forward proxy (client-side) + +To start a local forward proxy: + +```bash +rldp-http-proxy -p 8080 -c 3333 -C global.config.json +``` + +Configure the browser to use `127.0.0.1:8080` as an HTTP proxy. TON Sites are then reachable by their `.ton` domain names. + +| Flag | Long form | Description | +|---|---|---| +| `-p ` | `--port` | HTTP listening port for browser connections | +| `-c ` | `--client-port` | UDP port for client ADNL queries | +| `-C ` | `--global-config` | Path to the TON global network config file | +| `-P ` | `--proxy-all` | Proxy all HTTP requests, not only `.ton`, `.adnl`, `.bag` (default `NO`) | +| `-S ` | `--storage-gateway` | ADNL address of a TON Storage gateway for `.bag` resolution | +| `-D ` | `--db` | Database root path | +| `-d` | `--daemonize` | Daemonize the process | +| `-l ` | `--logname` | Log file path | + +## Reverse proxy mode + +`rldp-http-proxy` also operates as a reverse proxy for servers hosting TON Sites. In this mode it accepts inbound ADNL connections and forwards HTTP requests to a local or remote web server. Two implementations are available. + +### Use rldp-http-proxy + +`rldp-http-proxy` is the reverse proxy from the official TON monorepo. Key generation is manual. + +**Step 1.** Generate a persistent ADNL address: + +```bash +mkdir keyring +utils/generate-random-id -m adnlid +``` + +The command prints two values to stdout: the hex address and its user-friendly form: + +```text +45061C1D4EC44A937D0318589E13C73D151D1CEF5D3C0E53AFBCF56A6C2FE2BD vcqmha5j3ceve35ammfrhqty46rkhi455otydstv66pk2tmf7rl25f3 +``` + +It also writes the private key to a file named after the hex address. Move it into the keyring directory: + +```bash +mv 45061C1D* keyring/ +``` + +**Step 2.** Start the reverse proxy, using the user-friendly ADNL address from step 1: + +```bash +rldp-http-proxy -a :3333 -L '*' -C global.config.json -A -d -l tonsite.log +``` + +| Flag | Description | +|---|---| +| `-a :` | Public IP and UDP port for inbound ADNL connections (published to the TON DHT) | +| `-A ` | ADNL address generated in step 1 | +| `-L [:]` | Forward requests for `` to `127.0.0.1` (default ports: 80, 443) | +| `-R [:]@:` | Forward requests for `` to a remote HTTP server at `:` | +| `-C ` | Path to the TON global network configuration file | +| `-D ` | Database root path | +| `-d` | Daemonize the process | +| `-l ` | Log file path | + +### Use tonutils-reverse-proxy + +`tonutils-reverse-proxy` is a Go implementation that handles key generation and domain linking automatically. + +**Install on Linux:** + +```bash +wget https://github.com/tonutils/reverse-proxy/releases/latest/download/tonutils-reverse-proxy-linux-amd64 +chmod +x tonutils-reverse-proxy-linux-amd64 +``` + +To build from source: + +```bash +git clone https://github.com/tonutils/reverse-proxy +cd reverse-proxy && make build +``` + +Run with the target `.ton` domain: + +```bash +./tonutils-reverse-proxy --domain .ton +``` + +On first run, the proxy generates a persistent ADNL key pair and displays a QR code. Scan it with a compatible TON wallet (such as Tonkeeper) to confirm domain ownership and link the ADNL address to the domain. + +The web server must listen on `http://127.0.0.1:80`. The proxy adds two headers to each forwarded request: + +- `X-Adnl-Ip`: the IP address of the connecting client as seen by the ADNL network. +- `X-Adnl-Id`: the ADNL node ID of the connecting client. + +### Domain assignment + +To assign the ADNL address to a `.ton` domain, open the domain in the TON DNS management interface, paste the ADNL address into the "Site" field, and confirm the transaction with the domain owner's wallet. For the full procedure, see [Set a site ADNL address record](/foundations/web3/ton-dns#set-a-site-adnl-address-record). + +## Security and privacy + +All traffic between the proxy and the TON Site is encrypted at the ADNL layer. The server is authenticated by its ADNL address, derived from its public key. + +The server IP is published to the TON DHT for ADNL routing but is not exposed at the HTTP layer. The proxy does not forward client network information to the upstream web server. + +## Response headers + +The proxy adds version headers to all responses. + +| Header | Added by | Value format | +|---|---|---| +| `Ton-Proxy-Site-Version` | Reverse proxy (server-side) | `Commit: , Date: ` | +| `Ton-Proxy-Entry-Version` | Forward proxy (client-side) | `Commit: , Date: ` | + +The proxy also supports the HTTP `CONNECT` method, which enables WebSocket connections and other TCP-based protocols to be tunneled over ADNL. + +## Related components + +- **ADNL**: the abstract datagram network layer used to reach TON Sites by their abstract address. +- **RLDP**: the reliable large datagram protocol over ADNL that carries HTTP requests and responses. +- **[TON Sites](/foundations/web3/ton-sites)**: web services accessible through TON Proxy. +- **[TON DNS](/foundations/web3/ton-dns)**: resolves `.ton` domain names to ADNL addresses for request routing. diff --git a/foundations/web3/ton-sites.mdx b/foundations/web3/ton-sites.mdx new file mode 100644 index 000000000..c13401aa6 --- /dev/null +++ b/foundations/web3/ton-sites.mdx @@ -0,0 +1,115 @@ +--- +title: "TON Sites" +description: "Web services accessible through TON Network via ADNL and RLDP" +--- + +TON Sites are web services that support an HTTP interface. They use RLDP datagrams over the ADNL protocol to transfer HTTP queries and responses, residing entirely within the TON Network. A TON Site is identified by its ADNL address rather than an IP address, so no IP address appears in the URL or at the HTTP layer. + +## How TON Sites work + +When a user visits a TON Site: + +1. The TON Proxy client accepts an HTTP request from an ordinary web browser. +2. The proxy resolves the target address, either through TON DNS (for human-readable `.ton` domains) or directly from a known ADNL address. +3. The HTTP request is encapsulated in RLDP datagrams and sent through the ADNL network to the TON Site's abstract address. +4. The TON Site processes the request and returns an HTTP response. + +## ADNL address as identity + +Every TON Site is identified by a 256-bit ADNL abstract address derived from its public key. Because the ADNL address is independent of the server's IP address, a site is not tied to any particular server location. + +## Host a TON Site + +A TON Site requires a reverse proxy that accepts inbound ADNL connections and forwards them to a local web server. Two implementations are available. + +### Use rldp-http-proxy + +`rldp-http-proxy` is the reverse proxy from the official TON monorepo. Key generation is manual. + +**Step 1.** Generate a persistent ADNL address: + +```bash +mkdir keyring +utils/generate-random-id -m adnlid +``` + +The command prints two values to stdout: the hex address and its user-friendly form: + +```text +45061C1D4EC44A937D0318589E13C73D151D1CEF5D3C0E53AFBCF56A6C2FE2BD vcqmha5j3ceve35ammfrhqty46rkhi455otydstv66pk2tmf7rl25f3 +``` + +It also writes the private key to a file named after the hex address. Move it into the keyring directory: + +```bash +mv 45061C1D* keyring/ +``` + +**Step 2.** Start the reverse proxy, using the user-friendly ADNL address from step 1: + +```bash +rldp-http-proxy -a :3333 -L '*' -C global.config.json -A -d -l tonsite.log +``` + +| Flag | Description | +|---|---| +| `-a :` | Public IP and UDP port for inbound ADNL connections (published to the TON DHT) | +| `-A ` | ADNL address generated in step 1 | +| `-L [:]` | Forward requests for `` to `127.0.0.1` (default ports: 80, 443) | +| `-R [:]@:` | Forward requests for `` to a remote HTTP server at `:` | +| `-C ` | Path to the TON global network configuration file | +| `-D ` | Database root path | +| `-d` | Daemonize the process | +| `-l ` | Log file path | + +### Use tonutils-reverse-proxy + +`tonutils-reverse-proxy` is a Go implementation that handles key generation and domain linking automatically. + +**Install on Linux:** + +```bash +wget https://github.com/tonutils/reverse-proxy/releases/latest/download/tonutils-reverse-proxy-linux-amd64 +chmod +x tonutils-reverse-proxy-linux-amd64 +``` + +To build from source: + +```bash +git clone https://github.com/tonutils/reverse-proxy +cd reverse-proxy && make build +``` + +Run with the target `.ton` domain: + +```bash +./tonutils-reverse-proxy --domain .ton +``` + +On first run, the proxy generates a persistent ADNL key pair and displays a QR code. Scan it with a compatible TON wallet (such as Tonkeeper) to confirm domain ownership and link the ADNL address to the domain. + +The web server must listen on `http://127.0.0.1:80`. The proxy adds two headers to each forwarded request: + +- `X-Adnl-Ip`: the IP address of the connecting client as seen by the ADNL network. +- `X-Adnl-Id`: the ADNL node ID of the connecting client. + +### Domain assignment + +To assign the ADNL address to a `.ton` domain, open the domain in the TON DNS management interface, paste the ADNL address into the "Site" field, and confirm the transaction with the domain owner's wallet. For the full procedure, see [Set a site ADNL address record](/foundations/web3/ton-dns#set-a-site-adnl-address-record). + +## Domain name resolution + +### TON DNS + +A `dns_adnl_address` record in TON DNS maps a `.ton` domain name to the ADNL address of the TON Site. The proxy client resolves this record and connects to the correct ADNL address. See [TON DNS](/foundations/web3/ton-dns) for the full record type specification. + +### Direct access via ADNL address + +TON Sites can also be accessed directly by their ADNL address, without DNS resolution. + +## Related components + +- **ADNL**: the abstract datagram network layer providing transport for TON Sites. +- **RLDP**: the reliable large datagram protocol over ADNL for HTTP transfer. +- **[TON Proxy](/foundations/web3/ton-proxy)**: bridges HTTP requests from a conventional browser to the ADNL network. +- **[TON DNS](/foundations/web3/ton-dns)**: maps `.ton` domains to ADNL addresses. diff --git a/foundations/web3/ton-storage.mdx b/foundations/web3/ton-storage.mdx new file mode 100644 index 000000000..965d20725 --- /dev/null +++ b/foundations/web3/ton-storage.mdx @@ -0,0 +1,202 @@ +--- +title: "TON Storage" +description: "Distributed file storage for the TON Network using torrent-like technology and on-chain payment contracts" +--- + +import { Aside } from '/snippets/aside.jsx'; + +TON Storage is a distributed file storage system on the TON Network. Files are shared using a torrent-like protocol, with optional on-chain smart contracts for paid storage guarantees. The TON Blockchain uses TON Storage to distribute archive copies of blocks and state snapshots. + +## Bags + +Files are organized into **bags**, each identified by a unique 256-bit `BagID` (the hash of the torrent info cell). A bag can contain a single file or a directory. + +Files in a bag are split into **128 KB chunks**. A Merkle tree built from SHA-256 hashes allows verification of individual chunks without downloading the full bag. Bag metadata can be exported as a metafile. + +## Peer discovery + +Nodes that store a bag register in the TON DHT under a key derived from the `BagID`. Clients query the DHT to find seeder addresses for a given bag. + +## Storage daemon + +`storage-daemon` is the official implementation, distributed as part of the TON software suite. To start: + +```bash +storage-daemon -v 3 -C global.config.json -I :3333 -p 5555 -D storage-db +``` + +### Manage bags + +| Command | Description | +|---|---| +| `create -d "description"` | Create a new bag from a file or directory | +| `add-by-hash -d ` | Add a bag by its `BagID` | +| `add-by-meta -d ` | Add a bag from a metafile | +| `list` | List all bags | +| `list --hashes` | List bags with their `BagID`s | +| `get ` | Show full bag information | +| `get-peers ` | Show peers connected for a bag | +| `get-meta ` | Export the bag metafile | +| `download-pause ` | Pause a download | +| `download-resume ` | Resume a download | +| `priority-name ` | Set download priority for a file (0 = skip, 255 = highest) | + +To download only specific files from a bag, use the `--partial` flag when adding: + +```bash +add-by-hash --partial file1.txt file2.txt +``` + +Files not listed are assigned priority 0 and are not downloaded. + +## Storage providers + +A storage provider is a node that stores bags for a fee, backed by an on-chain smart contract. The provider system has two components: + +- **Smart contract**: deployed on the TON Blockchain, stores the Merkle tree hash of each bag, issues proof challenges, and manages client payments. +- **`storage-daemon`**: runs on the provider's machine, downloads bags, serves data to peers, and submits storage proofs to the contract. + +### Provider workflow + +1. The provider deploys a main smart contract and shares its address with clients. +2. A client creates a bag and sends a storage request to the provider contract. +3. The provider contract deploys a per-bag storage contract and notifies the client. +4. The provider downloads the bag and activates the per-bag contract. +5. The client transfers payment. The provider submits periodic Merkle proofs to prove data possession. +6. When the client balance reaches zero or either party closes the contract, remaining funds return to the client and the contract self-destructs. + +Storage pricing is expressed in **nanoTON per megabyte per day**. + +### Provider commands + +| Command | Description | +|---|---| +| `deploy-provider` | Deploy the main provider smart contract | +| `get-provider-info` | Show current provider settings | +| `get-provider-info --contracts --balances` | List active storage contracts and balances | +| `set-provider-params --accept 1 --rate --max-span ` | Set pricing and acceptance policy | +| `set-provider-config --max-contracts --max-total-size ` | Set capacity limits | +| `new-contract-message --provider
` | Create a storage request message (client-side) | +| `get-provider-params
` | Retrieve on-chain provider parameters | +| `withdraw-all` | Withdraw accumulated provider earnings | +| `close-contract
` | Close a storage contract | + +## Smart contract reference + +The provider system uses two FunC contracts, available in the TON monorepo under [`storage/storage-daemon/smartcont`](https://github.com/ton-blockchain/ton/tree/master/storage/storage-daemon/smartcont). + +- `storage-provider.fc`: main provider contract (wallet with seqno authentication). Deploys per-bag contracts. +- `storage-contract.fc`: per-bag contract. Tracks client balance, enforces proof challenges, transfers earnings. + +### Contract state + +The per-bag contract stores state in a single cell: + +```tlb +storage_contract#_ active:Bool balance:Coins provider:MsgAddress + merkle_hash:uint256 file_size:uint64 next_proof_byte:uint64 + rate_per_mb_day:Coins max_span:uint32 last_proof_time:uint32 + ^[client:MsgAddress torrent_hash:uint256] = StorageContract; +``` + +The provider contract state: + +```tlb +storage_provider#_ seqno:uint32 subwallet:uint32 public_key:uint256 + accept_new_contracts:Bool rate_per_mb_day:Coins max_span:uint32 + minimal_file_size:uint64 maximal_file_size:uint64 = StorageProvider; +``` + +### Messages + +To request a new storage contract, a client sends `offer_storage_contract` to the provider address with a minimum attached value of 60,000,000 nanoTON: + +```tlb +torrent_info piece_size:uint32 file_size:uint64 root_hash:uint256 + header_size:uint64 header_hash:uint256 + description:Text = TorrentInfo; + +offer_storage_contract#107c49ef query_id:uint64 info:(^TorrentInfo) microchunk_hash:uint256 + expected_rate:Coins expected_max_span:uint32 = OfferStorageContract; +``` + +The following op codes are defined in `constants.fc` and used across both contracts: + +| Op code | Value | Direction | Description | +|---|---|---|---| +| `offer_storage_contract` | `0x107c49ef` | Client → Provider | Client requests a new per-bag contract | +| `deploy_storage_contract` | `0xe4748df1` | Provider → StorageContract | Provider deploys the per-bag contract | +| `contract_deployed` | `0xbf7bd0c1` | StorageContract → Client | Notifies the client that the contract is deployed | +| `accept_storage_contract` | `0x7a361688` | Provider → StorageContract | Provider activates the contract after downloading the bag | +| `storage_contract_confirmed` | `0xd4caedcd` | StorageContract → Client | Contract is active; client may top up balance | +| `proof_storage` | `0x419d5d4d` | Provider → StorageContract | Provider submits a Merkle proof to collect earnings | +| `withdraw` | `0x46ed2e94` | Provider → StorageContract | Provider withdraws accrued earnings | +| `reward_withdrawal` | `0xa91baf56` | StorageContract → Provider | Earnings transfer confirmation | +| `close_contract` | `0x79f937ea` | Client or Provider → StorageContract | Either party closes the contract | +| `storage_contract_terminated` | `0xb6236d63` | StorageContract → Client and Provider | Contract closed; remaining client balance returned | +| `update_pubkey` | `0x53f34cd6` | Internal | Provider contract rotates its signing key | +| `update_storage_params` | `0x54cbf19b` | Internal | Provider contract updates its parameters | + +### Proof challenge + +The provider must periodically prove it still holds the bag. The contract stores `next_proof_byte`, a random byte offset. To collect earnings, the provider sends `proof_storage` with an exotic cell (type 3, Merkle proof) rooted at `merkle_hash`, proving the 64-byte chunk at `next_proof_byte / 64` exists in the bag. + +On a valid proof, the contract computes the provider's earnings for the elapsed period: + +``` +bounty = (file_size × rate_per_mb_day × actual_span) ÷ (86400 × 1 048 576) +actual_span = min(now() − last_proof_time, max_span) +``` + +Where `file_size` is in bytes, `rate_per_mb_day` is in nanoTON per MiB per day, `actual_span` is in seconds, and `bounty` is in nanoTON. + +### Error codes + +| Code | Name | Condition | +|---|---|---| +| `401` | `unauthorized` | Sender is not authorized for the operation | +| `1001` | `not_enough_money` | Attached value is below the required minimum | +| `1002` | `wrong_proof` | The submitted Merkle proof is invalid | +| `1003` | `contract_not_active` | Operation requires an active contract | +| `1004` | `file_too_small` | File size is below the provider's `minimal_file_size` | +| `1005` | `file_too_big` | File size exceeds the provider's `maximal_file_size` | +| `1006` | `no_new_contracts` | Provider is not accepting new contracts | +| `1007` | `contract_already_active` | Contract has already been activated | +| `1008` | `no_microchunk_hash` | A microchunk hash is required but not present | +| `1009` | `provider_params_changed` | Offered rate or `max_span` does not match current provider parameters | + +### Get methods + +**Storage contract** (`storage-contract.fc`): + +| Method | Returns | Description | +|---|---|---| +| `get_storage_contract_data()` | `(active, balance, provider, merkle_hash, file_size, next_proof, rate_per_mb_day, max_span, last_proof_time, client, torrent_hash)` | Full contract state | +| `get_torrent_hash()` | `uint256` | Hash of the bag's torrent info cell | +| `is_active()` | `Bool` | Whether the contract is currently active | +| `get_next_proof_info()` | `(next_proof, last_proof_time, max_span)` | Proof challenge parameters | + +**Provider contract** (`storage-provider.fc`): + +| Method | Returns | Description | +|---|---|---| +| `seqno()` | `uint32` | Current wallet sequence number | +| `get_public_key()` | `uint256` | Provider's Ed25519 public key | +| `get_wallet_params()` | `(seqno, subwallet, public_key)` | Wallet authentication parameters | +| `get_storage_params()` | `(accept_new_contracts, rate_per_mb_day, max_span, minimal_file_size, maximal_file_size)` | Current provider configuration | +| `get_storage_contract_address(merkle_hash, file_size, client, torrent_hash)` | `slice` | Deterministic address of the per-bag contract for given parameters | + +## Integration with TON DNS + +A `.ton` domain can point to a bag using the `dns_storage_address` record: + +```tlb +dns_storage_address#7473 bag_id:bits256 = DNSRecord; +``` + +See [TON DNS](/foundations/web3/ton-dns#dns-record-types) for all record types. + +## Ecosystem use cases + +- **NFT metadata**: NFT collections can reference off-chain media and metadata stored as bags, using the `BagID` as a stable content identifier. Individual files within a bag are addressed using the `tonstorage:///path` URI scheme. +- **[Static TON Sites](/foundations/web3/ton-sites)**: a bag containing HTML and static assets can be served as a TON Site by combining TON Storage, [TON DNS](/foundations/web3/ton-dns), and [TON Proxy](/foundations/web3/ton-proxy).