文章

Ton生成多签地址与注意

用 Python 生成 TON 普通钱包地址与多签钱包地址

下面用可运行的 Python 示例演示如何“离线”生成:

  • 普通钱包(Wallet v4R2)的地址

  • 多签钱包(以 SafeMultisig 风格为例)的地址

并解释底层原理:在 TON 中,地址来自合约的 StateInit(code + data)的哈希,而非“公钥哈希”。

示例基于 Python 库:

  • tonsdk(轻量,便于构造 Cell/BOC/地址)

  • 如你更偏好 pytoniq 或 tonpy,我可以给出等价版本

提示:真实生产需使用“真实 code.boc”。本文也提供一个“演示 code”写法帮助你理解 data 变化对地址的影响,再给出如何替换为真实代码的方式。


TON 地址的底层原理

  • 账户地址来源Address = (workchain, hash(StateInit))

  • 其中 StateInit = { code: Cell, data: Cell, library?: Cell }

  • 序列化为 BOC 后取根 Cell 哈希,再与工作链 id(主网一般为 0)一起编码为可读字符串(EQ...UQ...)。

  • 普通钱包 vs 多签

    • 普通钱包:code = 钱包标准代码(Wallet v4)data = {public_key, wallet_id, seqno, ...}

    • 多签钱包:code = 多签合约代码(如 SafeMultisig)data = {阈值k, 公钥集合keys, seqno, pending等}

    • 地址与 data 强绑定,哪怕只改一个 bit,地址都会变化。


环境准备


pip install tonsdk

示例一:演示 StateInit → 地址(使用“演示 code”理解 data 影响)

这个例子用一个固定的“演示 code cell”代替真实合约代码,重点展示:

  • 如何构造 data

  • 如何组装 StateInit

  • 改变 data(例如阈值)会如何改变地址

from tonsdk.boc import Cell, Builder
from tonsdk.utils import Address

def build_demo_code_cell():
    # 仅用于演示:随便写入一些常数,代表“合约字节码”
    b = Builder()
    b.store_uint(0xDEADBEEF, 32)
    return b.end_cell()

def build_multisig_like_data_cell(pubkeys, threshold, initial_seqno=0):
    # 演示型多签 data:顺序写入阈值、数量、seqno、N个公钥(每个32字节)
    b = Builder()
    b.store_uint(threshold, 16)        # 阈值 M
    b.store_uint(len(pubkeys), 16)     # N
    b.store_uint(initial_seqno, 32)    # seqno
    for pk in pubkeys:
        assert len(pk) == 32, "pubkey must be 32 bytes"
        b.store_bytes(pk)
    return b.end_cell()

def build_state_init(code_cell, data_cell):
    # 最简“StateInit 容器”:将 code 和 data 作为两个引用
    # 注意:正式 StateInit 有特定 TL-B 标志位;tonsdk/高层库通常已有封装。
    root = Builder().end_cell()
    root.refs.append(code_cell)
    root.refs.append(data_cell)
    return root

def to_address(stateinit_cell, workchain=0):
    # 地址 = (wc, hash(StateInit根cell))
    return Address((workchain, stateinit_cell.hash))

# 构造“演示 code”
code = build_demo_code_cell()

# 三个演示公钥
K1 = bytes.fromhex("11"*32)
K2 = bytes.fromhex("22"*32)
K3 = bytes.fromhex("33"*32)

# 2-of-3
data_A = build_multisig_like_data_cell([K1, K2, K3], threshold=2, initial_seqno=0)
addr_A = to_address(build_state_init(code, data_A), 0)

