From d9dc6f64d2f6a50f71559dca3bfd43aac6753e14 Mon Sep 17 00:00:00 2001 From: king Date: Thu, 15 Oct 2026 18:25:53 +0800 Subject: [PATCH 1/5] fix(ticket): only allow Exec_Genesis at height 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ticket.Exec_Genesis 缺少 coins/coinsx/token 都有的 height 校验:任意高度都能重入 GenesisInit,造出 isGenesis=true、3 段 ticketId 的“创世票”——既绕过 pubHash 校验 (3 段 id 的 privHash 恒为空),又绕过成熟期与提现等待。分叉生效后受害矿工会把这种 票当成自己的票挖出来,ExecDepositFrozen 把出块奖励增发到攻击者的 ReturnAddress, 攻击者可立即 Tclose 提走,本金零锁仓。 - 执行器层:高度 != 0 直接返回 ErrReRunGenesis;不依赖 chain33 侧新增的框架分叉, 在旧版本 chain33 上同样生效 - 回归用例:executor 层断言高度 1.7M 上的 Genesis 被拒绝、高度 0 的正常创世路径 不受影响(并直接调用执行器方法验证执行器自身的校验);testnode 端到端断言交易 可以进块但回执非 ExecOk、链上不产生伪造票、受害矿工挖不到伪造票 - 原 PoC 用例改为修复后的回归断言 --- plugin/dapp/ticket/executor/exec.go | 7 + .../executor/ticket_miner_inject_poc_test.go | 443 ++++++++++++++++++ .../testnode/genesis_reentry_attack_test.go | 151 ++++++ 3 files changed, 601 insertions(+) create mode 100644 plugin/dapp/ticket/executor/ticket_miner_inject_poc_test.go create mode 100644 plugin/dapp/ticket/testnode/genesis_reentry_attack_test.go diff --git a/plugin/dapp/ticket/executor/exec.go b/plugin/dapp/ticket/executor/exec.go index 2e69a32941..e493b092de 100644 --- a/plugin/dapp/ticket/executor/exec.go +++ b/plugin/dapp/ticket/executor/exec.go @@ -12,6 +12,13 @@ import ( // Exec_Genesis exec genesis func (t *Ticket) Exec_Genesis(payload *ty.TicketGenesis, tx *types.Transaction, index int) (*types.Receipt, error) { + // 创世动作只允许在高度 0 执行, 与 coins/coinsx/token 对齐: + // 否则可在任意高度重入 GenesisInit 造出 isGenesis=true 的 3 段 ticketId 票据, + // 绕过 pubHash 校验/成熟期/提现等待, 把出块奖励记到攻击者 ReturnAddress。 + if t.GetHeight() != 0 { + tlog.Error("ticket genesis is only allowed at height 0", "height", t.GetHeight()) + return nil, types.ErrReRunGenesis + } if payload.Count <= 0 { return nil, ty.ErrTicketCount } diff --git a/plugin/dapp/ticket/executor/ticket_miner_inject_poc_test.go b/plugin/dapp/ticket/executor/ticket_miner_inject_poc_test.go new file mode 100644 index 0000000000..919441f62b --- /dev/null +++ b/plugin/dapp/ticket/executor/ticket_miner_inject_poc_test.go @@ -0,0 +1,443 @@ +// Copyright Fuzamei Corp. 2018 All Rights Reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// 本文件是审计项 H-2(TicketOpen 不校验 MinerAddress 归属 -> 向任意矿工注入票据) +// 的可执行 PoC,配合 docs/security/ticket-topen-mineraddress-injection-analysis.md 阅读。 +// +// 结论概览: +// 1. Test_TicketOpen_InjectsTicketsToArbitraryMiner +// 攻击者用自己的私钥签名,只要 ReturnAddress = 自己,就能把票据挂到任意 +// MinerAddress(受害矿工)名下;票据状态索引 LODB-ticket-tl::1 +// 也写在受害矿工名下,因此受害节点的钱包/矿工会把注入票据当成自己的票据。 +// 2. Test_InjectedTicket_NotMinableByVictim_PostFork +// ForkTicketId 生效后,ticketId 中的 pubHash 由开票人私钥派生;受害矿工用它 +// 自己的私钥算出的 privHash 无法通过 TicketMiner 的校验(ErrCheckPubHash), +// 即注入只能造成“矿工反复挖出废块”的拒绝服务,无法直接盗取该票据奖励。 +// 3. Test_TicketGenesisReRunRejected_PostFork / Test_TicketGenesisAllowedAtHeightZero +// 修复后的回归用例:ticket 的 Exec_Genesis 曾经缺少 height != 0 的重入保护 +// (coins/coinsx/token 都有),分叉生效后攻击者仍能造出 IsGenesis=true、3 段 +// ticketId 的“创世票”:既绕过 pubHash 校验,又绕过成熟期与提现等待,让受害矿工 +// 立刻替攻击者挖票、奖励增发给攻击者 ReturnAddress。现在执行器自身以及 chain33 +// 框架层(DriverBase.Exec + ForkGenesisOnlyAtHeightZero)都会拒绝高度 != 0 的 +// Genesis,同时高度 0 的正常创世路径不受影响。 +// 4. Test_InjectedTicket_MinedByVictim_PreFork_RewardGoesToAttacker +// ForkTicketId 未生效(或旧格式 3 段 ticketId)时,受害矿工可以成功挖出注入 +// 票据,而出块奖励(MinerValue)会增发到注入者控制的 ReturnAddress 上。 +// 5. Test_TicketOpen_CountExceedsPubHashesPanic +// 同一函数内的相邻缺陷:ForkTicketId 生效后只检查 len(PubHashes)==0, +// 之后按 Count 下标访问 PubHashes,Count > len(PubHashes) 会 panic +// (被 DriverBase.Exec 的 recover 兜住,返回 ErrActionNotSupport)。 +package executor_test + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/33cn/chain33/account" + apimock "github.com/33cn/chain33/client/mocks" + "github.com/33cn/chain33/common" + "github.com/33cn/chain33/common/address" + "github.com/33cn/chain33/common/crypto" + dbm "github.com/33cn/chain33/common/db" + "github.com/33cn/chain33/system/dapp" + "github.com/33cn/chain33/types" + "github.com/33cn/chain33/util" + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + ex "github.com/33cn/plugin/plugin/dapp/ticket/executor" + pty "github.com/33cn/plugin/plugin/dapp/ticket/types" +) + +// testdata/chain33.cfg.toml: [fork.sub.ticket] ForkTicketId = 1600000 +const ( + pocPostForkHeight = int64(1700000) + pocPreForkHeight = int64(100) + pocBlockTime = int64(1539918074) +) + +type pocEnv struct { + driver dapp.Driver + db dbm.KVDB + close func() +} + +func newPocEnv(t *testing.T, height int64) *pocEnv { + cfg := mock33.GetAPI().GetConfig() + dir, ldb, kvdb := util.CreateTestDB() + api := new(apimock.QueueProtocolAPI) + api.On("GetConfig", mock.Anything).Return(cfg, nil) + driver, err := dapp.LoadDriver("ticket", height) + assert.NoError(t, err) + driver.SetAPI(api) + driver.SetEnv(height, pocBlockTime, uint64(pocBlockTime)) + driver.SetStateDB(kvdb) + driver.SetLocalDB(kvdb) + return &pocEnv{driver: driver, db: kvdb, close: func() { util.CloseTestDB(dir, ldb) }} +} + +// fundCoins 给地址预置 coins 余额,等价于攻击者自己持有买票的本金。 +// 真实链上买票的第一步是把币转到 ticket 执行器地址(exec 子账户), +// 这里直接写入 ticket 执行器的 exec 子账户。 +func (e *pocEnv) fundCoins(t *testing.T, addr string, amount int64) { + acc := account.NewCoinsAccount(mock33.GetAPI().GetConfig()) + acc.SetDB(e.db) + acc.SaveExecAccount(address.ExecAddress(pty.TicketX), &types.Account{Addr: addr, Balance: amount, Frozen: 0}) +} + +// apply 把 Exec/ExecLocal 返回的 KV 落到 DB,模拟真实节点执行+写本地库 +func (e *pocEnv) apply(t *testing.T, kvs []*types.KeyValue) { + for _, kv := range kvs { + if len(kv.Value) > 0 { + assert.NoError(t, e.db.Set(kv.Key, kv.Value)) + } + } +} + +func pocAddress(priv crypto.PrivKey) string { + return address.PubKeyToAddr(address.DefaultID, priv.PubKey().Bytes()) +} + +func createTicketTx(t *testing.T, action string, payload proto.Message, priv crypto.PrivKey) *types.Transaction { + cfg := mock33.GetAPI().GetConfig() + ety := types.LoadExecutorType("ticket") + tx, err := ety.Create(action, payload) + assert.NoError(t, err) + tx, err = types.FormatTx(cfg, "ticket", tx) + assert.NoError(t, err) + tx.Sign(types.SECP256K1, priv) + return tx +} + +func ticketIDsFromLogs(t *testing.T, receipt *types.Receipt) (ids []string) { + for _, l := range receipt.Logs { + if l.Ty != pty.TyLogNewTicket { + continue + } + var rt pty.ReceiptTicket + assert.NoError(t, types.Decode(l.Log, &rt)) + ids = append(ids, rt.TicketId) + } + return ids +} + +// openInjectedTicket 由攻击者签名开票:MinerAddress=受害矿工,ReturnAddress=攻击者自己。 +// withPubHash 对应 ForkTicketId 是否生效。 +func openInjectedTicket(t *testing.T, env *pocEnv, height int64, attackerPriv, victimPriv crypto.PrivKey, count int32, withPubHash bool) (ids []string, tx *types.Transaction, receipt *types.Receipt, seed int64) { + cfg := mock33.GetAPI().GetConfig() + attackerAddr := pocAddress(attackerPriv) + victimAddr := pocAddress(victimPriv) + price := pty.GetTicketMinerParam(cfg, height).TicketPrice + env.fundCoins(t, attackerAddr, int64(count+1)*price) + + seed = 1234567 + var pubHashes [][]byte + if withPubHash { + // 注入者用自己的私钥派生 pubHash;受害矿工不知道这个私钥 + pubHashes = make([][]byte, count) + for i := 0; i < int(count); i++ { + privHash := common.Sha256([]byte(fmt.Sprintf("%x:%d:%d", attackerPriv.Bytes(), i, seed))) + pubHashes[i] = common.Sha256(privHash) + } + } + topen := &pty.TicketOpen{ + MinerAddress: victimAddr, + ReturnAddress: attackerAddr, + Count: count, + RandSeed: seed, + PubHashes: pubHashes, + } + tx = createTicketTx(t, "Topen", topen, attackerPriv) + // 关键点:签名者只是攻击者自己,TicketOpen 不校验它的 MinerAddress 归属 + assert.Equal(t, attackerAddr, tx.From()) + + receipt, err := env.driver.Exec(tx, 0) + if err != nil || receipt == nil { + t.Fatalf("TicketOpen exec failed: %v", err) + } + assert.Equal(t, int32(types.ExecOk), receipt.Ty) + + ids = ticketIDsFromLogs(t, receipt) + assert.Len(t, ids, int(count)) + for _, id := range ids { + // 票据状态对象写入 mavl-ticket-,MinerAddress 是受害矿工 + var tk pty.Ticket + var found bool + for _, kv := range receipt.KV { + if string(kv.Key) == string(ex.Key(id)) { + assert.NoError(t, types.Decode(kv.Value, &tk)) + found = true + } + } + assert.True(t, found, "ticket %s should be written into state", id) + assert.Equal(t, victimAddr, tk.MinerAddress) + assert.Equal(t, attackerAddr, tk.ReturnAddress) + assert.True(t, strings.HasPrefix(id, victimAddr+":"), "ticketId 前缀是受害矿工的 MinerAddress: %s", id) + } + env.apply(t, receipt.KV) + return ids, tx, receipt, seed +} + +// Test_TicketOpen_InjectsTicketsToArbitraryMiner 证明“向任意矿工注入票据”本身可以实现: +// 攻击者不持有受害矿工私钥,也能让受害矿工地址名下出现票据,且官方 TicketList 查询 +// 会把这些票据返回给受害地址。 +func Test_TicketOpen_InjectsTicketsToArbitraryMiner(t *testing.T) { + attackerPriv, err := FromPrivkey(PrivKeyA) + assert.NoError(t, err) + victimPriv, err := FromPrivkey(PrivKeyB) + assert.NoError(t, err) + attackerAddr, victimAddr := pocAddress(attackerPriv), pocAddress(victimPriv) + + env := newPocEnv(t, pocPostForkHeight) + defer env.close() + + const count = int32(3) + ids, tx, receipt, _ := openInjectedTicket(t, env, pocPostForkHeight, attackerPriv, victimPriv, count, true) + + // 本地索引 LODB-ticket-tl::: 使用的是 MinerAddress, + // 受害矿工的节点会把这些票据当成自己的票据(wallet.getTicketsByStatus/Query_TicketList) + lset, err := env.driver.ExecLocal(tx, &types.ReceiptData{Ty: receipt.Ty, Logs: receipt.Logs}, 0) + assert.NoError(t, err) + indexed := make(map[string]bool) + for _, kv := range lset.KV { + indexed[string(kv.Key)] = len(kv.Value) > 0 + } + for _, id := range ids { + key := fmt.Sprintf("LODB-ticket-tl:%s:%d:%s", address.FormatAddrKey(victimAddr), pty.TicketOpened, id) + assert.True(t, indexed[key], "票据必须以受害矿工地址建立索引: %s", key) + } + env.apply(t, lset.KV) + + // 用官方查询路径确认:受害矿工地址名下凭空多出了票据 + msg, err := ex.List(env.db, env.db, &pty.TicketList{Addr: victimAddr, Status: pty.TicketOpened}) + assert.NoError(t, err) + listed := msg.(*pty.ReplyTicketList).GetTickets() + assert.Len(t, listed, int(count)) + for _, tk := range listed { + assert.Equal(t, victimAddr, tk.MinerAddress) + assert.Equal(t, attackerAddr, tk.ReturnAddress) + } + t.Logf("injected %d tickets, victim=%s attacker=%s", len(listed), victimAddr, attackerAddr) +} + +// Test_InjectedTicket_NotMinableByVictim_PostFork:ForkTicketId 生效时, +// pubHash 把票据绑到开票人私钥,受害矿工无法挖出注入票据(只能造成废块/拒绝服务)。 +func Test_InjectedTicket_NotMinableByVictim_PostFork(t *testing.T) { + cfg := mock33.GetAPI().GetConfig() + attackerPriv, err := FromPrivkey(PrivKeyA) + assert.NoError(t, err) + victimPriv, err := FromPrivkey(PrivKeyB) + assert.NoError(t, err) + attackerAddr, victimAddr := pocAddress(attackerPriv), pocAddress(victimPriv) + + env := newPocEnv(t, pocPostForkHeight) + defer env.close() + + ids, _, _, seed := openInjectedTicket(t, env, pocPostForkHeight, attackerPriv, victimPriv, 1, true) + id := ids[0] + parts := strings.Split(id, ":") + assert.Len(t, parts, 5, "ForkTicketId 生效后 ticketId 形如 miner:txhash:index:pubHash:seed") + countNum, err := strconv.Atoi(parts[2]) + assert.NoError(t, err) + + params := pty.GetTicketMinerParam(cfg, pocPostForkHeight) + reward := params.CoinReward + + // 受害矿工的共识节点会用“自己的私钥”派生 privHash(consensus/ticket.genPrivHash) + victimPrivHash := common.Sha256([]byte(fmt.Sprintf("%x:%d:%s", victimPriv.Bytes(), countNum, parts[4]))) + assert.NotEqual(t, parts[3], hex.EncodeToString(common.Sha256(victimPrivHash)), + "受害矿工算出的 privHash 必然对不上注入者写入的 pubHash") + // 注入者的 privHash 才能对上 + attackerPrivHash := common.Sha256([]byte(fmt.Sprintf("%x:%d:%d", attackerPriv.Bytes(), countNum, seed))) + assert.Equal(t, parts[3], hex.EncodeToString(common.Sha256(attackerPrivHash))) + + // 让票据过冻结期 + env.driver.SetEnv(pocPostForkHeight, pocBlockTime+params.TicketFrozenTime+1, uint64(pocBlockTime)) + victimMinerTx := createTicketTx(t, "Miner", &pty.TicketMiner{TicketId: id, Reward: reward, PrivHash: victimPrivHash}, victimPriv) + assert.Equal(t, victimAddr, victimMinerTx.From()) + _, err = env.driver.Exec(victimMinerTx, 0) + assert.Error(t, err) + assert.Contains(t, err.Error(), "ErrCheckPubHash") + + // 只有知道 pubHash 原像(注入者自己的私钥)的人能挖,且只能以 ReturnAddress 身份挖 + attackerMinerTx := createTicketTx(t, "Miner", &pty.TicketMiner{TicketId: id, Reward: reward, PrivHash: attackerPrivHash}, attackerPriv) + assert.Equal(t, attackerAddr, attackerMinerTx.From()) + mined, err := env.driver.Exec(attackerMinerTx, 0) + if err != nil || mined == nil { + t.Fatalf("injector self-mining failed: %v", err) + } + assert.Equal(t, int32(types.ExecOk), mined.Ty) +} + +// Test_InjectedTicket_MinedByVictim_PreFork_RewardGoesToAttacker: +// ForkTicketId 未生效(旧格式 3 段 ticketId)时,受害矿工能挖出注入票据, +// 而出块奖励增发到注入者的 ReturnAddress。 +func Test_InjectedTicket_MinedByVictim_PreFork_RewardGoesToAttacker(t *testing.T) { + cfg := mock33.GetAPI().GetConfig() + attackerPriv, err := FromPrivkey(PrivKeyA) + assert.NoError(t, err) + victimPriv, err := FromPrivkey(PrivKeyB) + assert.NoError(t, err) + attackerAddr, victimAddr := pocAddress(attackerPriv), pocAddress(victimPriv) + + env := newPocEnv(t, pocPreForkHeight) + defer env.close() + + ids, _, _, _ := openInjectedTicket(t, env, pocPreForkHeight, attackerPriv, victimPriv, 1, false) + id := ids[0] + assert.Len(t, strings.Split(id, ":"), 3, "ForkTicketId 未生效时 ticketId 只有 3 段") + + params := pty.GetTicketMinerParam(cfg, pocPreForkHeight) + reward := params.CoinReward + + // 受害矿工的节点正常挖矿(旧格式没有 privHash 校验),挖的是“别人挂在自己名下的票” + env.driver.SetEnv(pocPreForkHeight, pocBlockTime+params.TicketFrozenTime+1, uint64(pocBlockTime)) + minerTx := createTicketTx(t, "Miner", &pty.TicketMiner{TicketId: id, Reward: reward}, victimPriv) + assert.Equal(t, victimAddr, minerTx.From()) + receipt, err := env.driver.Exec(minerTx, 0) + if err != nil || receipt == nil { + t.Fatalf("victim mining injected ticket failed: %v", err) + } + assert.Equal(t, int32(types.ExecOk), receipt.Ty) + + // 出块奖励(MinerValue)被增发并冻结到票据的 ReturnAddress —— 也就是攻击者地址。 + // 注意开票时已冻结了票面价,这里比较冻结余额的增量。 + var credited string + var minted int64 + for _, l := range receipt.Logs { + if l.Ty != types.TyLogExecDeposit { + continue + } + var transfer types.ReceiptExecAccountTransfer + if err := types.Decode(l.Log, &transfer); err != nil { + continue + } + if transfer.GetCurrent().GetAddr() == attackerAddr { + credited = transfer.GetCurrent().GetAddr() + minted = transfer.GetCurrent().GetFrozen() - transfer.GetPrev().GetFrozen() + } + } + assert.Equal(t, attackerAddr, credited, "出块奖励必须记在攻击者的 ReturnAddress 上") + assert.Equal(t, reward, minted) + t.Logf("victim %s mined injected ticket, reward %d credited to attacker %s", victimAddr, minted, attackerAddr) +} + +// Test_TicketGenesisReRunRejected_PostFork:修复后的回归用例。 +// Genesis 只允许在高度 0 执行:ticket 执行器自身(GetHeight() != 0 -> ErrReRunGenesis) +// 以及 chain33 框架层(DriverBase.Exec + ForkGenesisOnlyAtHeightZero)都会拒绝 +// height != 0 的创世重入,因此 ForkTicketId 生效后也无法再造出“免检”的创世票, +// 攻击链在第一步就被挡住。 +func Test_TicketGenesisReRunRejected_PostFork(t *testing.T) { + cfg := mock33.GetAPI().GetConfig() + height := pocPostForkHeight + assert.True(t, cfg.IsDappFork(height, pty.TicketX, "ForkTicketId"), "本用例要求 ForkTicketId 已生效") + + attackerPriv, err := FromPrivkey(PrivKeyA) + assert.NoError(t, err) + victimPriv, err := FromPrivkey(PrivKeyB) + assert.NoError(t, err) + attackerAddr, victimAddr := pocAddress(attackerPriv), pocAddress(victimPriv) + + env := newPocEnv(t, height) + defer env.close() + + price := pty.GetTicketMinerParam(cfg, height).TicketPrice + env.fundCoins(t, attackerAddr, 4*price) + + // 攻击者伪造“创世票”:MinerAddress = 受害矿工,ReturnAddress = 自己 + genesisTx := createTicketTx(t, "Genesis", &pty.TicketGenesis{ + MinerAddress: victimAddr, + ReturnAddress: attackerAddr, + Count: 1, + }, attackerPriv) + receipt, err := env.driver.Exec(genesisTx, 0) + assert.Equal(t, types.ErrReRunGenesis, err, "高度 %d 上的 Exec_Genesis 必须被拒绝", height) + assert.Nil(t, receipt, "被拒绝时不得产生回执与状态写入") + + // 直接调用执行器方法:即使链接的 chain33 未开启框架层分叉(旧版本或配置为 -1), + // ticket 自身的 height 校验也必须独立拒绝该重入。 + ticketDriver, ok := env.driver.(*ex.Ticket) + assert.True(t, ok) + receipt, err = ticketDriver.Exec_Genesis(&pty.TicketGenesis{ + MinerAddress: victimAddr, + ReturnAddress: attackerAddr, + Count: 1, + }, genesisTx, 0) + assert.Equal(t, types.ErrReRunGenesis, err, "执行器自身必须拒绝高度 != 0 的 Genesis") + assert.Nil(t, receipt) + t.Logf("post-fork(height=%d) genesis re-run rejected: %v", height, err) +} + +// Test_TicketGenesisAllowedAtHeightZero:创世高度(0)上的正常路径不受修复影响。 +func Test_TicketGenesisAllowedAtHeightZero(t *testing.T) { + cfg := mock33.GetAPI().GetConfig() + attackerPriv, err := FromPrivkey(PrivKeyA) + assert.NoError(t, err) + attackerAddr := pocAddress(attackerPriv) + + env := newPocEnv(t, 0) + defer env.close() + + price := pty.GetTicketMinerParam(cfg, 0).TicketPrice + env.fundCoins(t, attackerAddr, 4*price) + + genesisTx := createTicketTx(t, "Genesis", &pty.TicketGenesis{ + MinerAddress: attackerAddr, + ReturnAddress: attackerAddr, + Count: 1, + }, attackerPriv) + receipt, err := env.driver.Exec(genesisTx, 0) + assert.NoError(t, err) + assert.NotNil(t, receipt) + assert.Equal(t, int32(types.ExecOk), receipt.Ty) + assert.Len(t, ticketIDsFromLogs(t, receipt), 1) +} + +// Test_TicketOpen_CountExceedsPubHashesPanic:同一函数内的相邻缺陷。 +// ForkTicketId 生效时只拒绝 len(PubHashes)==0,却按 Count 下标取值, +// Count > len(PubHashes) 直接 panic(链上执行路径会被 recover 成 ErrExecPanic)。 +func Test_TicketOpen_CountExceedsPubHashesPanic(t *testing.T) { + attackerPriv, err := FromPrivkey(PrivKeyA) + assert.NoError(t, err) + victimPriv, err := FromPrivkey(PrivKeyB) + assert.NoError(t, err) + + env := newPocEnv(t, pocPostForkHeight) + defer env.close() + + attackerAddr, victimAddr := pocAddress(attackerPriv), pocAddress(victimPriv) + cfg := mock33.GetAPI().GetConfig() + price := pty.GetTicketMinerParam(cfg, pocPostForkHeight).TicketPrice + env.fundCoins(t, attackerAddr, 2000*price) + + topen := &pty.TicketOpen{ + MinerAddress: victimAddr, + ReturnAddress: attackerAddr, + Count: 1000, + RandSeed: 1, + PubHashes: [][]byte{[]byte("only-one-pubhash")}, + } + tx := createTicketTx(t, "Topen", topen, attackerPriv) + + ticket, ok := env.driver.(*ex.Ticket) + assert.True(t, ok) + action := ex.NewAction(ticket, tx) + + var recovered interface{} + func() { + defer func() { recovered = recover() }() + _, _ = action.TicketOpen(topen) + }() + assert.NotNil(t, recovered, "Count(1000) > len(PubHashes)(1) 必须触发 panic") + t.Logf("TicketOpen panic: %v", recovered) + + // 链上执行路径(DriverBase.Exec)会把 panic 转成 ErrActionNotSupport, + // 区块执行路径(executor.procExecAddBlock)则转成 types.ErrExecPanic + _, err = env.driver.Exec(tx, 0) + assert.Equal(t, types.ErrActionNotSupport, err) +} diff --git a/plugin/dapp/ticket/testnode/genesis_reentry_attack_test.go b/plugin/dapp/ticket/testnode/genesis_reentry_attack_test.go new file mode 100644 index 0000000000..50041b4791 --- /dev/null +++ b/plugin/dapp/ticket/testnode/genesis_reentry_attack_test.go @@ -0,0 +1,151 @@ +// Copyright Fuzamei Corp. 2018 All Rights Reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testnode + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/33cn/chain33/common" + "github.com/33cn/chain33/common/address" + "github.com/33cn/chain33/types" + "github.com/33cn/chain33/util" + "github.com/33cn/chain33/util/testnode" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "github.com/33cn/chain33/system" + _ "github.com/33cn/plugin/plugin" + + ty "github.com/33cn/plugin/plugin/dapp/ticket/types" +) + +// TestGenesisReentryRejectedOnTicketNode 在真实票共识测试节点上验证审计项 H-2 的修复链路, +// 全程走 mempool -> 出块 -> 执行器,而不是直接调 driver.Exec: +// +// 1. 默认配置下 ForkTicketId=0(创世即生效),即报告里的 post-fork 场景; +// 2. 攻击者发一笔 TicketAction{Ty=Genesis} 交易;修复前 Ticket.Exec_Genesis 缺少 +// height != 0 保护(coins/coinsx/token 都有),高度 >0 依然执行成功并造出 +// IsGenesis=true、3 段 ticketId 的“创世票”(MinerAddress=受害矿工、ReturnAddress=攻击者), +// 既绕过 pubHash 校验又绕过成熟期,受害矿工会替攻击者挖票并把奖励增发到攻击者地址; +// 3. 修复后该交易仍然可以进块,但 Exec_Genesis 在执行阶段被拒绝(chain33 框架层 +// DriverBase.Exec + ForkGenesisOnlyAtHeightZero,以及 ticket 执行器自身的 height 校验), +// 回执为 ExecErr,链上不产生任何票据状态,受害矿工也就无从挖出伪造票。 +func TestGenesisReentryRejectedOnTicketNode(t *testing.T) { + // 关闭手续费:攻击者地址的 plain coins 为 0(资产都在 ticket 子账户里) + cfgStr := strings.Replace(types.GetDefaultCfgstring(), "minTxFeeRate=100000", "minTxFeeRate=0", 1) + cfgStr = strings.Replace(cfgStr, `loglevel = "debug"`, `loglevel = "error"`, 1) + cfg := types.NewChain33Config(cfgStr) + cfg.GetModuleConfig().Consensus.Name = "ticket" + cfg.GetModuleConfig().Wallet.MinFee = 0 + cfg.SetMinFee(0) + + mock33 := testnode.NewWithConfig(cfg, nil) + defer mock33.Close() + require.NoError(t, mock33.WaitHeightTimeout(3, 120)) + + // 0) 环境确认:ForkTicketId 已生效 => 报告里的 post-fork 场景 + height := mock33.GetLastBlock().Height + require.True(t, cfg.IsDappFork(height, ty.TicketX, "ForkTicketId"), + "默认配置 ForkTicketId=0,创世即生效,属于 post-fork 场景") + params := ty.GetTicketMinerParam(cfg, height) + t.Logf("node height=%d ForkTicketId=%v ticketPrice=%d coinReward=%d", + height, cfg.IsDappFork(height, ty.TicketX, "ForkTicketId"), params.TicketPrice, params.CoinReward) + + // 角色:攻击者持伪造票的 ReturnAddress;受害矿工是节点钱包持有私钥的地址 + attackerPriv := util.TestPrivkeyList[2] + attackerAddr := address.PubKeyToAddr(address.DefaultID, attackerPriv.PubKey().Bytes()) + victimPriv := util.TestPrivkeyList[0] + victimAddr := address.PubKeyToAddr(address.DefaultID, victimPriv.PubKey().Bytes()) + t.Logf("attacker=%s victim=%s", attackerAddr, victimAddr) + + // 1) 攻击者提交伪造“创世票”交易(MinerAddress=受害矿工,ReturnAddress=自己) + // 守卫在余额校验之前生效,因此这里不依赖攻击者是否有票面资金。 + ety := types.LoadExecutorType("ticket") + genesisAction, err := ety.Create("Genesis", &ty.TicketGenesis{ + MinerAddress: victimAddr, + ReturnAddress: attackerAddr, + Count: 1, + }) + require.NoError(t, err) + genesisTx, err := types.FormatTx(cfg, "ticket", genesisAction) + require.NoError(t, err) + genesisTx.Sign(types.SECP256K1, attackerPriv) + txHash := mock33.SendTx(genesisTx) + txHashHex := common.ToHex(txHash) + + // 2) 交易上链,但执行失败:回执必须是 ExecErr 而不是 ExecOk + var detail *types.TransactionDetail + deadline := time.Now().Add(60 * time.Second) + for time.Now().Before(deadline) { + detail, err = mock33.GetAPI().QueryTx(&types.ReqHash{Hash: txHash}) + if err == nil && detail != nil && detail.GetReceipt() != nil { + break + } + time.Sleep(200 * time.Millisecond) + } + require.NotNil(t, detail) + require.NotNil(t, detail.GetReceipt(), "交易必须上链并返回回执") + // 失败交易的回执不会是 ExecOk(收费为 0 时通常是 ExecPack);关键是动作没有执行成功 + assert.NotEqual(t, int32(types.ExecOk), detail.GetReceipt().GetTy(), + "高度 %d 上的 Exec_Genesis 必须执行失败", detail.GetHeight()) + t.Logf("genesis reentry tx=%s height=%d receiptTy=%d rejected", txHashHex, detail.GetHeight(), detail.GetReceipt().GetTy()) + + // 3) 链上不产生伪造创世票(3 段 ticketId 由 MinerAddress + txHash 确定) + firstID := fmt.Sprintf("%s:%s:%010d", victimAddr, txHashHex, 0) + require.Equal(t, 2, strings.Count(firstID, ":"), "伪造票的 ticketId 形态: %s", firstID) + reply, err := mock33.GetAPI().Query(ty.TicketX, "TicketInfos", &ty.TicketInfos{TicketIds: []string{firstID}}) + if err == nil { + assert.Empty(t, reply.(*ty.ReplyTicketList).GetTickets(), "被拒绝的 Genesis 不得写入任何票据状态") + } + + // 4) 受害矿工名下也没有这笔交易产生的票据 + listReply, err := mock33.GetAPI().Query(ty.TicketX, "TicketList", &ty.TicketList{Addr: victimAddr, Status: ty.TicketOpened}) + if err == nil { + assert.Zero(t, countForged(listReply.(*ty.ReplyTicketList).GetTickets(), txHashHex), + "伪造票不得落到受害矿工名下") + } + + // 5) 继续出块:受害矿工不会挖出伪造票 + require.NoError(t, mock33.WaitHeightTimeout(height+5, 60)) + mined := minedForged(t, mock33, victimAddr, txHashHex, attackerAddr) + assert.Zero(t, mined.count, "受害矿工不得挖出伪造创世票") + t.Logf("after %d more blocks: minedForged=%d (attack blocked)", mock33.GetLastBlock().Height-height, mined.count) +} + +type forgedMined struct { + count int + ids []string + values []int64 +} + +func countForged(tickets []*ty.Ticket, txHashHex string) int { + var n int + for _, tk := range tickets { + if strings.Contains(tk.GetTicketId(), txHashHex) { + n++ + } + } + return n +} + +func minedForged(t *testing.T, mock33 *testnode.Chain33Mock, victimAddr, txHashHex, attackerAddr string) forgedMined { + var res forgedMined + reply, err := mock33.GetAPI().Query(ty.TicketX, "TicketList", &ty.TicketList{Addr: victimAddr, Status: ty.TicketMined}) + if err != nil { + return res + } + for _, tk := range reply.(*ty.ReplyTicketList).GetTickets() { + if !strings.Contains(tk.GetTicketId(), txHashHex) || tk.GetReturnAddress() != attackerAddr { + continue + } + res.count++ + res.ids = append(res.ids, tk.GetTicketId()) + res.values = append(res.values, tk.GetMinerValue()) + } + return res +} From 91e5faf905487b240be62705c5d52e8e88234c77 Mon Sep 17 00:00:00 2001 From: king Date: Thu, 15 Oct 2026 18:49:40 +0800 Subject: [PATCH 2/5] chore(deps): bump chain33 for the genesis height guard and declare its fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接入 chain33 新增的系统分叉 ForkGenesisOnlyAtHeightZero(创世动作 Genesis 只允许在 高度 0 执行,由 DriverBase.checkGenesisHeight 在 Exec 与 ExecLocal/ExecDelLocal 派发 前统一拦截): - github.com/33cn/chain33 升到 v1.72.1-0.20261016021757-b12c753ff90b:该提交是在当前 pin 的 v1.72.0 之上只回移本次修复(chain33 分支 fix/genesis-only-height-zero-v1.72.0, 含 #1407 review 的补钉),避免把 chain33 master 上 30+ 个无关改动一并带入——实测直接 跳到 master 会让 ci_rgbx 的 TSS/DKG 场景稳定超时。上游修复见 33cn/chain33#1407 - 5 个含真实 [fork.system] 的配置补上 ForkGenesisOnlyAtHeightZero=0。chain33 的 initForkConfig 要求每个已注册的系统分叉都必须出现在配置里,缺键或未知键都会让节点 启动失败,因此版本升级与配置补键必须落在同一次提交 - 验证:go test ./plugin/dapp/ticket/... 全绿 --- chain33.fork.toml | 2 ++ chain33.para.toml | 2 ++ go.mod | 4 ++-- go.sum | 4 ++++ plugin/consensus/qbft/chain33.qbft.toml | 2 ++ plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml | 2 ++ plugin/dapp/ticket/executor/testdata/chain33.cfg.toml | 2 ++ 7 files changed, 16 insertions(+), 2 deletions(-) diff --git a/chain33.fork.toml b/chain33.fork.toml index c21badfd82..e8a1df1def 100644 --- a/chain33.fork.toml +++ b/chain33.fork.toml @@ -34,6 +34,8 @@ ForkParaFee=-1 ForkEthAddressFormat=0 # 交易chainID严格校验启用高度,启用后交易组内每笔交易及minTxFeeRate=0时的单笔交易都会校验chainID,防止跨链重放;-1 表示关闭 ForkTxChainIDStrict=-1 +# 创世动作(Genesis)执行高度限制:启用后框架统一拒绝 height != 0 的 Exec_Genesis,防止执行器漏写 height 校验被创世重入;-1 表示关闭 +ForkGenesisOnlyAtHeightZero=0 [fork.sub.none] ForkUseTimeDelay=0 diff --git a/chain33.para.toml b/chain33.para.toml index ee592b2d2c..b4bb173980 100644 --- a/chain33.para.toml +++ b/chain33.para.toml @@ -342,6 +342,8 @@ ForkParaFee=-1 ForkEthAddressFormat=0 # 交易chainID严格校验启用高度,启用后交易组内每笔交易及minTxFeeRate=0时的单笔交易都会校验chainID,防止跨链重放;-1 表示关闭 ForkTxChainIDStrict=-1 +# 创世动作(Genesis)执行高度限制:启用后框架统一拒绝 height != 0 的 Exec_Genesis,防止执行器漏写 height 校验被创世重入;-1 表示关闭 +ForkGenesisOnlyAtHeightZero=0 [fork.sub.none] ForkUseTimeDelay=0 diff --git a/go.mod b/go.mod index 04fe1fa21d..d023b5b20d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.12 replace github.com/ava-labs/avalanchego => github.com/33cn/avalanchego v1.10.10-0.20240529041529-ada691598153 require ( - github.com/33cn/chain33 v1.72.0 + github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b github.com/BurntSushi/toml v1.3.2 github.com/NebulousLabs/Sia v1.3.7 github.com/bitly/go-simplejson v0.5.0 @@ -247,7 +247,7 @@ require ( github.com/status-im/keycard-go v0.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/supranational/blst v0.3.11 // indirect - github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect diff --git a/go.sum b/go.sum index f3c01b41ae..fadafa6a1b 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/33cn/chain33 v1.72.0 h1:Nk1CYLSYd5rfvhw1szE2Jt0cJx3A8nZCZZvIEIfTarw= github.com/33cn/chain33 v1.72.0/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= +github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b h1:bcnU0R5fIHabIUfFP3SMsZ/u4Zd2xpvOAmFF31ygA74= +github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go v41.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -1358,6 +1360,8 @@ github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= diff --git a/plugin/consensus/qbft/chain33.qbft.toml b/plugin/consensus/qbft/chain33.qbft.toml index 568826afda..1379b6edcf 100644 --- a/plugin/consensus/qbft/chain33.qbft.toml +++ b/plugin/consensus/qbft/chain33.qbft.toml @@ -146,6 +146,8 @@ superManager=[ ForkExecKey=0 # 交易chainID严格校验启用高度,启用后交易组内每笔交易及minTxFeeRate=0时的单笔交易都会校验chainID,防止跨链重放;-1 表示关闭 ForkTxChainIDStrict=-1 +# 创世动作(Genesis)执行高度限制:启用后框架统一拒绝 height != 0 的 Exec_Genesis,防止执行器漏写 height 校验被创世重入;-1 表示关闭 +ForkGenesisOnlyAtHeightZero=0 [fork.sub.manage] Enable=0 diff --git a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml index 29513f2b5f..4d9bd97add 100644 --- a/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml +++ b/plugin/dapp/evm/cmd/ci2/chain33.proxyminer.toml @@ -539,6 +539,8 @@ ForkParaFee=-1 ForkEthAddressFormat=0 # 交易chainID严格校验启用高度,启用后交易组内每笔交易及minTxFeeRate=0时的单笔交易都会校验chainID,防止跨链重放;-1 表示关闭 ForkTxChainIDStrict=-1 +# 创世动作(Genesis)执行高度限制:启用后框架统一拒绝 height != 0 的 Exec_Genesis,防止执行器漏写 height 校验被创世重入;-1 表示关闭 +ForkGenesisOnlyAtHeightZero=0 [fork.sub.none] ForkUseTimeDelay=0 diff --git a/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml b/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml index db761f992c..579ffd78cd 100644 --- a/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml +++ b/plugin/dapp/ticket/executor/testdata/chain33.cfg.toml @@ -226,6 +226,8 @@ ForkAccountBlacklist=-1 ForkAccountBlacklistV2=-1 # 交易chainID严格校验启用高度,启用后交易组内每笔交易及minTxFeeRate=0时的单笔交易都会校验chainID,防止跨链重放;-1 表示关闭 ForkTxChainIDStrict=-1 +# 创世动作(Genesis)执行高度限制:启用后框架统一拒绝 height != 0 的 Exec_Genesis,防止执行器漏写 height 校验被创世重入;-1 表示关闭 +ForkGenesisOnlyAtHeightZero=0 [fork.sub.coins] Enable=0 From a9442bfaec9ee69397642867a45d09b42741e073 Mon Sep 17 00:00:00 2001 From: king Date: Fri, 16 Oct 2026 12:10:23 +0800 Subject: [PATCH 3/5] debug(rgbx): compare TSS peers before/after restart on chain33 v1.72.6 (temporary) --- go.mod | 2 +- go.sum | 2 ++ plugin/dapp/rgbx/cmd/ci/docker-compose.sh | 26 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d023b5b20d..a91dc74be9 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.12 replace github.com/ava-labs/avalanchego => github.com/33cn/avalanchego v1.10.10-0.20240529041529-ada691598153 require ( - github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b + github.com/33cn/chain33 v1.72.6 github.com/BurntSushi/toml v1.3.2 github.com/NebulousLabs/Sia v1.3.7 github.com/bitly/go-simplejson v0.5.0 diff --git a/go.sum b/go.sum index fadafa6a1b..0429b4ef62 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/33cn/chain33 v1.72.0 h1:Nk1CYLSYd5rfvhw1szE2Jt0cJx3A8nZCZZvIEIfTarw= github.com/33cn/chain33 v1.72.0/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b h1:bcnU0R5fIHabIUfFP3SMsZ/u4Zd2xpvOAmFF31ygA74= github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= +github.com/33cn/chain33 v1.72.6 h1:xxb9LimMiPhEacmvFcMtZLRoTlyYU5RLUkxFV9VUye0= +github.com/33cn/chain33 v1.72.6/go.mod h1:6C5lKl8ejjoXtyhetFXz13t+JV4UQvp9H4t9eLOTNXs= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go v41.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= diff --git a/plugin/dapp/rgbx/cmd/ci/docker-compose.sh b/plugin/dapp/rgbx/cmd/ci/docker-compose.sh index 9c6bee7534..ebda42ead3 100755 --- a/plugin/dapp/rgbx/cmd/ci/docker-compose.sh +++ b/plugin/dapp/rgbx/cmd/ci/docker-compose.sh @@ -524,6 +524,20 @@ function apply_dynamic_tss_peers_and_restart() { wait_cli_ready "${PARA2_CLI}" wait_cli_ready "${PARA3_CLI}" wait_cli_ready "${PARA4_CLI}" + + # 临时诊断:重启后重新发现一次 peer 名,与写进配置的 TSS_PEERS(重启前) 对比。 + # 若不一致,说明节点 DHT 身份在重启后发生变化,配置里的 TSS peers 已失效。 + local after="" + local k + for ((k = 0; k < 30; k++)); do + after=$(collect_peer_names_from_cli "${PARA1_CLI}" | sort | uniq | sed '/^$/d' | paste -sd, -) + if [ -n "${after}" ]; then + break + fi + sleep 1 + done + log_step "DIAG TSS_PEERS(before restart)=${TSS_PEERS}" + log_step "DIAG peer names(after restart)=${after}" } function setup_para_nodegroup_on_main() { @@ -581,8 +595,20 @@ function wait_auto_dkg_commit() { log_step "auto DKG done, tssAddress=${tss_addr}" return 0 fi + # 临时诊断:记录 DKG 等待期间 para1 看到的 peer 名,与配置里的 TSS_PEERS 对比 + if [ "${i}" = "20" ] || [ "${i}" = "60" ]; then + local now_peers + now_peers=$(collect_peer_names_from_cli "${PARA1_CLI}" | sort | uniq | sed '/^$/d' | paste -sd, -) + log_step "DIAG t=${i}s TSS_PEERS(config)=${TSS_PEERS} peer names(now)=${now_peers}" + fi sleep 1 done + # 临时诊断:超时时导出容器日志,确认是节点未启动还是 TSS 流程未完成 + compose_cmd ps || true + for svc in main para1 para2 para3 para4; do + echo "===== ${svc} logs (tail 120) =====" + compose_cmd logs --tail=120 --no-log-prefix "${svc}" 2>&1 | tail -120 || true + done fail "auto DKG commit timeout" } From 70e1038c210b3e829aa3caad490c2659c7c2af52 Mon Sep 17 00:00:00 2001 From: king Date: Fri, 16 Oct 2026 12:26:08 +0800 Subject: [PATCH 4/5] debug(rgbx): control run on v1.72.0-based chain33 (temporary) --- go.mod | 2 +- go.sum | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a91dc74be9..d023b5b20d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.12 replace github.com/ava-labs/avalanchego => github.com/33cn/avalanchego v1.10.10-0.20240529041529-ada691598153 require ( - github.com/33cn/chain33 v1.72.6 + github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b github.com/BurntSushi/toml v1.3.2 github.com/NebulousLabs/Sia v1.3.7 github.com/bitly/go-simplejson v0.5.0 diff --git a/go.sum b/go.sum index 0429b4ef62..fadafa6a1b 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,6 @@ github.com/33cn/chain33 v1.72.0 h1:Nk1CYLSYd5rfvhw1szE2Jt0cJx3A8nZCZZvIEIfTarw= github.com/33cn/chain33 v1.72.0/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b h1:bcnU0R5fIHabIUfFP3SMsZ/u4Zd2xpvOAmFF31ygA74= github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= -github.com/33cn/chain33 v1.72.6 h1:xxb9LimMiPhEacmvFcMtZLRoTlyYU5RLUkxFV9VUye0= -github.com/33cn/chain33 v1.72.6/go.mod h1:6C5lKl8ejjoXtyhetFXz13t+JV4UQvp9H4t9eLOTNXs= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go v41.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= From e775d5562d4727a19ce5da07385d9426991a8512 Mon Sep 17 00:00:00 2001 From: king Date: Fri, 16 Oct 2026 13:40:09 +0800 Subject: [PATCH 5/5] debug(rgbx): verify the chain33 dht identity fix (temporary) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d023b5b20d..f6237b24b9 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.12 replace github.com/ava-labs/avalanchego => github.com/33cn/avalanchego v1.10.10-0.20240529041529-ada691598153 require ( - github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b + github.com/33cn/chain33 v1.72.7-0.20261016053907-a95e7f042582 github.com/BurntSushi/toml v1.3.2 github.com/NebulousLabs/Sia v1.3.7 github.com/bitly/go-simplejson v0.5.0 diff --git a/go.sum b/go.sum index fadafa6a1b..c67680b78c 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/33cn/chain33 v1.72.0 h1:Nk1CYLSYd5rfvhw1szE2Jt0cJx3A8nZCZZvIEIfTarw= github.com/33cn/chain33 v1.72.0/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= -github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b h1:bcnU0R5fIHabIUfFP3SMsZ/u4Zd2xpvOAmFF31ygA74= -github.com/33cn/chain33 v1.72.1-0.20261016021757-b12c753ff90b/go.mod h1:QCdEczXYJI43WTeFuzEpEhNZ/SfEnLOdFaw6r6LvPz8= +github.com/33cn/chain33 v1.72.7-0.20261016053907-a95e7f042582 h1:oeeusTut2d1I2qkAdR30Iix7VWY5sDcKi51e4VVzkfE= +github.com/33cn/chain33 v1.72.7-0.20261016053907-a95e7f042582/go.mod h1:6C5lKl8ejjoXtyhetFXz13t+JV4UQvp9H4t9eLOTNXs= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go v41.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=