From 69bbc0549061e080fc0c08189db245f37ecc675a Mon Sep 17 00:00:00 2001 From: king Date: Mon, 19 Oct 2026 16:28:56 +0800 Subject: [PATCH] fix(x2ethereum): finalize prophecies and separate prophecy ids Two related bridge safety defects: 1. ProcessClaim only rejected prophecies whose status was Failed. A prophecy in Success status still accepted claims from validators that had not voted yet; each of those transactions returned Success again and procEth2Chain33_lock / procEth2Chain33_burn re-ran ProcessSuccessfulClaimForLock (accDB.Mint) or the burn transfer, minting/paying the same cross-chain transfer repeatedly. 2. Prophecy IDs concatenated chainID, nonce, sender, token and claim type without separators, so (chainID=1,nonce=23) and (chainID=12,nonce=3) collided and unrelated cross-chain claims could poison each other. Behind ForkX2EthFixReplayCollisionNormalize: - Treat Success and Failed as terminal in ProcessClaim. - Build the prophecy ID with ':' separated fields via calcProphecyID, used by both oracle claim creation and the lock/burn handlers. - Update tests to derive prophecy ids with the same helper. --- plugin/dapp/x2ethereum/executor/exec_test.go | 8 ++--- plugin/dapp/x2ethereum/executor/prophecy.go | 30 ++++++++++++++----- .../dapp/x2ethereum/executor/prophecy_test.go | 12 ++++++++ .../x2ethereum/executor/x2ethereumaction.go | 8 ++--- .../dapp/x2ethereum/executor/x2ethereumdb.go | 11 +++++-- plugin/dapp/x2ethereum/types/const.go | 9 ++++++ plugin/dapp/x2ethereum/types/x2ethereum.go | 1 + 7 files changed, 62 insertions(+), 17 deletions(-) diff --git a/plugin/dapp/x2ethereum/executor/exec_test.go b/plugin/dapp/x2ethereum/executor/exec_test.go index c1d727d150..39ea8edbaa 100644 --- a/plugin/dapp/x2ethereum/executor/exec_test.go +++ b/plugin/dapp/x2ethereum/executor/exec_test.go @@ -197,7 +197,7 @@ func (x *suiteX2Ethereum) Test_4_Eth2Chain33() { _, err = x.x2eth.Query_GetEthProphecy(&types2.QueryEthProphecyParams{ID: "010x7B95B6EC7EbD73572298cEf32Bb54FA408207359"}) x.Equal(err, types.ErrNotFound) - ID := strconv.Itoa(0) + strconv.Itoa(0) + ethereumAddr + tokenContractAddress + "lock" + ID := calcProphecyID(payload, true) x.queryGetEthProphecy(ID, types2.EthBridgeStatus_SuccessStatusText) x.queryGetSymbolTotalAmountByTxType(1, symbol, "lock", "", "10") @@ -214,7 +214,7 @@ func (x *suiteX2Ethereum) Test_4_Eth2Chain33() { x.NoError(err) x.setDb(receipt) - x.queryGetEthProphecy("000x7B95B6EC7EbD73572298cEf32Bb54FA4082073590x0000000000000000000000000000000000000000lock", types2.EthBridgeStatus_SuccessStatusText) + x.queryGetEthProphecy(calcProphecyID(payload, true), types2.EthBridgeStatus_SuccessStatusText) x.queryGetSymbolTotalAmount(symbol, "", 7, true) x.queryGetSymbolTotalAmountByTxType(2, symbol, "withdraw", "", "3") @@ -231,7 +231,7 @@ func (x *suiteX2Ethereum) Test_4_Eth2Chain33() { x.NoError(err) x.setDb(receipt) - x.queryGetEthProphecy("010x7B95B6EC7EbD73572298cEf32Bb54FA4082073590x0000000000000000000000000000000000000000lock", types2.EthBridgeStatus_SuccessStatusText) + x.queryGetEthProphecy(calcProphecyID(payload, true), types2.EthBridgeStatus_SuccessStatusText) x.queryGetSymbolTotalAmountByTxType(1, symbol, "lock", "", "11") } @@ -278,7 +278,7 @@ func (x *suiteX2Ethereum) Test_5_Chain33ToEth() { x.queryGetSymbolTotalAmount("coins.bty", tokenContractAddress, 1, false) x.queryGetSymbolTotalAmountByTxType(1, "coins.bty", "withdraw", tokenContractAddress, "4") - x.queryGetEthProphecy("020x7B95B6EC7EbD73572298cEf32Bb54FA4082073590x0000000000000000000000000000000000000000burn", types2.EthBridgeStatus_SuccessStatusText) + x.queryGetEthProphecy(calcProphecyID(payload, true), types2.EthBridgeStatus_SuccessStatusText) } func (x *suiteX2Ethereum) accountSetup() { diff --git a/plugin/dapp/x2ethereum/executor/prophecy.go b/plugin/dapp/x2ethereum/executor/prophecy.go index 5fc482b90b..7f47115e8c 100644 --- a/plugin/dapp/x2ethereum/executor/prophecy.go +++ b/plugin/dapp/x2ethereum/executor/prophecy.go @@ -3,6 +3,7 @@ package executor import ( "fmt" "strconv" + "strings" "github.com/33cn/chain33/types" x2eTy "github.com/33cn/plugin/plugin/dapp/x2ethereum/types" @@ -86,17 +87,32 @@ func NewClaim(id string, validatorAddress string, content string) x2eTy.OracleCl } } +// calcProphecyID 通过ethchain33结构生成prophecy唯一ID +// 分叉后采用带分隔符的拼接,避免如(chainID=1,nonce=23)与(chainID=12,nonce=3)产生相同ID的碰撞 +func calcProphecyID(ethClaim *x2eTy.Eth2Chain33, isFixFork bool) string { + claimType := "lock" + if ethClaim.ClaimType == int64(x2eTy.BurnClaimType) { + claimType = "burn" + } + if isFixFork { + return strings.Join([]string{ + strconv.Itoa(int(ethClaim.EthereumChainID)), + strconv.Itoa(int(ethClaim.Nonce)), + ethClaim.EthereumSender, + ethClaim.TokenContractAddress, + claimType, + }, ":") + } + return strconv.Itoa(int(ethClaim.EthereumChainID)) + strconv.Itoa(int(ethClaim.Nonce)) + + ethClaim.EthereumSender + ethClaim.TokenContractAddress + claimType +} + // CreateOracleClaimFromEthClaim 通过ethchain33结构构造一个OracleClaim结构,包括生成唯一的ID -func CreateOracleClaimFromEthClaim(ethClaim x2eTy.Eth2Chain33) (x2eTy.OracleClaim, error) { +func CreateOracleClaimFromEthClaim(ethClaim x2eTy.Eth2Chain33, isFixFork bool) (x2eTy.OracleClaim, error) { if ethClaim.ClaimType != int64(x2eTy.LockClaimType) && ethClaim.ClaimType != int64(x2eTy.BurnClaimType) { return x2eTy.OracleClaim{}, x2eTy.ErrInvalidClaimType } - oracleID := strconv.Itoa(int(ethClaim.EthereumChainID)) + strconv.Itoa(int(ethClaim.Nonce)) + ethClaim.EthereumSender + ethClaim.TokenContractAddress - if ethClaim.ClaimType == int64(x2eTy.LockClaimType) { - oracleID = oracleID + "lock" - } else if ethClaim.ClaimType == int64(x2eTy.BurnClaimType) { - oracleID = oracleID + "burn" - } + oracleID := calcProphecyID(ðClaim, isFixFork) claimContent := NewOracleClaimContent(ethClaim.Chain33Receiver, ethClaim.Amount, ethClaim.ClaimType, ethClaim.Decimals) claimBytes := types.Encode(&claimContent) diff --git a/plugin/dapp/x2ethereum/executor/prophecy_test.go b/plugin/dapp/x2ethereum/executor/prophecy_test.go index 052bff8bf7..4d4fff6da8 100644 --- a/plugin/dapp/x2ethereum/executor/prophecy_test.go +++ b/plugin/dapp/x2ethereum/executor/prophecy_test.go @@ -30,3 +30,15 @@ func TestAddClaim(t *testing.T) { assert.Contains(t, prophecy.ClaimValidators[1].Validators.Validators, "bb") } + +// TestCalcProphecyIDSeparator 确认分叉后 ID 带分隔符,不同(chainID,nonce)组合不再碰撞 +func TestCalcProphecyIDSeparator(t *testing.T) { + claimA := &x2eTy.Eth2Chain33{EthereumChainID: 1, Nonce: 23, EthereumSender: "0xsender", TokenContractAddress: "0xtoken", ClaimType: int64(x2eTy.LockClaimType)} + claimB := &x2eTy.Eth2Chain33{EthereumChainID: 12, Nonce: 3, EthereumSender: "0xsender", TokenContractAddress: "0xtoken", ClaimType: int64(x2eTy.LockClaimType)} + + // 旧格式无分隔符,两种组合拼出相同 ID + assert.Equal(t, calcProphecyID(claimA, false), calcProphecyID(claimB, false)) + // 分叉后带分隔符,ID 不再碰撞 + assert.NotEqual(t, calcProphecyID(claimA, true), calcProphecyID(claimB, true)) + assert.NotEqual(t, calcProphecyID(claimA, true), calcProphecyID(claimA, false)) +} diff --git a/plugin/dapp/x2ethereum/executor/x2ethereumaction.go b/plugin/dapp/x2ethereum/executor/x2ethereumaction.go index 84a7c2d5e6..3e1f04a82d 100644 --- a/plugin/dapp/x2ethereum/executor/x2ethereumaction.go +++ b/plugin/dapp/x2ethereum/executor/x2ethereumaction.go @@ -1,7 +1,6 @@ package executor import ( - "strconv" "strings" "github.com/33cn/chain33/account" @@ -29,9 +28,10 @@ type action struct { func newAction(a *x2ethereum, tx *types.Transaction, index int32) *action { hash := tx.Hash() fromaddr := tx.From() + isFixFork := a.GetAPI().GetConfig().IsDappFork(a.GetHeight(), x2eTy.X2ethereumX, x2eTy.ForkX2EthFixReplayCollisionNormalize) return &action{a.GetAPI(), a.GetCoinsAccount(), a.GetStateDB(), hash, fromaddr, - a.GetBlockTime(), a.GetHeight(), index, address.ExecAddress(string(tx.Execer)), NewOracle(a.GetStateDB(), x2eTy.DefaultConsensusNeeded)} + a.GetBlockTime(), a.GetHeight(), index, address.ExecAddress(string(tx.Execer)), NewOracle(a.GetStateDB(), x2eTy.DefaultConsensusNeeded, isFixFork)} } // ethereum ---> chain33 @@ -49,7 +49,7 @@ func (a *action) procEth2Chain33_lock(ethBridgeClaim *x2eTy.Eth2Chain33) (*types return nil, err } - ID := strconv.Itoa(int(ethBridgeClaim.EthereumChainID)) + strconv.Itoa(int(ethBridgeClaim.Nonce)) + ethBridgeClaim.EthereumSender + ethBridgeClaim.TokenContractAddress + "lock" + ID := calcProphecyID(ethBridgeClaim, a.oracle.isFixFork) //记录ethProphecy bz, err := a.db.Get(x2eTy.CalProphecyPrefix(ID)) @@ -209,7 +209,7 @@ func (a *action) procEth2Chain33_burn(withdrawEth *x2eTy.Eth2Chain33) (*types.Re return nil, err } - ID := strconv.Itoa(int(withdrawEth.EthereumChainID)) + strconv.Itoa(int(withdrawEth.Nonce)) + withdrawEth.EthereumSender + withdrawEth.TokenContractAddress + "burn" + ID := calcProphecyID(withdrawEth, a.oracle.isFixFork) //记录ethProphecy bz, err := a.db.Get(x2eTy.CalProphecyPrefix(ID)) diff --git a/plugin/dapp/x2ethereum/executor/x2ethereumdb.go b/plugin/dapp/x2ethereum/executor/x2ethereumdb.go index c1fa66af36..d62b00ee10 100644 --- a/plugin/dapp/x2ethereum/executor/x2ethereumdb.go +++ b/plugin/dapp/x2ethereum/executor/x2ethereumdb.go @@ -15,16 +15,18 @@ import ( type Oracle struct { db dbm.KV consensusThreshold int64 + isFixFork bool } // NewOracle ... -func NewOracle(db dbm.KV, consensusThreshold int64) *Oracle { +func NewOracle(db dbm.KV, consensusThreshold int64, isFixFork bool) *Oracle { if consensusThreshold <= 0 || consensusThreshold > 100 { return nil } return &Oracle{ consensusThreshold: consensusThreshold, db: db, + isFixFork: isFixFork, } } @@ -292,7 +294,7 @@ func (o *Oracle) checkProphecy(prophecy *x2eTy.ReceiptEthProphecy) error { // ProcessClaim 处理接收到的ethchain33请求 func (o *Oracle) ProcessClaim(claim x2eTy.Eth2Chain33) (*x2eTy.ProphecyStatus, error) { - oracleClaim, err := CreateOracleClaimFromEthClaim(claim) + oracleClaim, err := CreateOracleClaimFromEthClaim(claim, o.isFixFork) if err != nil { elog.Error("CreateEthClaimFromOracleString", "CreateOracleClaimFromOracleString error", err) return nil, err @@ -317,6 +319,11 @@ func (o *Oracle) ProcessClaim(claim x2eTy.Eth2Chain33) (*x2eTy.ProphecyStatus, e } prophecy = NewProphecy(oracleClaim.ID) } else { + //分叉后prophecy进入Success/Failed即为终态,后续投票直接拒绝,避免重复铸币/解锁 + if o.isFixFork && (prophecy.Status.Text == x2eTy.EthBridgeStatus_SuccessStatusText || + prophecy.Status.Text == x2eTy.EthBridgeStatus_FailedStatusText) { + return nil, x2eTy.ErrProphecyFinalized + } var exist bool for _, vc := range prophecy.ValidatorClaims { if vc.Claim == oracleClaim.Content { diff --git a/plugin/dapp/x2ethereum/types/const.go b/plugin/dapp/x2ethereum/types/const.go index 62a15c4502..3fef99c0ea 100644 --- a/plugin/dapp/x2ethereum/types/const.go +++ b/plugin/dapp/x2ethereum/types/const.go @@ -63,6 +63,15 @@ const ( // DefaultConsensusNeeded ... const DefaultConsensusNeeded = int64(70) +// dapp fork +const ( + // ForkX2EthFixReplayCollisionNormalize 修复三个安全问题: + // 1. prophecy达到Success/Failed后未进入终态, 后续validator投票会重复触发铸币/解锁 + // 2. prophecy ID由chainID+nonce+sender+token无分隔符拼接, 不同(chainID, nonce)组合产生碰撞 + // 3. procMsgTransfer的payload To未归一化, 大小写变体可绕过自我转账检查 + ForkX2EthFixReplayCollisionNormalize = "ForkX2EthFixReplayCollisionNormalize" +) + // direct ... const ( DirEth2Chain33 = "eth2chain33" diff --git a/plugin/dapp/x2ethereum/types/x2ethereum.go b/plugin/dapp/x2ethereum/types/x2ethereum.go index 087555ae32..446a578226 100644 --- a/plugin/dapp/x2ethereum/types/x2ethereum.go +++ b/plugin/dapp/x2ethereum/types/x2ethereum.go @@ -60,6 +60,7 @@ func init() { // InitFork defines register fork func InitFork(cfg *types.Chain33Config) { cfg.RegisterDappFork(X2ethereumX, "Enable", 0) + cfg.RegisterDappFork(X2ethereumX, ForkX2EthFixReplayCollisionNormalize, 0) } // InitExecutor defines register executor