# 3-of-3(仅阈值不同)
data_B = build_multisig_like_data_cell([K1, K2, K3], threshold=3, initial_seqno=0)
addr_B = to_address(build_state_init(code, data_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()

要点:

  • 即使 code 一样,只要 data 有差别(阈值、公钥顺序等),地址就不同。

  • 真实合约中 data 的布局由其 TL-B 定义决定。


示例二:生成“普通钱包(Wallet v4R2)”地址

这里给出两种做法:

  • A. 快速路线:使用库的现成钱包封装(推荐新手)

  • B. 原理路线:手动准备真实 wallet v4 code 和 data

A. 使用 tonsdk 的现成封装

from tonsdk.wallet import Wallets, WalletVersion

# 1) 准备私钥/公钥(示例:随机生成;生产请从助记词或硬件导入)
import os
secret_key = os.urandom(32)  # 32 bytes
# tonsdk 钱包封装会从 secret_key 推导 ed25519 公钥

# 2) 实例化 Wallet v4R2(主流事实标准)
wallet = Wallets.from_private_key(
    secret_key=secret_key,
    version=WalletVersion.V4R2,   # 指定 v4R2
    workchain=0,                  # 主网 workchain = 0
)

# 3) 读取钱包地址(库内部已构造 StateInit 并计算哈希)
addr_str_bounce = wallet.address.to_string(is_user_friendly=True, bounce=True)
addr_str_nonbounce = wallet.address.to_string(is_user_friendly=True, bounce=False)

print("Wallet v4R2 Address (bounce):", addr_str_bounce)
print("Wallet v4R2 Address (non-bounce):", addr_str_nonbounce)

解释:

  • Wallets.from_private_key(..., V4R2) 内部会装配 v4R2 的标准 code,并按其 TL-B 布局写入 data(包含 public_keywallet_idseqno 等)。

  • 然后封装计算 StateInit 哈希生成地址。

  • 这就是大多数钱包 App/SDK 采用的事实标准。

B. 原理路线:手动 code + data

如果你手上有 wallet_v4r2.code.boc,可以手动构造 data:

  • 常见字段(示意):wallet_id:uint32seqno:uint32public_key:uint256

  • 字段实际顺序与宽度需与所用代码版本严格匹配,否则地址会不一致

from tonsdk.boc import Cell, Builder
from tonsdk.utils import Address

def load_code_boc(path):
    with open(path, "rb") as f:
        return Cell.one_from_boc(f.read())

def build_walletv4r2_data(pubkey_bytes32, wallet_id=698983191, seqno=0):
    # 注意:此布局需与你的 v4R2 代码一致,示例仅展示常见字段
    b = Builder()
    b.store_uint(wallet_id, 32)
    b.store_uint(seqno, 32)
    b.store_bytes(pubkey_bytes32)  # 32 bytes
    return b.end_cell()

wallet_code = load_code_boc("wallet_v4r2.code.boc")  # 真实文件
pubkey = bytes.fromhex("aa"*32)                       # 示例公钥
data = build_walletv4r2_data(pubkey)

# 组装 StateInit 并生成地址
root = Builder().end_cell()
root.refs.append(wallet_code)
root.refs.append(data)

addr = Address((0, root.hash))
print("Wallet v4R2 Address:", addr.to_string(is_user_friendly=True, bounce=True))

提示:

  • 如果你希望完全“按字节”验证,可以将 StateInit 序列化为 BOC,并与浏览器(tonviewer/tonscan)上显示的 state init 哈希校对。


示例三:生成“多签钱包(SafeMultisig 风格)”地址

多签没有强制唯一标准,不同仓库的 data 布局可能不同。下面提供一种常见布局思路,并告诉你如何替换为真实 code.boc 与真实 TL-B 布局。

A. 概念/演示版(顺序数组存 keys)

from tonsdk.boc import Cell, Builder
from tonsdk.utils import Address

def load_code_boc(path):
    with open(path, "rb") as f:
        return Cell.one_from_boc(f.read())

def build_multisig_data(keys, threshold, seqno=0):
    # 演示布局:k、n、seqno、线性keys(每个32字节)
    b = Builder()
    b.store_uint(threshold, 16)    # k
    b.store_uint(len(keys), 16)    # n
    b.store_uint(seqno, 32)        # seqno
    for pk in keys:
        assert len(pk) == 32
        b.store_bytes(pk)
    return b.end_cell()

# 用真实 SafeMultisig 的 code.boc 替换这个文件
# 你需要从所用仓库拿到编译产物,如 safe_multisig.code.boc
multisig_code = load_code_boc("safe_multisig.code.boc")

K1 = bytes.fromhex("11"*32)
K2 = bytes.fromhex("22"*32)
K3 = bytes.fromhex("33"*32)

data = build_multisig_data([K1, K2, K3], threshold=2, seqno=0)

stateinit = Builder().end_cell()
stateinit.refs.append(multisig_code)
stateinit.refs.append(data)

addr = Address((0, stateinit.hash))
print("Multisig Address (2-of-3):", addr.to_string(is_user_friendly=True, bounce=True))

说明:

  • 真实 SafeMultisig 常用 dictionary 存 keys 或带有 bitset/索引,对字段顺序、位宽、是否 pack 到 dict 有严格定义。必须对照源码或 README。

  • 一旦字段与 code 版本匹配,离线地址就与部署一致。

B. 使用 dictionary 存储 keys(更贴近常见实现)

多数实现会用 dict 存储 keys,以索引为 key、以 pubkey(256) 为 value。tonsdk 有 Dictionary 支持,具体 API 依版本略有不同。示意如下:

from tonsdk.boc import Builder, Cell, Dict
from tonsdk.utils import Address

def build_keys_dict(keys):
    # 字典 key 宽度 16(custodian index),value 宽度 256(pubkey)
    d = Dict(16, 256)
    for i, pk in enumerate(keys):
        d.set(i, int.from_bytes(pk, "big"))
    return d

def build_multisig_data_with_dict(keys, threshold, seqno=0):
    b = Builder()
    b.store_uint(threshold, 16)  # k
    b.store_uint(len(keys), 16)  # n
    b.store_uint(seqno, 32)      # seqno
    d = build_keys_dict(keys)
    b.store_dict(d)              # 将 dict 写入 data
    return b.end_cell()

具体 Dict 的用法与 bit 宽、是否用 store_dict 还是 store_ref 取决于实现。你需要以目标仓库的 TL-B 为准。


关键步骤总结

  1. 准备 code

    • 普通钱包:选 Wallet v4R2 的标准 code(库通常内置)

    • 多签:选择目标仓库提供的 code.boc(如 SafeMultisig)

  2. 构造 data

    • 严格按 TL-B 定义写字段顺序和位宽

    • 普通钱包一般包含:public_keywallet_idseqno

    • 多签一般包含:kkeys(dict/array)seqno、以及可选的 pending/配置

  3. 组装 StateInit

    • 根 cell 引用 code 和 data(正式 StateInit 还有字段存在位;多数库有现成封装)

  4. 计算地址

    • addr_hash = hash(StateInit_root_cell)

    • Address = (workchain, addr_hash) → to_string(is_user_friendly=True, bounce=...)


验证与实践建议

  • 一致性校验:部署前,多方离线计算地址,核对 code 与 data 的哈希,避免被替换。

  • 排序规则:多签 keys 建议按字节序排序固定化,避免不同顺序导致地址不一致。

  • 版本匹配:Wallet v4R2、SafeMultisig 不同版本之间的 data 布局可能不兼容,务必对应源码。

  • 工具链:生产中建议用已有封装(如 ton-core/tonsdk/tonweb)来实例化标准钱包,减少手写 data 出错风险。


一些问题

  • 为什么普通钱包/多签地址可以“部署前确定”?

  • 因为地址只取决于 StateInit(code+data),你可以离线构造它并计算哈希,这是 TON 的“可预测地址”特性。

  • 更换阈值或添加密钥会改变地址吗?

  • 会。如果这是初始 data 的改变。部署后如果合约支持“升级/变更状态”则不会改变已部署地址,但那是运行时状态变更,不是初始 StateInit 的改变。

 

许可协议:  CC BY 4.0