Ton 多签账户地址生成原理
TON 多签账户地址如何生成与其与普通账户的关联
总览:TON 地址来源于 StateInit
在 TON 中,账户地址并不是“公钥哈希”的简单映射,而是由账户的 StateInit
(即初始状态:code + data
)通过规范化序列化并哈希得出。因此:
普通钱包地址 = 指定钱包合约(如 Wallet v4)的
code
+ 含公钥/seqno 等的data
→ 计算地址。多签钱包地址 = 多签合约(如 SafeMultisigWallet)的
code
+ 含公钥集合、阈值、初始配置的data
→ 计算地址。
地址与你填入的参数强绑定:同一份 code,不同的 data(例如不同公钥集合或阈值)会得到不同地址;这也意味着“地址先天包含了部署配置的承诺”。
与普通账户的关联与差异
相同点:
都是“合约账户”。地址来源于
StateInit
的哈希。都通过外部消息(external message)驱动执行。
不同点:
普通钱包 data 中保存单个公钥、
seqno
等;多签钱包 data 中保存“公钥集合 + 阈值 + 提案池等”。普通钱包的授权是单签验证;多签是 M-of-N 验证,并常带提案/确认流程。
地址的“可预测性”一致:在部署前就可离线预计算地址(counterfactual address)。当
StateInit
与工作链/分片参数固定时,地址唯一确定。
底层原理分解
StateInit 结构
code
: 合约字节码(Cell)data
: 合约初始数据(Cell,嵌套结构;多签里包含公钥集合、阈值、初始 seqno、管理员设置等)可选
library
字段
序列化与哈希
将
StateInit
用 TL-B 规则序列化为 Cell 树(BOC)。规范化地计算哈希(通常是根 cell 的 hash,基于 merkle-like 的 cell 哈希算法)。
结合工作链 id(通常
0
)得到地址:addr = (workchain, hash(StateInit))
。
地址格式
用户看到的是 Base64/URL-safe 的字符串(
EQ...
/UQ...
开头),包含:workchain、哈希、bounce 标志、校验和等。
内部表示更接近
<wc>:<hex-256-hash>
。
多签地址为何与参数强绑定
阈值、N 个公钥列表等都编码进
data
,改变任一一位字节都会改变StateInit
哈希,从而得到完全不同的地址。这提供了“链下可承诺”的特性:你可以与成员在链下就 code 与 data 达成一致,再各自为该地址充值或引用。
典型多签合约的数据布局(概念示意)
具体字段名称与布局因实现而异,这里是常见 SafeMultisigWallet 类实现的思路:
k
: 阈值 MMn
: 公钥总数 NNkeys
: map 或 cell 序列,存 N 个 ed25519 公钥(32 字节)seqno
: 递增序号可选:
pending
: 提案池(map: query_id → 提案 cell)管理配置位、升级权限(setcode)
当我们构造 data
cell 时,会将上述字段按 TL-B schema 写入。有些实现会将 keys 以 dictionary(哈希表)保存,并以 key index 或 key bytes 作为 dict key。
Python 实战:从 code+data 生成多签地址
下面例子展示两种路径:
路径 A:使用社区已发布的多签合约的编译产物(BOC)与已知的 data 编码规则。
路径 B:用简化的 data 结构说明地址如何受 data 影响(便于理解与测试)。
说明:
我们用
tonsdk
或tonpy
/pytoniq
之类库来处理 Cell/BOC。不同库 API 名称略有不同,示例以 tonsdk 为主思路给出。若你的环境不同,我可以按你的库调整。
安装依赖(任选其一)
pip install tonsdk
# 或
pip install pytoniq
准备材料
多签合约的
code.boc
(或以十六进制/BASE64 形式内嵌)了解该合约 data 的写入顺序与 TL-B 定义(来自该仓库 README 或源码)
若你暂时没有现成 code,我们先用“占位 code”(一个固定的演示 Cell),重点演示 data 变化如何改变地址;随后示范如何替换为真实多签 code。
示例 1:演示型(非真实多签 code),看地址如何受 data 影响
from tonsdk.utils import Address, b64str_to_bytes
from tonsdk.boc import Cell, Builder
import hashlib
def build_demo_code_cell():
# 用一个固定内容的 cell 作为“假 code”,仅用于演示地址计算
b = Builder()
b.store_uint(0xDEADBEEF, 32)
return b.end_cell()
def build_multisig_data_cell(pubkeys, threshold, initial_seqno=0):
# 演示型 data: [threshold, n, seqno, keys...]
# 实际多签合约会更复杂,可能用 dict 存 keys,这里用简化顺序写入
b = Builder()
b.store_uint(threshold, 16) # 阈值,用 16 位存放
b.store_uint(len(pubkeys), 16) # N
b.store_uint(initial_seqno, 32) # 初始 seqno
for pk in pubkeys:
assert len(pk) == 32
b.store_bytes(pk)
return b.end_cell()
def build_state_init(code_cell, data_cell):
# StateInit TL-B: optional fields; 这里最简单:都 present
# tonsdk 的 Cell/Builder 抽象通常按合约库写,这里用一个标准容器 cell 装 code+data
# 在真实场景应按 StateInit TL-B 写入标志位与引用
root = Builder()
# Flags: present code + data
# 简化做法:将 code、data 作为引用(refs)加入
root_cell = root.end_cell()
root_cell.refs.append(code_cell)
root_cell.refs.append(data_cell)
return root_cell
def address_from_stateinit(stateinit_cell, workchain=0):
# 在 TON 中地址 = (workchain, hash(root_cell))
# Cell 自身有 hash 计算;这里用 tonsdk 的 cell_hash()
cell_hash = stateinit_cell.hash
# 封装为标准显示格式
addr = Address((workchain, cell_hash))
return addr
# 演示:两组不同的公钥组合/阈值,地址不同
code = build_demo_code_cell()
# 伪公钥(32 字节)
K1 = bytes.fromhex("11"*32)
K2 = bytes.fromhex("22"*32)
K3 = bytes.fromhex("33"*32)
data_A = build_multisig_data_cell([K1, K2, K3], threshold=2, initial_seqno=0)
state_A = build_state_init(code, data_A)
addr_A = address_from_stateinit(state_A, 0)
data_B = build_multisig_data_cell([K1, K2, K3], threshold=3, initial_seqno=0) # 仅阈值不同
state_B = build_state_init(code, data_B)
addr_B = address_from_stateinit(state_B, 0)
print("Address A (2-of-3):", addr_A.to_string(is_user_friendly=True, bounce=True))
print("Address B (3-of-3):", addr_B.to_string(is_user_friendly=True, bounce=True))
assert addr_A.to_string() != addr_B.to_string()
要点:
仅改变阈值,地址就会变化;真实多签也一样,因为阈值在 data 中。
公钥顺序或存储方式改变也会改变地址。规范通常固定顺序或使用排序保证可重复性。
示例 2:使用真实多签 code(SafeMultisigWallet 类)
以下为思路示例,真实字段需按该合约的 TL-B 来写。常见做法:
从仓库拿到
safe_multisig.code.boc
了解构造器或 data 格式,例如:
threshold: uint16
keys: dict(int→bytes32)
或 packed 列表seqno: uint32
可能还有
custodians_count
、plugin
、setcode
标志等
伪代码如下(字段名仅演示):
from tonsdk.boc import Cell, Builder
from tonsdk.utils import Address, bytes_to_b64str
def load_code_boc(path):
with open(path, "rb") as f:
return Cell.one_from_boc(f.read()) # 读取编译好的 code
def write_keys_dict(keys):
# 如果真实合约定义 keys 为 dict,需要按其 key 格式构建 dictionary
# tonsdk 有 Dictionary 支持(不同版本 API 不同),这里作概念示例
from tonsdk.boc import Dict, Builder
d = Dict(16, 256) # key_bits, value_bits 示例
for idx, pk in enumerate(keys):
d.set(idx, pk) # 实际需按 schema 写入
return d.serialize()
def build_safemultisig_data(keys, threshold, seqno=0):
b = Builder()
b.store_uint(threshold, 16)
b.store_uint(len(keys), 16)
b.store_uint(seqno, 32)
# 若实际为 dict:
# dict_cell = write_keys_dict(keys)
# b.store_dict(dict_cell)
# 若实际为线性 bytes:
for pk in keys:
b.store_bytes(pk)
return b.end_cell()
code = load_code_boc("safe_multisig.code.boc")
K1, K2, K3 = [bytes.fromhex("11"*32), bytes.fromhex("22"*32), bytes.fromhex("33"*32)]
data = build_safemultisig_data([K1, K2, K3], threshold=2, seqno=0)
state = build_state_init(code, data)
addr = address_from_stateinit(state, 0)
print("Multisig Address:", addr.to_string(is_user_friendly=True, bounce=True))
提示:
上述 data 构造函数中的位宽、顺序、是否用 dict 要严格按照你选用的合约源码/README。
如果合约提供“构造器”(constructor)入口,链上部署时也会写入相同 data;我们离线构造的 data 必须与其等价,才能得到一致地址。
与普通钱包(Wallet v4 等)的对照示例
普通钱包的地址生成同理,只是 data 更简单。例如 Wallet v4:
data 通常包含:
public_key: bytes32
、wallet_id: uint32
、seqno: uint32
等。code 为钱包标准 code(不同子版本 v3/v4 有差异)。
示例(简化演示):
def build_walletv4_data(pubkey, wallet_id=698983191, seqno=0): b = Builder() b.store_uint(wallet_id, 32) b.store_uint(seqno, 32) b.store_bytes(pubkey) # 32 bytes return b.end_cell() wallet_code = Cell.one_from_boc(open("wallet_v4.code.boc", "rb").read())
user_pubkey = bytes.fromhex("aa"*32)
wallet_data = build_walletv4_data(user_pubkey)
wallet_state = build_state_init(wallet_code, wallet_data)
wallet_addr = address_from_stateinit(wallet_state, 0)
print("Wallet v4 Address:", wallet_addr.to_string(is_user_friendly=True, bounce=True))
对照可见:
两者都由 code+data 定义地址。
多签 data 含有“公钥集合+阈值”;单签仅一个公钥。
注意事项
使用已审计、社区常用的多签实现(如 SafeMultisigWallet / SetcodeMultisigWallet),并严格按照其 TL-B/ABI 构造 data。
一致性与可重复性:
规定公钥排序(如 lexicographic)来避免不同排序导致地址不一致。
固定 workchain(主网一般为 0)。
部署前离线计算并对齐地址:
多方在链下对 code 哈希与 data 哈希进行“比对签署”,确保所有人部署的是相同字节(防止替换攻击)。
验证方法:
将本地计算出的地址与在区块浏览器上看到的合约
StateInit
哈希比对。
升级与 setcode:
某些多签支持升级(
setcode
),这意味着未来 code 可能改变;升级提案本身也应走多签流程。变更 code 会改变后续行为,但不会改变已部署合约的地址(因为地址绑定的是初始 StateInit,而不是运行时 code);不过通常升级是通过合约逻辑变更内部 code 存储,细节依实现。