Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 130 additions & 42 deletions contract-dev/techniques/contract-sharding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Aside
type="caution"
>
Child contracts should store information about the Parent so that only it can authorize important state changes.
Child contracts should store the parent contract address and verify that incoming messages originate from it. This ensures that only the parent contract can perform authorized state changes.
</Aside>

## 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small accuracy thing: "The key is the wallet's smart contract address" is ambiguous - and probably wrong as it reads.the previous sentence says the children are user wallets, so "the wallet's smart contract address" naturally refers to the child's own address, which is the output of address derivation, not the input/key

the key for a jetton wallet is the address of the wallet's owner (the user's main wallet contract) which gets fed into address derivation along with the minter address and the wallet code

-For jettons, the parent contract is the minter, and the child contracts are user wallets. The key is the wallet's smart contract address.
+For jettons, the parent contract is the minter, and the child contracts are jetton wallets. The key is the address of the jetton wallet's owner, and the value is the owner's jetton balance.


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.

<Image
src="/resources/images/child_light.png"
darkSrc="/resources/images/child_dark.png"
alt="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.

Comment on lines +42 to 43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Issue #500 scope is still incomplete (missing Poll/Voter + bounced-message logic).

The page now has a solid parent/child sample, but it still does not include (or link to) the requested Poll/Voter Tolk example demonstrating bounced-message handling from the linked issue scope.

Also applies to: 85-110, 114-167

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contract-dev/techniques/contract-sharding.mdx` around lines 42 - 43, The
sample is missing the Poll/Voter example and bounced-message handling from Issue
`#500`: add two new contract examples named Poll and Voter (mirroring the
parent/child pattern) where Poll deploys Voter instances and assigns sequence
numbers, extend the shared storage definitions to include the Poll/Voter message
types and a bounced-message envelope, and implement explicit bounce handling in
the Voter (e.g., an onBounce or handle_bounced_message function and examples of
sending messages that may bounce and how Poll reacts); update the narrative to
show deployment, message flow, and bounced-message test cases and link to Issue
`#500` for scope reference.

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) {
Comment thread
aigerimu marked this conversation as resolved.
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 {}
```
Comment thread
aigerimu marked this conversation as resolved.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but I think the else branch in the match below may not be reachable as written. Since TodoChildMessage = Identify is a single-variant union, lazy TodoChildMessage.fromSlice(in.body) should throw on any body that doesn't parse as Identify - including an empty body, since the opcode parse fails first

If the intent is to ignore empty top-up messages, the empty-body check probably needs to happen before the lazy fromSlice call for instance

 fun onInternalMessage(in: InMessage) {
+    if (in.body.isEmpty()) {
+        return;
+    }
     val msg = lazy TodoChildMessage.fromSlice(in.body);

it definitely worths double-checking against actual Tolk semantics


match (msg) {
Identify => {
val storage = lazy TodoChildStorage.load();
assert (in.senderAddress == storage.parentAddress) throw 0xFFFF;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw 0xFFFF is reused for three different conditions: "sender is not the parent" (here), "unexpected message body" (line 101), and "sender is not the admin" (line 144). same code for three distinct failure modes makes a tx-dump in an explorer harder to triage

worth picking distinct codes (any value in 32-65535 outside reserved ranges), e.g. 100 / 101 / 200, but I am not sure

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;
}
```
Comment thread
aigerimu marked this conversation as resolved.

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 => {
Comment thread
aigerimu marked this conversation as resolved.
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;
Comment thread
aigerimu marked this conversation as resolved.

// Send a message to the auto-calculated address and attach
// the child code and initial data so the child is deployed.
val deployMsg = createMessage({
Comment thread
aigerimu marked this conversation as resolved.
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;
}
```
Comment thread
aigerimu marked this conversation as resolved.
Loading