diff --git a/contract-dev/techniques/contract-sharding.mdx b/contract-dev/techniques/contract-sharding.mdx index d910de600..9bf8525bf 100644 --- a/contract-dev/techniques/contract-sharding.mdx +++ b/contract-dev/techniques/contract-sharding.mdx @@ -5,80 +5,168 @@ title: "Contract sharding" import { Aside } from '/snippets/aside.jsx'; import { Image } from '/snippets/image.jsx'; -Some protocols need to store a lot of information in contracts, for example, tokens that have many users. In TON, there is a limit on how much can be stored in a single contract. The solution in TON is to split the data across many different contracts, where you can quickly find the right contract by a key and retrieve the required information from it. +Some protocols need to store a lot of information in contracts, for example, token contracts with many users. In TON, there is a limit on how much can be stored in a single contract. The solution is to split the data across multiple contracts. -In such protocols, there is a child contract that initially contains the information identified by a key. In some protocols, it is important to know the Parent contract, which acts as the information manager. +Each such contract, referred to as a _child_ contract, is associated with a key that determines its address within a parent contract. Some protocols also introduce a _parent_ contract that coordinates child contracts. -To avoid having to know the key upfront, we do not populate that field in `StateInit`; we only populate the key field. This makes it easy to locate the required contract later. +## Child contract address by key + +The contract address depends on the initial data provided in [`StateInit`](/foundations/messages/deploy). To ensure that a child contract can be accessed using only the key within a specific parent, the initial data includes the key and the parent address, but does not include the associated value. As a result, the address of the child contract can be determined from the parent address and key alone. +## NFT and jetton examples + +Consider NFTs: the collection acts as the parent contract, and each NFT item is a child contract. The key in this case is the item index, and only the collection can set the initial owner. + +For jettons, the parent contract is the minter, and the child contracts are user wallets. The key is the wallet's smart contract address. + +Both patterns follow the same principle: each key maps to a separate contract. In jetton protocols, there is a unique wallet contract per user, while in NFT collections, there is a unique item contract per NFT index, regardless of who owns the item. + Shard pattern -Consider NFTs: the collection serves as the Parent contract, and the NFT items are the child contracts. The key in this case is the index, and only a message from the collection can set the initial owner. +## Unbounded data structures -For Jettons, the Parent is the minter and the Children are user wallets. The key is the user's smart contract address, and the value is the user's token balance. +Contract sharding supports an unbounded number of potential child contracts. -In general, jettons and NFTs share this principle, but broadly speaking, jetton protocols have a unique contract per user, while NFTs have a single contract per item (by index) that is shared across all users. +In general, data structures that can scale to very large sizes are difficult to implement efficiently on blockchains. This pattern allows such scaling by distributing data across multiple contracts. -## Unbounded data structures +The following example shows a parent contract that deploys child contracts and assigns each child a sequence number. The shared storage file defines the data layouts and message types used by both contracts. -An interesting property of this pattern is that the number of potential children is unbounded! We can have an infinite number of children. +```tolk title="storage.tolk" +// Shared storage layouts and messages used by both contracts. -In general, infinite data structures that can actually scale to billions are very difficult to implement on blockchain efficiently. This pattern showcases the power of TON. +struct TodoChildStorage { + parentAddress: address + seqno: uint64 +} -```tact Tact -import "@stdlib/deploy"; +fun TodoChildStorage.load() { + return TodoChildStorage.fromCell(contract.getData()) +} -// we have multiple instances of the children -contract TodoChild { +fun TodoChildStorage.save(self) { + contract.setData(self.toCell()) +} - seqno: Int as uint64; +struct TodoParentStorage { + // Initialize this field in the parent's StateInit during deployment. + adminAddress: address + numChildren: uint64 = 0 + // Parent must know the child contract code to deploy new instances. + todoChildCode: cell +} - // when deploying an instance, we must specify its index (sequence number) - init(seqno: Int) { - self.seqno = seqno; - } +fun TodoParentStorage.load() { + return TodoParentStorage.fromCell(contract.getData()) +} - // this message handler will just debug print the seqno so we can see when it's called - receive("identify") { - dump(self.seqno); - } +fun TodoParentStorage.save(self) { + contract.setData(self.toCell()) } -// we have one instance of the parent -contract TodoParent with Deployable { +// Child prints its sequence number when it receives this message. +struct (0x49f29a21) Identify {} + +// Parent deploys another child when it receives this message. +struct (0x5b6f1392) DeployAnother {} +``` + +The child contract stores the parent address in its initial data and accepts the `Identify` message only from that parent. - numChildren: Int as uint64; +```tolk title="child.tolk" +import "storage" - init() { - self.numChildren = 0; +type TodoChildMessage = Identify + +fun onInternalMessage(in: InMessage) { + val msg = lazy TodoChildMessage.fromSlice(in.body); + + match (msg) { + Identify => { + val storage = lazy TodoChildStorage.load(); + assert (in.senderAddress == storage.parentAddress) throw 0xFFFF; + debug.print(storage.seqno); + } + else => { + // Ignore empty top-up messages, reject everything else. + assert (in.body.isEmpty()) throw 0xFFFF; + } } +} - // this message handler will cause the contract to deploy another child - receive("deploy another") { - self.numChildren = self.numChildren + 1; - let init: StateInit = initOf TodoChild(self.numChildren); - send(SendParameters{ - to: contractAddress(init), - value: ton("0.1"), // pay for message, the deployment, and give some TON for storage - mode: SendIgnoreErrors, - code: init.code, // attaching the `StateInit` will cause the message to deploy - data: init.data, - body: "identify".asComment() // we must piggyback the deployment on another message - }); +get fun seqno(): uint64 { + val storage = lazy TodoChildStorage.load(); + return storage.seqno; +} +``` + +The parent contract owns the deployment logic. It derives each child address from the parent address, the child sequence number, and the child code. + +```tolk title="parent.tolk" +import "storage" + +type TodoParentMessage = DeployAnother + +// Build StateInit for a TodoChild instance owned by the given parent. +fun calcDeployedTodoChild( + parentAddress: address, + seqno: uint64, + todoChildCode: cell, +): AutoDeployAddress { + val childStorage: TodoChildStorage = { + parentAddress, + seqno, + }; + + return { + stateInit: { + code: todoChildCode, + data: childStorage.toCell(), + } } +} - get fun numChildren(): Int { - return self.numChildren; +fun onInternalMessage(in: InMessage) { + val msg = lazy TodoParentMessage.fromSlice(in.body); + + match (msg) { + DeployAnother => { + var storage = lazy TodoParentStorage.load(); + assert (in.senderAddress == storage.adminAddress) throw 0xFFFF; + // `numChildren` is used as the next child id. Because deployment + // is sent with SEND_MODE_IGNORE_ERRORS, failed sends can leave gaps. + storage.numChildren += 1; + + // Send a message to the auto-calculated address and attach + // the child code and initial data so the child is deployed. + val deployMsg = createMessage({ + bounce: BounceMode.Only256BitsOfBody, + dest: calcDeployedTodoChild( + contract.getAddress(), + storage.numChildren, + storage.todoChildCode, + ), + value: ton("0.1"), + body: Identify {}, + }); + + storage.save(); + deployMsg.send(SEND_MODE_IGNORE_ERRORS); + } } } + +get fun numChildren(): uint64 { + val storage = lazy TodoParentStorage.load(); + return storage.numChildren; +} ```