TON 分片优化
架构基础知识
TON 可以并行处理无数个事务。这种功能基于无限分片范式,这意味着一旦一组验证器的负载接近其吞吐量极限,就会被分割(分片)。然后,两组验证器独立并行处理这些负载。这些拆分是确定发生的,交易是否由特定组处理取决于与交 易相关的合约地址。彼此相近(共享相同前缀)的地址将在同一个分片中处理。
当信息从一个合约发送到另一个合约时,有两种可能:一种是两个合约都在同一个分片中,另一种是两个合约在不同的分片中。在前一种情况下,当前组已经拥有所有必要的数据,可以立即处理信息。在后一种情况下,信息必须从一个组路由到另一个组。为避免信息丢失或重复处理,需要进行适当的核算。具体做法是在主链区块中注册发送者分片的传出信息队列,然后接收者分片必须明确确认它已处理了该队列。这样的开销使得跨分片消息传递的速度变慢;在发送消息的区块和接收消息的区块之间至少需要一个主链区块。这种延迟通常约为 12-13 秒。
由于一个账户的交易总是在一个分片中处理,因此单个账户的每秒交易速度(TPS)是有限的。这意味着,在为新的大规模协议开发架构时,应尽量避免中心点。此外,如果一连串的交易遵循相同的路径,也不会因为分片而得到更快的处理速度:链中每个合约的 TPS 限制相同,但由于交付延迟,整个链的处理时间会更长。
在大规模系统中,延迟和吞吐量之间的权衡是区分优秀协议和卓越协议的关键。
要分片还是不要分片
为了改善用户体验和处理时间,协议需要了解其系统中哪些部分可以并行处理,因此应该分片以提高吞吐量,哪些部分是严格按顺序处理的,因此如果放在一个分片中,会降低延迟。
jetton 就是吞吐量优化的一个很好的例子。由于从 A 到 B 和从 C 到 D 的转账互不依赖,因此 可以并行处理。通过将所有 Jetton-wallet 随机、均匀地分布在地址空间中,我们可以在区块链上实现负载的完美分布,并在适当延迟的情况下实现每秒数百次(未来可达数千次)的吞吐量。
相反,如果另一个处理 jetton 的智能合约(比方说合约 A)在收到 jetton X 时做了什么(而 A 的 jetton 钱包合约是 B),将合约 A 及其钱包 B 放在不同的分片中并不会提高吞吐量。事实上,每笔转账都会经过相同的地址链,每个地址都会成为瓶颈。在这种情况下,最好将 A 和 B 放在同一个分片中,从而缩短整个链的时间,以改善延迟。
智能合约开发人员的实用结论
如果你有一个执行业务逻辑的智能合约,可以考虑部署多个这样的合约,以享受 TON 的并行性。如果无法做到这一点,而且您的智能合约与一组预定义的其他智能合约(比方说 jetton-wallets )交互,则可以考虑将它们放在同一个分片中。这通常可以在链外完成(通过暴力破解特定合约地址,使所有需要的 jetton-wallet 都有相邻地址),有时链上暴力破解也是可以接受的。
即将到来的节点和网络性能改进有望提高分片的吞吐量并减少交付延迟,但同时用户数量也会增加。随着越来越多的用户加入,分片优化将变得越来越重要。最终,这将成为大规模应用的一个决定性因素:用户会选择对他们来说最方便的应用,也就是延迟较低的应用。因此,不要再犹豫了,从整体网络改善的角度出发,对应用程序进行分片优化吧。现在就做!在很多情况下,这甚至 比 gas 优化更重要。
服务的实际结论
存款
如果您希望存款速度高于每秒 30 笔转账,建议您拥有多个地址,这样您就可以并行接受存款,享受高吞吐量。如果您知道用户将从哪个地址入金,例如通过 TON Connect 发起的交易,请选择离用户钱包地址最近的入金地址。用于选择最近地址的现成 Typescript 代码可以如下所示:
import { Address } from '@ton/ton';
function findMatchingBits (a: number, b: number, start_from: number) {
let bitPos = start_from;
let keepGoing = true;
do {
const bitCount = bitPos + 1;
const mask = (1 << (bitCount)) - 1;
const shift = 8 - bitCount;
if(((a >> shift) & mask) == ((b >> shift) & mask)) {
bitPos++;
}
else {
keepGoing = false;
}
} while(keepGoing && bitPos < 7);
return bitPos;
}
function chooseAddress(user: Address, contracts: Address[]) {
const maxBytes = 32;
let byteIdx = 0;
let bitIdx = 0;
let bestMatch: Address | undefined;
if(user.workChain !== 0) {
throw new TypeError(`Only basechain user address allowed:${user}`);
}
for(let testContract of contracts) {
if(testContract.workChain !== 0) {
throw new TypeError(`Only basechain deposit address allowed:${testContract}`);
}
if(byteIdx >= maxBytes) {
break;
}
if(byteIdx == 0 || testContract.hash.subarray(0, byteIdx).equals(user.hash.subarray(0, byteIdx))) {
let keepGoing = true;
do {
if(keepGoing && testContract.hash[byteIdx] == user.hash[byteIdx]) {
bestMatch = testContract;
byteIdx++;
bitIdx = 0;
if(byteIdx == maxBytes) {
break;
}
}
else {
keepGoing = false;
if(bitIdx < 7) {
const resIdx = findMatchingBits(user.hash[byteIdx], testContract.hash[byteIdx], bitIdx);
if(resIdx > bitIdx) {
bitIdx = resIdx;
bestMatch = testContract;
}
}
}
} while(keepGoing);
}
}
return {
match: bestMatch,
prefixLength: byteIdx * 8 + bitIdx
}
}
如果你希望存入 jetton ,除了创建多个存款地址外,最好还对这些地址进行分片优化:选择这样的地址,使每个存款地址与其 jetton 钱包位于同一分片。可在 此处 找到此类地址的生成器。选择离用户最近的地址也是一种权宜之计。
提款
取款也是如此;如果您需要每秒发送大量转账,建议您拥有多个发送地址,必要时使用 jetton-wallets 对它们进行 shard 优化。
分片优化 101
向 web 2 用户解释分片
TON 区块链与其他任何区块链一样是一个网络,因此尝试用 web 2(ipv4)网络术语来解释它是有意义的。
终点
在一般网络中,终端是一个物理设备,而在区块链中,终端是一个智能合约。
分片
按照这种逻辑,shard 只不过是一个子网。从这个角度看,唯一不同的是,ipv4 采用的是 32 位寻址方案,而 TON 采用的是 256 位。 因此,合约地址分片前缀是合约地址的一部分,它标识了将计算所收到的信息结果的验证者群体。 从网络的角度看,很明显,同一网段上的请求会比其他网段上的请求处理得更快,对吗? 这有点像使用 CDN 将内容托管在离最终用户更近的地方,而在 TON 中,我们将合约部署在离最终用户更近的地方。
如果分片上的负载超过一定水平,分片就会分裂。这样做的目的是为承受负载的合约提供专用计算资源,并隔离其对整个网络的影响。 目前最大的分片前缀长度仅为 4 位,因此区块链从前缀 0 到 15 最多可以分成 16 个分片。
分片优化过程中的问题
让我们更加务实