diff --git a/.golangci.yml b/.golangci.yml index bba57feb0..006237371 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ version: "2" run: - go: 1.26.1 + go: 1.26.2 tests: true linters: enable: diff --git a/app/abci_test.go b/app/abci_test.go index d78214b96..bd4cc8a19 100644 --- a/app/abci_test.go +++ b/app/abci_test.go @@ -530,7 +530,6 @@ func TestPreBlocker(t *testing.T) { validators := app.StakeKeeper.GetAllValidators(ctx) msg := &borTypes.MsgProposeSpan{ - // SpanId: 2, Proposer: validators[0].Signer, StartBlock: 26657, EndBlock: 30000, @@ -652,8 +651,6 @@ func TestSideTxsHappyPath(t *testing.T) { ), ) - // coins, _ := simulation.RandomFees(rand.New(rand.NewSource(time.Now().UnixNano())), ctx, sdk.Coins{sdk.NewCoin(authTypes.FeeToken, math.NewInt(1000000000000000000))}) - testCases := []struct { name string msg sdk.Msg @@ -673,7 +670,7 @@ func TestSideTxsHappyPath(t *testing.T) { { name: "Clerk Module Happy Path", msg: func() *clerkTypes.MsgEventRecord { - rec := clerkTypes.NewMsgEventRecord( + return new(clerkTypes.NewMsgEventRecord( validators[0].Signer, TxHash1, 1, @@ -682,8 +679,7 @@ func TestSideTxsHappyPath(t *testing.T) { propAddr, make([]byte, 0), "0", - ) - return &rec + )) }(), }, } @@ -858,7 +854,7 @@ func TestAllUnhappyPathBorSideTxs(t *testing.T) { blockHeader1 := ethTypes.Header{Number: big.NewInt(int64(seedBlock1))} blockHash1 := blockHeader1.Hash() - mockCaller.On("GetBorChainBlockAuthor", mock.Anything).Return(&val1Addr, nil) + mockCaller.On("GetBorChainBlockAuthor", mock.Anything, mock.Anything).Return(&val1Addr, nil) mockCaller.On("GetBorChainBlock", mock.Anything, mock.Anything).Return(&blockHeader1, nil) mockCaller. @@ -1116,17 +1112,6 @@ func TestAllUnhappyPathClerkSideTxs(t *testing.T) { Address2 := "0xb316fa9fa91700d7084d377bfdc81eb9f232f5ff" addrBz2, err := ac.StringToBytes(Address2) - msg := clerkTypes.NewMsgEventRecord( - addressUtils.FormatAddress("0xa316fa9fa91700d7084d377bfdc81eb9f232f5ff"), - TxHash1, - logIndex, - blockNumber, - 10, - addrBz2, - make([]byte, 0), - "101", - ) - mockCaller.On("GetConfirmedTxReceipt", mock.Anything, mock.Anything).Return(nil, nil).Once() mockCaller. On("GetBorChainBlock", mock.Anything, mock.Anything). @@ -1137,7 +1122,16 @@ func TestAllUnhappyPathClerkSideTxs(t *testing.T) { On("GetBorChainBlockInfoInBatch", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")). Return([]*ethTypes.Header{}, []uint64{}, []common.Address{}, nil) - txBytes, err := buildSignedTx(&msg, validators[0].Signer, ctx, priv, app) + txBytes, err := buildSignedTx(new(clerkTypes.NewMsgEventRecord( + addressUtils.FormatAddress("0xa316fa9fa91700d7084d377bfdc81eb9f232f5ff"), + TxHash1, + logIndex, + blockNumber, + 10, + addrBz2, + make([]byte, 0), + "101", + )), validators[0].Signer, ctx, priv, app) var txBytesCmt cmtTypes.Tx = txBytes extCommitBytes, extCommit, _, err := buildExtensionCommits(t, &app, txBytesCmt.Hash(), validators, validatorPrivKeys, 2) @@ -1194,7 +1188,14 @@ func TestAllUnhappyPathClerkSideTxs(t *testing.T) { addrBz2, err := ac.StringToBytes(Address2) - msg := clerkTypes.NewMsgEventRecord( + mockCaller.On("GetConfirmedTxReceipt", mock.Anything, mock.Anything).Return(txReceipt, nil).Once() + mockCaller.On("DecodeStateSyncedEvent", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() + + mockCaller. + On("GetBorChainBlockInfoInBatch", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")). + Return([]*ethTypes.Header{}, []uint64{}, []common.Address{}, nil) + + txBytes, err := buildSignedTx(new(clerkTypes.NewMsgEventRecord( addressUtils.FormatAddress("0xa316fa9fa91700d7084d377bfdc81eb9f232f5ff"), TxHash1, logIndex, @@ -1203,16 +1204,7 @@ func TestAllUnhappyPathClerkSideTxs(t *testing.T) { addrBz2, make([]byte, 0), "0", - ) - - mockCaller.On("GetConfirmedTxReceipt", mock.Anything, mock.Anything).Return(txReceipt, nil).Once() - mockCaller.On("DecodeStateSyncedEvent", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() - - mockCaller. - On("GetBorChainBlockInfoInBatch", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")). - Return([]*ethTypes.Header{}, []uint64{}, []common.Address{}, nil) - - txBytes, err := buildSignedTx(&msg, validators[0].Signer, ctx, priv, app) + )), validators[0].Signer, ctx, priv, app) var txBytesCmt cmtTypes.Tx = txBytes extCommitBytes, extCommit, _, err := buildExtensionCommits(t, &app, txBytesCmt.Hash(), validators, validatorPrivKeys, 2) @@ -1363,17 +1355,6 @@ func TestAllUnhappyPathClerkSideTxs(t *testing.T) { addrBz2, err := ac.StringToBytes(Address2) - msg := clerkTypes.NewMsgEventRecord( - addressUtils.FormatAddress("0xa316fa9fa91700d7084d377bfdc81eb9f232f5ff"), - TxHash1, - logIndex, - blockNumber, - id, - addrBz2, - make([]byte, 0), - "0", - ) - mockCaller.On("GetConfirmedTxReceipt", mock.Anything, mock.Anything).Return(txReceipt, nil).Once() mockCaller.On("DecodeStateSyncedEvent", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() @@ -1383,7 +1364,16 @@ func TestAllUnhappyPathClerkSideTxs(t *testing.T) { On("GetBorChainBlockInfoInBatch", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")). Return([]*ethTypes.Header{}, []uint64{}, []common.Address{}, nil) - txBytes, err := buildSignedTx(&msg, validators[0].Signer, ctx, priv, app) + txBytes, err := buildSignedTx(new(clerkTypes.NewMsgEventRecord( + addressUtils.FormatAddress("0xa316fa9fa91700d7084d377bfdc81eb9f232f5ff"), + TxHash1, + logIndex, + blockNumber, + id, + addrBz2, + make([]byte, 0), + "0", + )), validators[0].Signer, ctx, priv, app) var txBytesCmt cmtTypes.Tx = txBytes extCommitBytes, extCommit, _, err := buildExtensionCommits(t, &app, txBytesCmt.Hash(), validators, validatorPrivKeys, 2) @@ -1528,15 +1518,6 @@ func TestAllUnhappyPathTopupSideTxs(t *testing.T) { coins, err := simulation.RandomFees(rand.New(rand.NewSource(time.Now().UnixNano())), ctx, sdk.Coins{sdk.NewCoin(authTypes.FeeToken, math.NewInt(1000000000000000000))}) - msg := *topUpTypes.NewMsgTopupTx( - addr1.String(), - addr1.String(), - coins.AmountOf(authTypes.FeeToken), - hash, - logIndex, - blockNumber, - ) - mockCaller.On("GetConfirmedTxReceipt", mock.Anything, mock.Anything).Return(nil, nil).Once() mockCaller.On("DecodeStateSyncedEvent", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() mockCaller. @@ -1548,7 +1529,14 @@ func TestAllUnhappyPathTopupSideTxs(t *testing.T) { On("GetBorChainBlockInfoInBatch", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")). Return([]*ethTypes.Header{}, []uint64{}, []common.Address{}, nil) - txBytes, err := buildSignedTx(&msg, validators[0].Signer, ctx, priv, app) + txBytes, err := buildSignedTx(new(*topUpTypes.NewMsgTopupTx( + addr1.String(), + addr1.String(), + coins.AmountOf(authTypes.FeeToken), + hash, + logIndex, + blockNumber, + )), validators[0].Signer, ctx, priv, app) var txBytesCmt cmtTypes.Tx = txBytes extCommitBytes, extCommit, _, err := buildExtensionCommits(t, &app, txBytesCmt.Hash(), validators, validatorPrivKeys, 2) @@ -1604,15 +1592,6 @@ func TestAllUnhappyPathTopupSideTxs(t *testing.T) { coins, err := simulation.RandomFees(rand.New(rand.NewSource(time.Now().UnixNano())), ctx, sdk.Coins{sdk.NewCoin(authTypes.FeeToken, math.NewInt(1000000000000000000))}) - msg := *topUpTypes.NewMsgTopupTx( - addr1.String(), - addr1.String(), - coins.AmountOf(authTypes.FeeToken), - hash, - logIndex, - blockNumber, - ) - mockCaller.On("GetConfirmedTxReceipt", mock.Anything, mock.Anything).Return(txReceipt, nil).Once() mockCaller.On("DecodeValidatorTopupFeesEvent", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() @@ -1620,7 +1599,14 @@ func TestAllUnhappyPathTopupSideTxs(t *testing.T) { On("GetBorChainBlockInfoInBatch", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")). Return([]*ethTypes.Header{}, []uint64{}, []common.Address{}, nil) - txBytes, err := buildSignedTx(&msg, validators[0].Signer, ctx, priv, app) + txBytes, err := buildSignedTx(new(*topUpTypes.NewMsgTopupTx( + addr1.String(), + addr1.String(), + coins.AmountOf(authTypes.FeeToken), + hash, + logIndex, + blockNumber, + )), validators[0].Signer, ctx, priv, app) var txBytesCmt cmtTypes.Tx = txBytes extCommitBytes, extCommit, _, err := buildExtensionCommits(t, &app, txBytesCmt.Hash(), validators, validatorPrivKeys, 2) @@ -1675,14 +1661,6 @@ func TestAllUnhappyPathTopupSideTxs(t *testing.T) { coins, err := simulation.RandomFees(rand.New(rand.NewSource(time.Now().UnixNano())), ctx, sdk.Coins{sdk.NewCoin(authTypes.FeeToken, math.NewInt(1000000000000000000))}) - msg := *topUpTypes.NewMsgTopupTx( - addr1.String(), - addr1.String(), - coins.AmountOf(authTypes.FeeToken), - hash, - logIndex, - blockNumber, - ) event := &stakinginfo.StakinginfoTopUpFee{ User: common.Address(addr1.Bytes()), Fee: coins.AmountOf(authTypes.FeeToken).BigInt(), @@ -1695,7 +1673,14 @@ func TestAllUnhappyPathTopupSideTxs(t *testing.T) { On("GetBorChainBlockInfoInBatch", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")). Return([]*ethTypes.Header{}, []uint64{}, []common.Address{}, nil) - txBytes, err := buildSignedTx(&msg, validators[0].Signer, ctx, priv, app) + txBytes, err := buildSignedTx(new(*topUpTypes.NewMsgTopupTx( + addr1.String(), + addr1.String(), + coins.AmountOf(authTypes.FeeToken), + hash, + logIndex, + blockNumber, + )), validators[0].Signer, ctx, priv, app) var txBytesCmt cmtTypes.Tx = txBytes extCommitBytes, extCommit, _, err := buildExtensionCommits(t, &app, txBytesCmt.Hash(), validators, validatorPrivKeys, 2) @@ -1750,14 +1735,6 @@ func TestAllUnhappyPathTopupSideTxs(t *testing.T) { coins, err := simulation.RandomFees(rand.New(rand.NewSource(time.Now().UnixNano())), ctx, sdk.Coins{sdk.NewCoin(authTypes.FeeToken, math.NewInt(1000000000000000000))}) - msg := *topUpTypes.NewMsgTopupTx( - addr1.String(), - addr1.String(), - coins.AmountOf(authTypes.FeeToken), - hash, - logIndex, - blockNumber, - ) event := &stakinginfo.StakinginfoTopUpFee{ User: common.Address(addr2.Bytes()), Fee: coins.AmountOf(authTypes.FeeToken).BigInt(), @@ -1771,9 +1748,14 @@ func TestAllUnhappyPathTopupSideTxs(t *testing.T) { On("GetBorChainBlockInfoInBatch", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")). Return([]*ethTypes.Header{}, []uint64{}, []common.Address{}, nil) - // mockChainKeeper.EXPECT().GetParams(gomock.Any()).Return(chainmanagertypes.DefaultParams(), nil).AnyTimes() - - txBytes, err := buildSignedTx(&msg, validators[0].Signer, ctx, priv, app) + txBytes, err := buildSignedTx(new(*topUpTypes.NewMsgTopupTx( + addr1.String(), + addr1.String(), + coins.AmountOf(authTypes.FeeToken), + hash, + logIndex, + blockNumber, + )), validators[0].Signer, ctx, priv, app) var txBytesCmt cmtTypes.Tx = txBytes extCommitBytes, extCommit, _, err := buildExtensionCommits(t, &app, txBytesCmt.Hash(), validators, validatorPrivKeys, 2) @@ -2517,7 +2499,6 @@ func TestPrepareProposal(t *testing.T) { BlockHash: goodExt.BlockHash, Height: goodExt.Height, // keep height correct SideTxResponses: badSide, // invalid payload - // MilestoneProposition: nil, // optional } fakeBz2, err := gogoproto.Marshal(fakeExt2) require.NoError(t, err, "gogo‐Marshal should succeed") @@ -2568,7 +2549,6 @@ func TestPrepareProposal(t *testing.T) { require.NoError(t, err) msgBor := &borTypes.MsgProposeSpan{ - // SpanId: 2, Proposer: validators[0].Signer, StartBlock: 26657, EndBlock: 30000, @@ -2619,7 +2599,7 @@ func TestPrepareProposal(t *testing.T) { _, err = app.PreBlocker(ctx, &finalizeReqBorSideTx) require.NoError(t, err) - msgClerk := clerkTypes.NewMsgEventRecord( + require.NoError(t, txBuilder.SetMsgs(new(clerkTypes.NewMsgEventRecord( validators[0].Signer, TxHash1, 1, @@ -2628,8 +2608,7 @@ func TestPrepareProposal(t *testing.T) { propAddr, make([]byte, 0), "0", - ) - require.NoError(t, txBuilder.SetMsgs(&msgClerk)) + )))) require.NoError(t, err) require.NoError(t, txBuilder.SetSignatures(sigV2)) @@ -3074,8 +3053,8 @@ func TestCheckAndRotateCurrentSpan(t *testing.T) { // Mock IContractCaller with proper producer mapping mockCaller := new(helpermocks.IContractCaller) producerSignerStr := validators[0].Signer - producerSignerAddr := common.HexToAddress(producerSignerStr) - mockCaller.On("GetBorChainBlockAuthor", mock.Anything, lastMilestone.EndBlock+1).Return(&producerSignerAddr, nil) + mockCaller.On("GetBorChainBlockAuthor", mock.Anything, lastMilestone.EndBlock+1). + Return(new(common.HexToAddress(producerSignerStr)), nil) app.BorKeeper.SetContractCaller(mockCaller) // Call the function @@ -3147,8 +3126,8 @@ func TestPreBlockerSpanRotationWithMinorityMilestone(t *testing.T) { // Set up the mock contract caller mockCaller := new(helpermocks.IContractCaller) - producerSigner := common.HexToAddress(validators[0].Signer) - mockCaller.On("GetBorChainBlockAuthor", mock.Anything, mock.Anything).Return(&producerSigner, nil) + mockCaller.On("GetBorChainBlockAuthor", mock.Anything, mock.Anything). + Return(new(common.HexToAddress(validators[0].Signer)), nil) app.BorKeeper.SetContractCaller(mockCaller) // Set context to trigger span rotation conditions @@ -3229,8 +3208,8 @@ func TestPreBlockerSpanRotationWithoutMinorityMilestone(t *testing.T) { // Set up the mock contract caller mockCaller := new(helpermocks.IContractCaller) - producerSigner := common.HexToAddress(validators[0].Signer) - mockCaller.On("GetBorChainBlockAuthor", mock.Anything, mock.Anything).Return(&producerSigner, nil) + mockCaller.On("GetBorChainBlockAuthor", mock.Anything, mock.Anything). + Return(new(common.HexToAddress(validators[0].Signer)), nil) app.BorKeeper.SetContractCaller(mockCaller) // Set context to trigger span rotation conditions @@ -3772,7 +3751,7 @@ func TestPrepareProposal_AccountSequenceMismatch(t *testing.T) { require.NotNil(t, res) // Only the first transaction should be accepted (sequence 0 is correct for the first tx) - // All subsequent transactions will fail because they also have sequence 0 but the account sequence is now 1 + // All later transactions will fail because they also have sequence 0, but the account sequence is now 1 // Result should be: 1 ExtendedCommitInfo + 1 successful tx = 2 total require.Equal(t, 2, len(res.Txs), "Should have exactly 2 transactions (1 ExtendedCommitInfo + 1 tx, others rejected due to sequence mismatch)") }) diff --git a/go.mod b/go.mod index b27d9b8bd..e27e292d8 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/0xPolygon/heimdall-v2 // Note: Change the go image version in Dockerfile if you change this. -go 1.26.1 +go 1.26.2 require ( cosmossdk.io/api v0.7.5 @@ -14,7 +14,7 @@ require ( cosmossdk.io/store v1.1.1 cosmossdk.io/tools/confix v0.1.1 cosmossdk.io/x/tx v0.13.7 - github.com/0xPolygon/polyproto v0.0.7 + github.com/0xPolygon/polyproto v0.0.8-0.20260423132317-7d955b45ef8a github.com/RichardKnop/machinery v1.10.8 github.com/bufbuild/buf v1.61.0 github.com/cbergoon/merkletree v0.2.0 @@ -42,12 +42,12 @@ require ( github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 golang.org/x/crypto v0.46.0 golang.org/x/sync v0.19.0 - google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 - google.golang.org/grpc v1.77.0 + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 + google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.10 gopkg.in/yaml.v3 v3.0.1 ) @@ -67,7 +67,7 @@ require ( buf.build/go/protoyaml v0.6.0 // indirect buf.build/go/spdx v0.2.0 // indirect buf.build/go/standard v0.1.0 // indirect - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.121.6 // indirect cloud.google.com/go/auth v0.16.4 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -278,7 +278,7 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect @@ -286,15 +286,15 @@ require ( golang.org/x/arch v0.4.0 // indirect golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/api v0.247.0 // indirect google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools/v3 v3.5.2 // indirect nhooyr.io/websocket v1.8.7 // indirect @@ -318,7 +318,7 @@ replace ( github.com/cometbft/cometbft => github.com/0xPolygon/cometbft v0.3.6-polygon github.com/cometbft/cometbft-db => github.com/0xPolygon/cometbft-db v0.14.1-polygon github.com/cosmos/cosmos-sdk => github.com/0xPolygon/cosmos-sdk v0.2.8-polygon - github.com/ethereum/go-ethereum => github.com/0xPolygon/bor v1.14.14-0.20260304162036-54a90c4aa8ef + github.com/ethereum/go-ethereum => github.com/0xPolygon/bor v1.14.14-0.20260423133202-fd961fa68a2d // Following version of goleveldb might cause an unexpected behavior. github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 nhooyr.io/websocket => github.com/coder/websocket v1.8.7 diff --git a/go.sum b/go.sum index d02508399..87fa37b85 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= buf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U= buf.build/go/standard v0.1.0/go.mod h1:PiqpHz/7ZFq+kqvYhc/SK3lxFIB9N/aiH2CFC2JHIQg= -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -92,8 +92,8 @@ cosmossdk.io/depinject v1.2.1/go.mod h1:lqQEycz0H2JXqvOgVwTsjEdMI0plswI7p6KX+MVq dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/0xPolygon/bor v1.14.14-0.20260304162036-54a90c4aa8ef h1:ASRWYbexk0+Tvh2LE0LceG29k13v7vfg7NBSIjkaREs= -github.com/0xPolygon/bor v1.14.14-0.20260304162036-54a90c4aa8ef/go.mod h1:1uAgvR+L8fzmnEi2/7+IA+Vwi1j/pfxrYi8N7A047n8= +github.com/0xPolygon/bor v1.14.14-0.20260423133202-fd961fa68a2d h1:wbCWrBCgFCo2RpzWyASbM8pOiQkoX4tudk5x22WgEFM= +github.com/0xPolygon/bor v1.14.14-0.20260423133202-fd961fa68a2d/go.mod h1:uhAIzrrvF6ZE52PMtnXqJnYOqpy4CHxhEItZFceAw3U= github.com/0xPolygon/cometbft v0.3.6-polygon h1:V2ZB0ECg59xGrS1FKJv4p88UF2HeYMTmFl+UTLSuyFA= github.com/0xPolygon/cometbft v0.3.6-polygon/go.mod h1:ZFIXbBrjS2AlVL5UFIgnShMe3b2dso9M0yHLN+mBg4Y= github.com/0xPolygon/cometbft-db v0.14.1-polygon h1:sMlEPISgW0Wm9bC3bnLVPiPnyZ9EOuWJxoAV8ujrN3o= @@ -122,8 +122,8 @@ github.com/0xPolygon/cosmos-sdk/x/tx v0.13.6-0.20241126102051-89dc71d02611 h1:Z1 github.com/0xPolygon/cosmos-sdk/x/tx v0.13.6-0.20241126102051-89dc71d02611/go.mod h1:ZCQM9tU2LeW5kIuaq0q02KRHhMWAvNtj4cmVRyLfZmQ= github.com/0xPolygon/crand v1.0.3 h1:BYYflmgLhmGPEgqtopG4muq6wV6DOkwD8uPymNz5WeQ= github.com/0xPolygon/crand v1.0.3/go.mod h1:km4366oC7EVFl1xNUCwzxUXNM10swZqd8LZ0E5SgbAE= -github.com/0xPolygon/polyproto v0.0.7 h1:Ody+kFyCRK4QXRPXbsP5pdxKrDgwAAXtFB8NPgaIxRs= -github.com/0xPolygon/polyproto v0.0.7/go.mod h1:2Iw93k2LismvckKKeXQITuhJH9vLbqOa212AMskH6no= +github.com/0xPolygon/polyproto v0.0.8-0.20260423132317-7d955b45ef8a h1:vVtSjO29FcFBZbNVsVGy6z3lv3RRr/sI1vPR7IzbZZE= +github.com/0xPolygon/polyproto v0.0.8-0.20260423132317-7d955b45ef8a/go.mod h1:2Iw93k2LismvckKKeXQITuhJH9vLbqOa212AMskH6no= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= @@ -267,8 +267,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -403,12 +403,12 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= -github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= @@ -988,16 +988,14 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= -github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons= +github.com/pion/dtls/v3 v3.0.11/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -1189,6 +1187,8 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -1253,20 +1253,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= @@ -1405,8 +1405,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1417,8 +1417,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1516,8 +1516,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1692,10 +1692,10 @@ google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846 h1:dDbsTLIK7EzwUq36kCSAsk0slouq/S0tWHeeGi97cD8= google.golang.org/genproto v0.0.0-20251124214823-79d6a2a48846/go.mod h1:PP0g88Dz3C7hRAfbQCQggeWAXjuqGsNPLE4s7jh0RGU= -google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk= -google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1722,8 +1722,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/helper/call.go b/helper/call.go index d7967c8dd..2bb479391 100644 --- a/helper/call.go +++ b/helper/call.go @@ -25,7 +25,7 @@ import ( "github.com/0xPolygon/heimdall-v2/contracts/statereceiver" "github.com/0xPolygon/heimdall-v2/contracts/statesender" "github.com/0xPolygon/heimdall-v2/contracts/validatorset" - "github.com/0xPolygon/heimdall-v2/x/bor/grpc" + borgrpc "github.com/0xPolygon/heimdall-v2/x/bor/grpc" "github.com/0xPolygon/heimdall-v2/x/stake/types" ) @@ -66,7 +66,7 @@ type IContractCaller interface { GetBorChainBlock(context.Context, *big.Int) (*ethTypes.Header, error) GetBorChainBlockInfoInBatch(ctx context.Context, start, end int64) ([]*ethTypes.Header, []uint64, []common.Address, error) GetBorChainBlockTd(ctx context.Context, blockHash common.Hash) (uint64, error) - GetBorChainBlockAuthor(*big.Int) (*common.Address, error) + GetBorChainBlockAuthor(ctx context.Context, blockNum *big.Int) (*common.Address, error) IsTxConfirmed(common.Hash, uint64) bool GetConfirmedTxReceipt(common.Hash, uint64) (*ethTypes.Receipt, error) GetBlockNumberFromTxHash(common.Hash) (*big.Int, error) @@ -103,6 +103,23 @@ type IContractCaller interface { GetTokenInstance(tokenAddress string) (*erc20.Erc20, error) } +// BorGRPCClienter is the subset of *grpc.BorGRPCClient used by helper code. +// Declared as an interface so tests can inject fakes without dialing a real +// bor gRPC server. The concrete *grpc.BorGRPCClient satisfies this interface +// automatically. +type BorGRPCClienter interface { + HeaderByNumber(ctx context.Context, blockID int64) (*ethTypes.Header, error) + BlockByNumber(ctx context.Context, blockID int64) (*ethTypes.Block, error) + GetRootHash(ctx context.Context, startBlock uint64, endBlock uint64) (string, error) + GetVoteOnHash(ctx context.Context, startBlock uint64, endBlock uint64, rootHash string, milestoneId string) (bool, error) + GetAuthor(ctx context.Context, blockNum *big.Int) (*common.Address, error) + GetTdByHash(ctx context.Context, hash common.Hash) (uint64, error) + GetTdByNumber(ctx context.Context, blockNum *big.Int) (uint64, error) + GetBlockInfoInBatch(ctx context.Context, start, end int64) ([]*ethTypes.Header, []uint64, []common.Address, error) + TransactionReceipt(ctx context.Context, txHash common.Hash) (*ethTypes.Receipt, error) + BorBlockReceipt(ctx context.Context, txHash common.Hash) (*ethTypes.Receipt, error) +} + // ContractCaller contract caller type ContractCaller struct { MainChainClient *ethclient.Client @@ -114,7 +131,7 @@ type ContractCaller struct { BorChainTimeout time.Duration BorChainGrpcFlag bool - BorChainGrpcClient *grpc.BorGRPCClient + BorChainGrpcClient BorGRPCClienter RootChainABI abi.ABI StakingInfoABI abi.ABI @@ -148,7 +165,9 @@ func NewContractCaller() (contractCallerObj ContractCaller, err error) { contractCallerObj.MainChainRPC = GetMainChainRPCClient() contractCallerObj.BorChainRPCClient = GetBorRPCClient() contractCallerObj.BorChainGrpcFlag = config.BorGRPCFlag - contractCallerObj.BorChainGrpcClient = GetBorGRPCClient() + if client := GetBorGRPCClient(); client != nil { + contractCallerObj.BorChainGrpcClient = client + } // listeners and processors instance cache (address->ABI) contractCallerObj.ContractInstanceCache = make(map[common.Address]interface{}) @@ -320,7 +339,7 @@ func (c *ContractCaller) GetTokenInstance(tokenAddress string) (*erc20.Erc20, er return contractInstance.(*erc20.Erc20), nil } -// GetHeaderInfo get header info from checkpoint number +// GetHeaderInfo get header info from the checkpoint number func (c *ContractCaller) GetHeaderInfo(headerID uint64, rootChainInstance *rootchain.Rootchain, childBlockInterval uint64) ( root common.Hash, start, @@ -378,7 +397,15 @@ func (c *ContractCaller) GetRootHash(start, end, checkpointLength uint64) ([]byt return nil, err } - return common.FromHex(rootHash), nil + decoded := common.FromHex(rootHash) + if len(decoded) != common.HashLength { + return nil, fmt.Errorf("bor rootHash: expected %d bytes, got %d", common.HashLength, len(decoded)) + } + if (common.BytesToHash(decoded) == common.Hash{}) { + return nil, errors.New("bor rootHash: zero value") + } + + return decoded, nil } // GetVoteOnHash get vote on hash from the bor chain for the corresponding milestone @@ -547,101 +574,159 @@ func (c *ContractCaller) GetBorChainBlock(ctx context.Context, blockNum *big.Int } if err != nil { - Logger.Error(errUnableToConnect, "error", err) + // both HTTP and gRPC map a missing block to ethereum.NotFound. + if !errors.Is(err, ethereum.NotFound) { + Logger.Error(errUnableToConnect, "error", err) + } return } return latestBlock, nil } -// GetBorChainBlockInfoInBatch returns bor chain block headers and TD via a single RPC Batch call. -// It tries to get blocks from the range interval but returns only the ones found on the chain +// GetBorChainBlockInfoInBatch returns bor chain block headers, total difficulties, +// and authors for the inclusive range [start, end]. It dispatches to gRPC when +// BorChainGrpcFlag is set, otherwise falls back to the HTTP JSON-RPC batch. +// In both paths, it tries to get blocks from the range interval +// but returns only the ones found on the chain. func (c *ContractCaller) GetBorChainBlockInfoInBatch(ctx context.Context, start, end int64) ([]*ethTypes.Header, []uint64, []common.Address, error) { + if start < 0 || end < 0 || end < start { + return nil, nil, nil, fmt.Errorf("invalid range [%d,%d]", start, end) + } + // Prevents int64 (end-start+1) overflow + if end-start > borgrpc.MaxBlockInfoBatchSize-1 { + return nil, nil, nil, fmt.Errorf("range too large: %d blocks exceeds max %d", end-start+1, borgrpc.MaxBlockInfoBatchSize) + } + timeoutCtx, cancel := context.WithTimeout(ctx, c.BorChainTimeout) defer cancel() + if c.BorChainGrpcFlag { + grpcClient, err := c.getRequiredBorGRPCClient() + if err != nil { + return nil, nil, nil, err + } + return grpcClient.GetBlockInfoInBatch(timeoutCtx, start, end) + } + + return c.getBorChainBlockInfoInBatchHTTP(timeoutCtx, start, end) +} + +// tdResp is the JSON shape returned by eth_getTdByNumber. +type tdResp struct { + TotalDifficulty hexutil.Uint64 `json:"totalDifficulty"` +} + +// buildBorBatchElems constructs the flat BatchElem slice for a single BatchCallContext call. +func buildBorBatchElems(start, end int64, hdrOut []*ethTypes.Header, tdOut []*tdResp, authorOut []*common.Address) []rpc.BatchElem { totalBlocks := end - start + 1 - rpcClient := c.BorChainClient.Client() - batchElems := make([]rpc.BatchElem, 0, 2*(totalBlocks)) + // The 2* here is a capacity hint; mutating it doesn't break correctness because append grows the slice. + // mutator-disable-next-line slice-capacity hint only + elems := make([]rpc.BatchElem, 0, 2*totalBlocks) - // Header Batch - result := make([]*ethTypes.Header, totalBlocks) for i := start; i <= end; i++ { blockNumHex := fmt.Sprintf("0x%x", i) - - batchElems = append(batchElems, rpc.BatchElem{ - Method: "eth_getHeaderByNumber", - Args: []interface{}{blockNumHex}, - Result: &result[i-start], - }) + elems = append(elems, rpc.BatchElem{Method: "eth_getHeaderByNumber", Args: []interface{}{blockNumHex}, Result: &hdrOut[i-start]}) } - type tdResp struct { - TotalDifficulty hexutil.Uint64 `json:"totalDifficulty"` - } - - // TD Batch - resultTd := make([]*tdResp, totalBlocks) for i := start; i <= end; i++ { blockNumHex := fmt.Sprintf("0x%x", i) - - batchElems = append(batchElems, rpc.BatchElem{ - Method: "eth_getTdByNumber", - Args: []interface{}{blockNumHex}, - Result: &resultTd[i-start], - }) + elems = append(elems, rpc.BatchElem{Method: "eth_getTdByNumber", Args: []interface{}{blockNumHex}, Result: &tdOut[i-start]}) } - // Author Batch - resultAuthor := make([]*common.Address, totalBlocks) for i := start; i <= end; i++ { if i > 0 { // skip genesis block blockNumHex := fmt.Sprintf("0x%x", i) - batchElems = append(batchElems, rpc.BatchElem{ - Method: "bor_getAuthor", - Args: []interface{}{blockNumHex}, - Result: &resultAuthor[i-start], - }) + elems = append(elems, rpc.BatchElem{Method: "bor_getAuthor", Args: []interface{}{blockNumHex}, Result: &authorOut[i-start]}) } } - if err := rpcClient.BatchCallContext(timeoutCtx, batchElems); err != nil { - return nil, nil, nil, err + return elems +} + +// borAuthorFromBatch retrieves the author address for a non-genesis block from the flat +// batchElems slice. It returns the address and true on success, or the zero address and +// false when the batch entry indicates an error or a nil result. +func borAuthorFromBatch(i int, start, totalBlocks int64, batchElems []rpc.BatchElem, authors []*common.Address) (common.Address, bool) { + authorReqIndex := 2*int(totalBlocks) + i + if start == 0 { + // genesis block has no author entry in the batch, so all later indices shift left by 1. + authorReqIndex-- } + elem := batchElems[authorReqIndex] + if elem.Error != nil || authors[i] == nil { + return common.Address{}, false + } + return *authors[i], true +} - // Get results until capture an error (header not found) - tds := make([]uint64, 0, totalBlocks) +// collateBorBatchResults walks the flat BatchElem result slice and collects the contiguous +// prefix of successfully fetched blocks, stopping at the first error or missing result. +func collateBorBatchResults(start, totalBlocks int64, batchElems []rpc.BatchElem, hdrs []*ethTypes.Header, tds []*tdResp, authors []*common.Address) ([]*ethTypes.Header, []uint64, []common.Address) { headers := make([]*ethTypes.Header, 0, totalBlocks) - authors := make([]common.Address, 0, totalBlocks) + tdSlice := make([]uint64, 0, totalBlocks) + authorSlice := make([]common.Address, 0, totalBlocks) + for i := 0; i < int(totalBlocks); i++ { blockNum := start + int64(i) elemHeader := batchElems[i] elemTd := batchElems[i+int(totalBlocks)] - if elemHeader.Error != nil || elemTd.Error != nil || result[i] == nil || resultTd[i] == nil { - Logger.Debug("Error fetching block info", "error", elemHeader.Error, "error", elemTd.Error, "blockNum", blockNum) + if elemHeader.Error != nil || elemTd.Error != nil || hdrs[i] == nil || tds[i] == nil { + Logger.Debug("Error fetching block info", "headerErr", elemHeader.Error, "tdErr", elemTd.Error, "blockNum", blockNum) + break + } + // Verify the returned header's block number matches the requested slot, + // to avoid potential wrong hashes into downstream milestone propositions. + if hdrs[i].Number == nil || hdrs[i].Number.Uint64() != uint64(blockNum) { + Logger.Debug("bor batch returned header with mismatched block number", "want", blockNum, "got", hdrs[i].Number) break } var author common.Address if blockNum > 0 { - authorReqIndex := 2*int(totalBlocks) + i - if start == 0 { - authorReqIndex-- - } - elemAuthor := batchElems[authorReqIndex] - if elemAuthor.Error != nil || resultAuthor[i] == nil { - Logger.Debug("Error fetching block author", "error", elemAuthor.Error, "blockNum", blockNum) + var ok bool + author, ok = borAuthorFromBatch(i, start, totalBlocks, batchElems, authors) + if !ok { + // statement_deletion only drops a debug message, the break still stops the loop. + // mutator-disable-next-line operator-log line + Logger.Debug("Error fetching block author", "blockNum", blockNum) break } - author = *resultAuthor[i] } - headers = append(headers, result[i]) - tds = append(tds, uint64(resultTd[i].TotalDifficulty)) - authors = append(authors, author) + headers = append(headers, hdrs[i]) + tdSlice = append(tdSlice, uint64(tds[i].TotalDifficulty)) + authorSlice = append(authorSlice, author) } + return headers, tdSlice, authorSlice +} + +// getBorChainBlockInfoInBatchHTTP is the HTTP/JSON-RPC implementation of GetBorChainBlockInfoInBatch. +// It issues a single RPC batch call covering headers, total difficulties, and authors for the +// inclusive range [start, end], and returns only the contiguous prefix of blocks found on the chain. +func (c *ContractCaller) getBorChainBlockInfoInBatchHTTP(ctx context.Context, start, end int64) ([]*ethTypes.Header, []uint64, []common.Address, error) { + // Range arithmetic compensated by the per-index bounds inside buildBorBatchElems/collateBorBatchResults. + // mutator-disable-next-line range-arithmetic compensated downstream + totalBlocks := end - start + 1 + rpcClient := c.BorChainClient.Client() + + headerResults := make([]*ethTypes.Header, totalBlocks) + tdResults := make([]*tdResp, totalBlocks) + authorResults := make([]*common.Address, totalBlocks) + + batchElems := buildBorBatchElems(start, end, headerResults, tdResults, authorResults) + + // negate_conditional/branch_removal require a test that induces a transport-level batch failure mid-call. + // mutator-disable-next-line defensive BatchCallContext error guard + if err := rpcClient.BatchCallContext(ctx, batchElems); err != nil { + // implementation-detail: nil return on batch error; callers check err!=nil + // mutator-disable-next-line return-value on error propagation + return nil, nil, nil, err + } + + headers, tds, authors := collateBorBatchResults(start, totalBlocks, batchElems, headerResults, tdResults, authorResults) return headers, tds, authors, nil } @@ -650,12 +735,24 @@ func (c *ContractCaller) GetBorChainBlockTd(ctx context.Context, blockHash commo ctx, cancel := context.WithTimeout(ctx, c.BorChainTimeout) defer cancel() + if c.BorChainGrpcFlag { + grpcClient, err := c.getRequiredBorGRPCClient() + if err != nil { + return 0, err + } + return grpcClient.GetTdByHash(ctx, blockHash) + } + rpcClient := c.BorChainClient.Client() var resp map[string]interface{} if err := rpcClient.CallContext(ctx, &resp, "eth_getTdByHash", blockHash.Hex()); err != nil { return 0, err } + // Same path for gRPC and HTTP: a missing block surfaces as ethereum.NotFound + if resp == nil || resp["totalDifficulty"] == nil { + return 0, ethereum.NotFound + } raw, ok := resp["totalDifficulty"].(string) if !ok { @@ -671,21 +768,42 @@ func (c *ContractCaller) GetBorChainBlockTd(ctx context.Context, blockHash commo } // GetBorChainBlockAuthor returns the producer of the bor block -func (c *ContractCaller) GetBorChainBlockAuthor(blockNum *big.Int) (*common.Address, error) { - ctx, cancel := context.WithTimeout(context.Background(), c.BorChainTimeout) +func (c *ContractCaller) GetBorChainBlockAuthor(ctx context.Context, blockNum *big.Int) (*common.Address, error) { + ctx, cancel := context.WithTimeout(ctx, c.BorChainTimeout) defer cancel() + if c.BorChainGrpcFlag { + grpcClient, err := c.getRequiredBorGRPCClient() + if err != nil { + // the return on the next line is what matters; log deletion is observable only in ops. + // mutator-disable-next-line operator-log line + Logger.Error(errUnableToConnect, "error", err) + return nil, err + } + author, err := grpcClient.GetAuthor(ctx, blockNum) + if err != nil { + if !errors.Is(err, ethereum.NotFound) { + Logger.Error(errUnableToConnect, "error", err) + } + return nil, err + } + if author == nil { + return nil, ethereum.NotFound + } + return author, nil + } + var author *common.Address err := c.BorChainClient.Client().CallContext(ctx, &author, "bor_getAuthor", toBlockNumArg(blockNum)) if err != nil { - Logger.Error(errUnableToConnect, "error", err) + if !errors.Is(err, ethereum.NotFound) { + Logger.Error(errUnableToConnect, "error", err) + } return nil, err } - if author == nil { return nil, ethereum.NotFound } - return author, nil } @@ -1102,7 +1220,7 @@ func (c *ContractCaller) GetBorTxReceipt(txHash common.Hash) (*ethTypes.Receipt, return c.getTxReceipt(ctx, c.BorChainClient, nil, txHash) } -func (c *ContractCaller) getTxReceipt(ctx context.Context, client *ethclient.Client, grpcClient *grpc.BorGRPCClient, txHash common.Hash) (*ethTypes.Receipt, error) { +func (c *ContractCaller) getTxReceipt(ctx context.Context, client *ethclient.Client, grpcClient BorGRPCClienter, txHash common.Hash) (*ethTypes.Receipt, error) { if grpcClient != nil { return grpcClient.TransactionReceipt(ctx, txHash) } @@ -1131,7 +1249,7 @@ func (c *ContractCaller) GetCheckpointSign(txHash common.Hash) ([]byte, []byte, } // getRequiredBorGRPCClient returns the bor grpc client or an error -func (c *ContractCaller) getRequiredBorGRPCClient() (*grpc.BorGRPCClient, error) { +func (c *ContractCaller) getRequiredBorGRPCClient() (BorGRPCClienter, error) { if c.BorChainGrpcClient == nil { return nil, errors.New("bor grpc client is nil while bor grpc flag is enabled") } diff --git a/helper/call_batch_test.go b/helper/call_batch_test.go new file mode 100644 index 000000000..b7eeb1ad2 --- /dev/null +++ b/helper/call_batch_test.go @@ -0,0 +1,318 @@ +package helper + +import ( + "errors" + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/require" +) + +// TestBuildBorBatchElems_Layout verifies that the exact layout of the returned slice is +// +// [totalBlocks header elems] ++ [totalBlocks TD elems] ++ [non-genesis author elems] +func TestBuildBorBatchElems_Layout(t *testing.T) { + t.Parallel() + + t.Run("range_1_to_3_layout", func(t *testing.T) { + t.Parallel() + + start, end := int64(1), int64(3) + totalBlocks := end - start + 1 // 3 + hdrs := make([]*ethTypes.Header, totalBlocks) + tds := make([]*tdResp, totalBlocks) + authors := make([]*common.Address, totalBlocks) + + elems := buildBorBatchElems(start, end, hdrs, tds, authors) + + // 3 headers + 3 TD + 3 authors (blocks 1,2,3 — all non-genesis) + require.Len(t, elems, int(3*totalBlocks)) + + // Header section: indices 0..2 + for i := int64(0); i < totalBlocks; i++ { + blockNum := start + i + expectedHex := fmt.Sprintf("0x%x", blockNum) + require.Equal(t, "eth_getHeaderByNumber", elems[i].Method) + require.Equal(t, expectedHex, elems[i].Args[0]) + } + + // TD section: indices 3..5 + for i := int64(0); i < totalBlocks; i++ { + blockNum := start + i + expectedHex := fmt.Sprintf("0x%x", blockNum) + idx := int(totalBlocks) + int(i) + require.Equal(t, "eth_getTdByNumber", elems[idx].Method) + require.Equal(t, expectedHex, elems[idx].Args[0]) + } + + // Author section: indices 6..8 (all 3 blocks are non-genesis) + for i := int64(0); i < totalBlocks; i++ { + blockNum := start + i + expectedHex := fmt.Sprintf("0x%x", blockNum) + idx := 2*int(totalBlocks) + int(i) + require.Equal(t, "bor_getAuthor", elems[idx].Method) + require.Equal(t, expectedHex, elems[idx].Args[0]) + } + }) + + t.Run("range_starting_at_genesis_skips_genesis_author", func(t *testing.T) { + t.Parallel() + + // start=0, end=2 → blocks 0,1,2 → 3 headers, 3 TDs, but only 2 authors (blocks 1,2) + start, end := int64(0), int64(2) + totalBlocks := end - start + 1 // 3 + hdrs := make([]*ethTypes.Header, totalBlocks) + tds := make([]*tdResp, totalBlocks) + authors := make([]*common.Address, totalBlocks) + + elems := buildBorBatchElems(start, end, hdrs, tds, authors) + + // 3 headers + 3 TD + 2 authors (genesis block 0 is skipped) + require.Len(t, elems, int(2*totalBlocks+2)) + + // Verify genesis (block 0) is not in author section + authorSection := elems[2*int(totalBlocks):] + require.Len(t, authorSection, 2) + require.Equal(t, "0x1", authorSection[0].Args[0], "first author elem must be block 1, not genesis") + require.Equal(t, "0x2", authorSection[1].Args[0]) + }) + + t.Run("single_block_non_genesis", func(t *testing.T) { + t.Parallel() + + start, end := int64(5), int64(5) + totalBlocks := int64(1) + hdrs := make([]*ethTypes.Header, totalBlocks) + tds := make([]*tdResp, totalBlocks) + authors := make([]*common.Address, totalBlocks) + + elems := buildBorBatchElems(start, end, hdrs, tds, authors) + + // 1 header + 1 TD + 1 author + require.Len(t, elems, 3) + require.Equal(t, "eth_getHeaderByNumber", elems[0].Method) + require.Equal(t, "eth_getTdByNumber", elems[1].Method) + require.Equal(t, "bor_getAuthor", elems[2].Method) + require.Equal(t, "0x5", elems[0].Args[0]) + }) + + t.Run("result_pointers_point_into_output_slices", func(t *testing.T) { + t.Parallel() + + start, end := int64(10), int64(11) + totalBlocks := end - start + 1 + hdrs := make([]*ethTypes.Header, totalBlocks) + tds := make([]*tdResp, totalBlocks) + authors := make([]*common.Address, totalBlocks) + + elems := buildBorBatchElems(start, end, hdrs, tds, authors) + + // Verify that result pointers actually point into the slices by writing + // through them and checking the output slices. + hdr10 := ðTypes.Header{Number: big.NewInt(10)} + td10 := &tdResp{TotalDifficulty: hexutil.Uint64(42)} + addr10 := common.HexToAddress("0x1111") + + *elems[0].Result.(**ethTypes.Header) = hdr10 + *elems[int(totalBlocks)].Result.(**tdResp) = td10 + *elems[2*int(totalBlocks)].Result.(**common.Address) = &addr10 + + require.Equal(t, hdr10, hdrs[0]) + require.Equal(t, td10, tds[0]) + require.Equal(t, &addr10, authors[0]) + }) +} + +// TestBorAuthorFromBatch verifies index arithmetic and error handling. +func TestBorAuthorFromBatch(t *testing.T) { + t.Parallel() + + t.Run("non_genesis_start_happy_path", func(t *testing.T) { + t.Parallel() + + // start=1, totalBlocks=3, i=0 → authorReqIndex = 2*3 + 0 = 6 + // (start != 0 → no decrement) + want := common.HexToAddress("0xdeadbeef") + authors := []*common.Address{&want, nil, nil} + + // Build a batchElems slice large enough: indices 0..5 are headers/TDs, + // index 6 is the first author. + batchElems := make([]rpc.BatchElem, 7) + // batchElems[6].Error is nil by default + + addr, ok := borAuthorFromBatch(0, 1, 3, batchElems, authors) + require.True(t, ok) + require.Equal(t, want, addr) + }) + + t.Run("genesis_start_index_shifted", func(t *testing.T) { + t.Parallel() + + // start=0, totalBlocks=3, i=1 (block 1) + // Correct: authorReqIndex = 2*3 + 1 - 1 = 6 + want := common.HexToAddress("0xabcdef") + authors := []*common.Address{nil, &want, nil} + + batchElems := make([]rpc.BatchElem, 7) + // Inject errors at wrong indices to distinguish mutants: + batchElems[0].Error = errors.New("wrong index 0") + batchElems[4].Error = errors.New("wrong index 4") + + addr, ok := borAuthorFromBatch(1, 0, 3, batchElems, authors) + require.True(t, ok, "correct index 6 has no error, should return the author") + require.Equal(t, want, addr) + }) + + t.Run("elem_error_returns_false", func(t *testing.T) { + t.Parallel() + + authors := []*common.Address{new(common.HexToAddress("0x1111"))} + batchElems := make([]rpc.BatchElem, 3) + batchElems[2].Error = errors.New("rpc error") // index = 2*1 + 0 = 2 + + addr, ok := borAuthorFromBatch(0, 1, 1, batchElems, authors) + require.False(t, ok) + require.Equal(t, common.Address{}, addr) + }) + + t.Run("nil_author_returns_false", func(t *testing.T) { + t.Parallel() + + // authors[0] == nil → return false even if no batch error + authors := []*common.Address{nil} + batchElems := make([]rpc.BatchElem, 3) // index 2, no error + + addr, ok := borAuthorFromBatch(0, 1, 1, batchElems, authors) + require.False(t, ok) + require.Equal(t, common.Address{}, addr) + }) +} + +// TestCollateBorBatchResults verifies the "stop at first error" semantics and +// the genesis-block author bypass. +func TestCollateBorBatchResults(t *testing.T) { + t.Parallel() + + // makeHdr returns a minimal non-nil header with the given block number. + makeHdr := func(n int64) *ethTypes.Header { + return ðTypes.Header{Number: big.NewInt(n), Difficulty: big.NewInt(1)} + } + makeTD := func(v uint64) *tdResp { return &tdResp{TotalDifficulty: hexutil.Uint64(v)} } + addr := func(s string) *common.Address { return new(common.HexToAddress(s)) } + + t.Run("all_blocks_good_non_genesis", func(t *testing.T) { + t.Parallel() + + start, totalBlocks := int64(1), int64(2) // blocks 1 and 2 + hdrs := []*ethTypes.Header{makeHdr(1), makeHdr(2)} + tds := []*tdResp{makeTD(100), makeTD(200)} + authors := []*common.Address{addr("0x1111"), addr("0x2222")} + + // batchElems layout: [hdr0, hdr1, td0, td1, author0, author1] + batchElems := buildBorBatchElems(start, start+totalBlocks-1, hdrs, tds, authors) + + headers, tdSlice, authorSlice := collateBorBatchResults(start, totalBlocks, batchElems, hdrs, tds, authors) + + require.Len(t, headers, 2) + require.Len(t, tdSlice, 2) + require.Len(t, authorSlice, 2) + require.Equal(t, big.NewInt(1), headers[0].Number) + require.Equal(t, big.NewInt(2), headers[1].Number) + require.Equal(t, uint64(100), tdSlice[0]) + require.Equal(t, uint64(200), tdSlice[1]) + require.Equal(t, common.HexToAddress("0x1111"), authorSlice[0]) + require.Equal(t, common.HexToAddress("0x2222"), authorSlice[1]) + }) + + t.Run("header_error_stops_at_first_bad_block", func(t *testing.T) { + t.Parallel() + + start, totalBlocks := int64(1), int64(3) // blocks 1, 2, 3 + hdrs := []*ethTypes.Header{makeHdr(1), makeHdr(2), makeHdr(3)} + tds := []*tdResp{makeTD(10), makeTD(20), makeTD(30)} + authors := []*common.Address{addr("0x1"), addr("0x2"), addr("0x3")} + + batchElems := buildBorBatchElems(start, start+totalBlocks-1, hdrs, tds, authors) + // Inject an error into block 2's header elem (index 1) + batchElems[1].Error = errors.New("server error") + + headers, tdSlice, authorSlice := collateBorBatchResults(start, totalBlocks, batchElems, hdrs, tds, authors) + + // Must stop at block 2, returning only block 1 + require.Len(t, headers, 1) + require.Len(t, tdSlice, 1) + require.Len(t, authorSlice, 1) + require.Equal(t, big.NewInt(1), headers[0].Number) + }) + + t.Run("nil_header_stops_early", func(t *testing.T) { + t.Parallel() + + start, totalBlocks := int64(5), int64(2) + hdrs := []*ethTypes.Header{makeHdr(5), nil} // second block has nil header + tds := []*tdResp{makeTD(500), makeTD(600)} + authors := []*common.Address{addr("0x5"), addr("0x6")} + + batchElems := buildBorBatchElems(start, start+totalBlocks-1, hdrs, tds, authors) + + headers, tdSlice, authorSlice := collateBorBatchResults(start, totalBlocks, batchElems, hdrs, tds, authors) + + require.Len(t, headers, 1, "nil header in position 1 must stop iteration") + require.Len(t, tdSlice, 1) + require.Len(t, authorSlice, 1) + }) + + t.Run("genesis_block_gets_zero_author", func(t *testing.T) { + t.Parallel() + + // start=0, block 0 is genesis: blockNum=0 → author bypassed, zero address used. + start, totalBlocks := int64(0), int64(1) + hdrs := []*ethTypes.Header{makeHdr(0)} + tds := []*tdResp{makeTD(0)} + authors := []*common.Address{nil} // genesis has no author + + batchElems := buildBorBatchElems(start, start+totalBlocks-1, hdrs, tds, authors) + + headers, tdSlice, authorSlice := collateBorBatchResults(start, totalBlocks, batchElems, hdrs, tds, authors) + + require.Len(t, headers, 1) + require.Len(t, tdSlice, 1) + require.Len(t, authorSlice, 1) + // Genesis block author is the zero address (not fetched from batch) + require.Equal(t, common.Address{}, authorSlice[0]) + }) + + t.Run("author_error_stops_iteration", func(t *testing.T) { + t.Parallel() + + start, totalBlocks := int64(1), int64(2) // blocks 1, 2 + hdrs := []*ethTypes.Header{makeHdr(1), makeHdr(2)} + tds := []*tdResp{makeTD(10), makeTD(20)} + authors := []*common.Address{addr("0x1"), nil} // block 2 has nil author → borAuthorFromBatch returns false + + batchElems := buildBorBatchElems(start, start+totalBlocks-1, hdrs, tds, authors) + + headers, tdSlice, authorSlice := collateBorBatchResults(start, totalBlocks, batchElems, hdrs, tds, authors) + + // Block 1 is good; block 2's nil author causes break. + require.Len(t, headers, 1) + require.Len(t, tdSlice, 1) + require.Len(t, authorSlice, 1) + require.Equal(t, big.NewInt(1), headers[0].Number) + }) + + t.Run("empty_range_returns_empty_slices", func(t *testing.T) { + t.Parallel() + + // totalBlocks=0 means the loop doesn't execute + headers, tdSlice, authorSlice := collateBorBatchResults(1, 0, nil, nil, nil, nil) + require.Empty(t, headers) + require.Empty(t, tdSlice) + require.Empty(t, authorSlice) + }) +} diff --git a/helper/call_dispatcher_test.go b/helper/call_dispatcher_test.go new file mode 100644 index 000000000..d34a36eca --- /dev/null +++ b/helper/call_dispatcher_test.go @@ -0,0 +1,89 @@ +package helper + +import ( + "context" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// makeDispatcherCaller returns a ContractCaller with the given gRPC flag and a +// nil gRPC client. The BorChainTimeout is set to a short value, so HTTP-path +// tests time out quickly without blocking the test suite. +func makeDispatcherCaller(grpcFlag bool) ContractCaller { + return ContractCaller{ + BorChainGrpcFlag: grpcFlag, + BorChainGrpcClient: nil, // nil client triggers getRequiredBorGRPCClient error + BorChainTimeout: 50 * time.Millisecond, + } +} + +// TestGetBorChainBlockInfoInBatch_GRPCNilClientError verifies that when +// BorChainGrpcFlag=true and BorChainGrpcClient=nil, the function returns an +// error rather than panicking or silently returning empty results. +func TestGetBorChainBlockInfoInBatch_GRPCNilClientError(t *testing.T) { + t.Parallel() + + c := makeDispatcherCaller(true) + headers, tds, authors, err := c.GetBorChainBlockInfoInBatch(context.Background(), 1, 5) + + require.Error(t, err, "nil gRPC client must return an error, not succeed") + require.Nil(t, headers) + require.Nil(t, tds) + require.Nil(t, authors) +} + +// TestGetBorChainBlockTd_GRPCNilClientError verifies the same for GetBorChainBlockTd. +func TestGetBorChainBlockTd_GRPCNilClientError(t *testing.T) { + t.Parallel() + + c := makeDispatcherCaller(true) + td, err := c.GetBorChainBlockTd(context.Background(), common.Hash{}) + + require.Error(t, err, "nil gRPC client must return an error") + require.Equal(t, uint64(0), td) +} + +// TestGetBorChainBlockAuthor_GRPCNilClientError verifies the same for GetBorChainBlockAuthor. +func TestGetBorChainBlockAuthor_GRPCNilClientError(t *testing.T) { + t.Parallel() + + c := makeDispatcherCaller(true) + author, err := c.GetBorChainBlockAuthor(context.Background(), nil) + + require.Error(t, err, "nil gRPC client must return an error") + require.Nil(t, author) +} + +// TestGetBorChainBlockInfoInBatch_NonGRPCCancelledContext verifies that +// BorChainGrpcFlag=false falls through to the HTTP path. +func TestGetBorChainBlockInfoInBatch_NonGRPCCancelledContext(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // pre-cancel so the HTTP call fails immediately + + c := makeDispatcherCaller(false) + // BorChainClient is nil; calling getBorChainBlockInfoInBatchHTTP will + // panic or error. We just need the function to NOT return the nil-client + var ( + panicVal interface{} + err error + ) + func() { + defer func() { panicVal = recover() }() + _, _, _, err = c.GetBorChainBlockInfoInBatch(ctx, 1, 5) + }() + + // Either a panic (from nil client dereference) or a context error is fine — + // what matters is that the gRPC nil-client error was not returned, which + // means the gRPC branch was correctly skipped. + if panicVal == nil { + // If no panic, should be a network or context error, not a "gRPC client is nil" error + require.Error(t, err) + require.NotContains(t, err.Error(), "bor gRPC client is nil") + } + // If panicVal != nil, the nil BorChainClient was dereferenced — correct branch was taken. +} diff --git a/helper/call_grpc_author_test.go b/helper/call_grpc_author_test.go new file mode 100644 index 000000000..44d5f5fac --- /dev/null +++ b/helper/call_grpc_author_test.go @@ -0,0 +1,116 @@ +package helper + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +// fakeBorGRPCClient is an in-memory BorGRPCClienter that returns whatever the +// test configured. Only the methods the tests exercise are implemented with +// real behavior +type fakeBorGRPCClient struct { + authorAddr *common.Address + authorErr error + calls int +} + +func (f *fakeBorGRPCClient) GetAuthor(_ context.Context, _ *big.Int) (*common.Address, error) { + f.calls++ + return f.authorAddr, f.authorErr +} + +func (f *fakeBorGRPCClient) HeaderByNumber(_ context.Context, _ int64) (*ethTypes.Header, error) { + panic("fakeBorGRPCClient.HeaderByNumber: unexpected call") +} +func (f *fakeBorGRPCClient) BlockByNumber(_ context.Context, _ int64) (*ethTypes.Block, error) { + panic("fakeBorGRPCClient.BlockByNumber: unexpected call") +} +func (f *fakeBorGRPCClient) GetRootHash(_ context.Context, _ uint64, _ uint64) (string, error) { + panic("fakeBorGRPCClient.GetRootHash: unexpected call") +} +func (f *fakeBorGRPCClient) GetVoteOnHash(_ context.Context, _ uint64, _ uint64, _ string, _ string) (bool, error) { + panic("fakeBorGRPCClient.GetVoteOnHash: unexpected call") +} +func (f *fakeBorGRPCClient) GetTdByHash(_ context.Context, _ common.Hash) (uint64, error) { + panic("fakeBorGRPCClient.GetTdByHash: unexpected call") +} +func (f *fakeBorGRPCClient) GetTdByNumber(_ context.Context, _ *big.Int) (uint64, error) { + panic("fakeBorGRPCClient.GetTdByNumber: unexpected call") +} +func (f *fakeBorGRPCClient) GetBlockInfoInBatch(_ context.Context, _, _ int64) ([]*ethTypes.Header, []uint64, []common.Address, error) { + panic("fakeBorGRPCClient.GetBlockInfoInBatch: unexpected call") +} +func (f *fakeBorGRPCClient) TransactionReceipt(_ context.Context, _ common.Hash) (*ethTypes.Receipt, error) { + panic("fakeBorGRPCClient.TransactionReceipt: unexpected call") +} +func (f *fakeBorGRPCClient) BorBlockReceipt(_ context.Context, _ common.Hash) (*ethTypes.Receipt, error) { + panic("fakeBorGRPCClient.BorBlockReceipt: unexpected call") +} + +// TestGetBorChainBlockAuthor_GRPC_HappyPath ensures that when the gRPC client +// returns a non-nil address and no error, the caller returns that exact +// pointer (kills the happy-path return_value mutant that would replace it with +// nil) and the err is nil. +func TestGetBorChainBlockAuthor_GRPC_HappyPath(t *testing.T) { + t.Parallel() + + want := common.HexToAddress("0x000000000000000000000000000000000000abcd") + fake := &fakeBorGRPCClient{authorAddr: &want, authorErr: nil} + c := ContractCaller{ + BorChainGrpcFlag: true, + BorChainGrpcClient: fake, + BorChainTimeout: time.Second, + } + + got, err := c.GetBorChainBlockAuthor(context.Background(), big.NewInt(42)) + require.NoError(t, err) + require.NotNil(t, got) + require.Equal(t, want, *got, "returned pointer must reference the fake's configured address") + require.Equal(t, 1, fake.calls) +} + +// TestGetBorChainBlockAuthor_GRPC_ErrorPropagates ensures that when the gRPC +// client returns a non-nil error, the caller returns that exact error (kills +// the `if err != nil` branch_removal / negate_conditional and the +// `return nil, err` return_value→zero mutants). +func TestGetBorChainBlockAuthor_GRPC_ErrorPropagates(t *testing.T) { + t.Parallel() + + sentinel := errors.New("grpc GetAuthor transport failure") + fake := &fakeBorGRPCClient{authorAddr: nil, authorErr: sentinel} + c := ContractCaller{ + BorChainGrpcFlag: true, + BorChainGrpcClient: fake, + BorChainTimeout: time.Second, + } + + got, err := c.GetBorChainBlockAuthor(context.Background(), big.NewInt(42)) + require.Nil(t, got) + require.ErrorIs(t, err, sentinel, "error from gRPC GetAuthor must propagate unchanged") +} + +// TestGetBorChainBlockAuthor_GRPC_NilAuthorIsNotFound ensures that when the +// gRPC client returns (nil, nil), and the caller maps it to ethereum.NotFound +// (kills the `if author == nil` branch_removal / negate_conditional). +func TestGetBorChainBlockAuthor_GRPC_NilAuthorIsNotFound(t *testing.T) { + t.Parallel() + + fake := &fakeBorGRPCClient{authorAddr: nil, authorErr: nil} + c := ContractCaller{ + BorChainGrpcFlag: true, + BorChainGrpcClient: fake, + BorChainTimeout: time.Second, + } + + got, err := c.GetBorChainBlockAuthor(context.Background(), big.NewInt(42)) + require.Nil(t, got) + require.ErrorIs(t, err, ethereum.NotFound, "nil author must map to ethereum.NotFound sentinel") +} diff --git a/helper/config.go b/helper/config.go index acc71ae72..8897a58c0 100644 --- a/helper/config.go +++ b/helper/config.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "log" + "math/big" "os" "path/filepath" "strconv" @@ -19,6 +20,7 @@ import ( addressCodec "github.com/cosmos/cosmos-sdk/codec/address" serverconfig "github.com/cosmos/cosmos-sdk/server/config" sdk "github.com/cosmos/cosmos-sdk/types" + ethTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" "github.com/rs/zerolog" @@ -45,10 +47,11 @@ const ( // app config flags - MainRPCUrlFlag = "eth_rpc_url" - BorRPCUrlFlag = "bor_rpc_url" - BorGRPCUrlFlag = "bor_grpc_url" - BorGRPCFlagFlag = "bor_grpc_flag" + MainRPCUrlFlag = "eth_rpc_url" + BorRPCUrlFlag = "bor_rpc_url" + BorGRPCUrlFlag = "bor_grpc_url" + BorGRPCFlagFlag = "bor_grpc_flag" + BorGRPCTokenFlag = "bor_grpc_token" // #nosec G101 -- config key name, not a credential value CometBFTNodeURLFlag = "comet_bft_rpc_url" HeimdallServerURLFlag = "heimdall_rest_server" @@ -136,6 +139,20 @@ const ( privValJsonFile = "priv_validator_key.json" bindPFlagLog = "%v | BindPFlag | %v" + + borGRPCParityRetryInterval = 5 * time.Second + borGRPCParityMaxAttempts = 60 // ~5 min total + + // borGRPCParityDepth is how many blocks behind the latest we sample for the + // parity check. Bor reorgs deeper than this are exceptional and would warrant alerting. + borGRPCParityDepth = int64(32) + + // borGRPCParityMismatchStreak is the number of consecutive confirmed + // mismatches required before we log.Fatal. A single mismatch can be a + // transient race (canonical block at target height changed between the + // HTTP and gRPC reads). Requiring N-in-a-row at the same height with + // stable HTTP re-reads virtually rules that out. + borGRPCParityMismatchStreak = 3 ) func init() { @@ -144,10 +161,11 @@ func init() { // CustomConfig represents heimdall config type CustomConfig struct { - EthRPCUrl string `mapstructure:"eth_rpc_url"` // RPC endpoint for main chain + EthRPCUrl string `mapstructure:"eth_rpc_url"` // RPC endpoint for the main chain BorRPCUrl string `mapstructure:"bor_rpc_url"` // RPC endpoint for bor chain BorGRPCFlag bool `mapstructure:"bor_grpc_flag"` // gRPC flag for bor chain BorGRPCUrl string `mapstructure:"bor_grpc_url"` // gRPC endpoint for bor chain + BorGRPCToken string `mapstructure:"bor_grpc_token"` // bearer token for bor gRPC; empty = no auth CometBFTRPCUrl string `mapstructure:"comet_bft_rpc_url"` // cometBft node url SubGraphUrl string `mapstructure:"sub_graph_url"` // sub graph url @@ -204,7 +222,7 @@ type CustomAppConfig struct { var conf CustomAppConfig -// MainChainClient stores eth client for mainChain +// MainChainClient stores the eth client for mainChain var ( mainChainClient *ethclient.Client mainRPCClient *rpc.Client @@ -437,12 +455,15 @@ func InitHeimdallConfigWith(homeDir string, heimdallConfigFileFromFlag string) { warnIfBorRPCInaccessible(borClient, conf.Custom.BorRPCTimeout, conf.Custom.BorRPCUrl) if conf.Custom.BorGRPCFlag && conf.Custom.BorGRPCUrl != "" { - client, err := borgrpc.NewBorGRPCClient(conf.Custom.BorGRPCUrl, Logger) + client, err := borgrpc.NewBorGRPCClient(conf.Custom.BorGRPCUrl, conf.Custom.BorGRPCToken, Logger) if err != nil { log.Fatal("unable to create bor gRPC client", "URL", conf.Custom.BorGRPCUrl, "error", err) } borGRPCClient = client warnIfBorGRPCInaccessible(borGRPCClient, conf.Custom.BorRPCTimeout, conf.Custom.BorGRPCUrl) + // Fire-and-forget parity goroutine; removal is only observable in production init. + // mutator-disable-next-line statement-deletion in production init + verifyBorGRPCHashParity(borClient, borGRPCClient, conf.Custom.BorRPCTimeout) } else if conf.Custom.BorGRPCFlag && conf.Custom.BorGRPCUrl == "" { log.Fatal("bor gRPC is enabled but bor_grpc_url is empty") } @@ -552,6 +573,193 @@ func warnIfBorGRPCInaccessible(client *borgrpc.BorGRPCClient, timeout time.Durat } } +// verifyBorGRPCHashParity launches a background goroutine that periodically +// asserts both transports return the same ethTypes.Header.Hash() for the same +// bor block. A mismatch typically means the operator is running a new heimdall +// with BorGRPCFlag=true against an old bor that doesn't populate the full proto +// Header — which would silently corrupt milestone propositions on this node. +// The goroutine retries until one of: +// - Both transports return a header, and the hashes match (log.Info, exit goroutine) +// - Both return a header, and hashes differ (log.Fatal, halt the node) +// - Retry budget is exhausted (log Error, exit goroutine) +func verifyBorGRPCHashParity(httpClient *ethclient.Client, grpcClient *borgrpc.BorGRPCClient, timeout time.Duration) { + // Negate_conditional requires injecting typed-nil stubs mixed with non-nil, which the production path never does. + // mutator-disable-next-line defensive nil-client guard + if httpClient == nil || grpcClient == nil { + return + } + go runBorGRPCHashParityCheck(httpClient, grpcClient, timeout) +} + +// runBorGRPCHashParityCheck launches the hash parity check +// mutator-disable-func thin production-init wiring around runBorGRPCHashParityCheckWith +// the full retry+fatal logic is tested via runBorGRPCHashParityCheckWith directly +func runBorGRPCHashParityCheck(httpClient *ethclient.Client, grpcClient *borgrpc.BorGRPCClient, timeout time.Duration) { + runBorGRPCHashParityCheckWith( + httpClient, grpcClient, timeout, + borGRPCParityMaxAttempts, borGRPCParityRetryInterval, + func(msg string, keysAndValues ...interface{}) { + Logger.Error(msg, keysAndValues...) + os.Exit(1) + }, + ) +} + +// runBorGRPCHashParityCheckWith is the core of runBorGRPCHashParityCheck. +// It accepts the parity interfaces plus injectable max-attempts, retry-interval, and +// fatalFunc so unit tests can exercise the full loop without network access or process exit. +func runBorGRPCHashParityCheckWith( + httpClient parityHTTPFetcher, + grpcClient parityGRPCFetcher, + timeout time.Duration, + maxAttempts int, + retryInterval time.Duration, + fatalFunc func(msg string, keysAndValues ...interface{}), +) { + mismatches := 0 + for attempt := 1; attempt <= maxAttempts; attempt++ { + ok, mismatch := checkBorGRPCHashParityOnceWith(httpClient, grpcClient, timeout) + if ok { + return + } + next, fatal := updateParityMismatchStreak(mismatches, mismatch, borGRPCParityMismatchStreak) + if fatal { + fatalFunc("FATAL: bor gRPC hash mismatch with HTTP confirmed across "+ + "multiple consecutive checks. The operator is likely running a new heimdall "+ + "with BorGRPCFlag=true against a bor that doesn't populate the full proto Header. "+ + "Continuing would corrupt milestone propositions on this node. "+ + "Either upgrade bor to a matching version or disable BorGRPCFlag.", + "consecutiveMismatches", next, + ) + // Return so control flow does not depend on fatalFunc exiting the + // process. + return + } + mismatches = next + // mutator-disable-next-line retry pacing + if attempt < maxAttempts { + time.Sleep(retryInterval) + } + } + // Statement_deletion only drops an advisory message; no logic change. + // mutator-disable-next-line operator-log line + Logger.Error("Bor gRPC hash parity check gave up after retries — could not confirm transport equivalence. "+ + "If BorGRPCFlag=true, verify that bor is running a version that populates the full proto Header. "+ + "Continuing without parity confirmation.", + "attempts", maxAttempts, + "interval", retryInterval, + ) +} + +// updateParityMismatchStreak evolves the consecutive-mismatch counter given +// the outcome of one parity check. Returns the new value and whether +// the caller should log.Fatal (reached the configured threshold). +func updateParityMismatchStreak(current int, mismatch bool, streakLimit int) (next int, fatal bool) { + if !mismatch { + // Transient / unavailable — reset the streak so a flaky network + // window can't ladder into false fatal later. + return 0, false + } + next = current + 1 + return next, next >= streakLimit +} + +// checkBorGRPCHashParityOnceWith runs a single parity comparison at a block a +// few confirmations behind the current head (to avoid head-churn / reorg +// races). +// Returns (ok, mismatch): +// - Ok=true: both transports returned the same hash for the same block → check passed, caller exits +// - Ok=false, mismatch=false: one transport was unavailable, a reorg was detected during the check, +// or the chain is too young for the target depth → retry later, do not count toward mismatch streak +// - Ok=false, mismatch=true: both transports returned headers but with different hashes → count toward +// mismatch streak; the caller logs fatal only after borGRPCParityMismatchStreak consecutive mismatches +func checkBorGRPCHashParityOnceWith(httpClient parityHTTPFetcher, grpcClient parityGRPCFetcher, timeout time.Duration) (ok, mismatch bool) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + targetNum, okDepth := resolveParityTargetHeight(ctx, httpClient) + if !okDepth { + return false, false + } + httpHeader, grpcHeader, stable := fetchStableHeadersAtHeight(ctx, httpClient, grpcClient, targetNum) + if !stable { + return false, false + } + + if grpcHeader.Hash() != httpHeader.Hash() { + // Statement_deletion drops an advisory log; the (false, true) return below still signals a mismatch. + // mutator-disable-next-line operator-log line + Logger.Warn("Bor gRPC hash mismatch with HTTP for the same block — counting toward mismatch streak before fatal", + "block", httpHeader.Number.String(), + "httpHash", httpHeader.Hash().Hex(), + "grpcHash", grpcHeader.Hash().Hex(), + ) + // Streak bookkeeping is covered directly via updateParityMismatchStreak tests. + // mutator-disable-next-line boolean_substitution on mismatch signal + return false, true + } + + // Statement_deletion drops a success message; no branch logic affected. + // mutator-disable-next-line operator-log line + Logger.Info("Bor gRPC hash parity check passed", + "block", httpHeader.Number.String(), + "hash", httpHeader.Hash().Hex(), + ) + // Ok-signal flip is observable only via runBorGRPCHashParityCheckWith's caller-side early-return, which is tested directly. + // mutator-disable-next-line boolean_substitution on ok signal + return true, false +} + +// parityHTTPFetcher is the subset of *ethclient.Client used by the parity check. +// Defined as an interface so unit tests can inject stubs without dialing a real +// Bor HTTP endpoint. *ethclient.Client satisfies this interface. +type parityHTTPFetcher interface { + HeaderByNumber(ctx context.Context, number *big.Int) (*ethTypes.Header, error) +} + +// parityGRPCFetcher is the subset of *borgrpc.BorGRPCClient used by the parity +// check. Defined as an interface so unit tests can inject stubs without dialing +// a real gRPC endpoint. *borgrpc.BorGRPCClient satisfies this interface. +type parityGRPCFetcher interface { + HeaderByNumber(ctx context.Context, blockID int64) (*ethTypes.Header, error) +} + +// resolveParityTargetHeight returns (targetNum, ok). ok=false when the chain +// is too young, the HTTP head lookup failed, or the result is otherwise +// unusable — in any of those cases the parity check should retry later. +func resolveParityTargetHeight(ctx context.Context, httpClient parityHTTPFetcher) (int64, bool) { + latest, err := httpClient.HeaderByNumber(ctx, nil) + if err != nil || latest == nil { + return 0, false + } + latestNum := latest.Number.Int64() + if latestNum < borGRPCParityDepth { + return 0, false + } + return latestNum - borGRPCParityDepth, true +} + +// fetchStableHeadersAtHeight pulls the block at targetNum via HTTP, then via +// gRPC, then via HTTP again, and only returns (httpHeader, grpcHeader, true) +// when both HTTP reads agree (ruling out a reorg mid-check). Any transport +// error or reorg returns stable=false, so the caller defers the decision. +func fetchStableHeadersAtHeight(ctx context.Context, httpClient parityHTTPFetcher, grpcClient parityGRPCFetcher, targetNum int64) (httpHeader, grpcHeader *ethTypes.Header, stable bool) { + target := big.NewInt(targetNum) + httpHeader, err := httpClient.HeaderByNumber(ctx, target) + if err != nil || httpHeader == nil { + return nil, nil, false + } + grpcHeader, err = grpcClient.HeaderByNumber(ctx, targetNum) + if err != nil || grpcHeader == nil { + return nil, nil, false + } + httpHeader2, err := httpClient.HeaderByNumber(ctx, target) + if err != nil || httpHeader2 == nil || httpHeader2.Hash() != httpHeader.Hash() { + return nil, nil, false + } + return httpHeader, grpcHeader, true +} + // GetDefaultHeimdallConfig returns configuration with default params func GetDefaultHeimdallConfig() CustomConfig { return CustomConfig{ @@ -626,12 +834,12 @@ func GetBorRPCClient() *rpc.Client { return borRPCClient } -// GetPrivKey returns priv key object +// GetPrivKey returns the priv key object func GetPrivKey() secp256k1.PrivKey { return privKeyObject } -// GetPubKey returns pub key object +// GetPubKey returns the pub key object func GetPubKey() secp256k1.PubKey { return pubKeyObject } @@ -815,6 +1023,20 @@ func DecorateWithHeimdallFlags(cmd *cobra.Command, v *viper.Viper, loggerInstanc loggerInstance.Error(fmt.Sprintf(bindPFlagLog, caller, BorGRPCFlagFlag), "Error", err) } + // add BorGRPCTokenFlag flag + cmd.PersistentFlags().String( + BorGRPCTokenFlag, + "", + "Bearer token for bor gRPC authentication (must match bor [grpc] token; empty disables auth)", + ) + + // viper.BindPFlag only errors if the flag doesn't exist, which the PersistentFlags().String call above guarantees. + // mutator-disable-next-line CLI flag-binding error guard + if err := v.BindPFlag(BorGRPCTokenFlag, cmd.PersistentFlags().Lookup(BorGRPCTokenFlag)); err != nil { + // mutator-disable-next-line operator-log line in unreachable error branch + loggerInstance.Error(fmt.Sprintf(bindPFlagLog, caller, BorGRPCTokenFlag), "Error", err) + } + // add CometBFTNodeURLFlag flag cmd.PersistentFlags().String( CometBFTNodeURLFlag, @@ -980,7 +1202,7 @@ func DecorateWithHeimdallFlags(cmd *cobra.Command, v *viper.Viper, loggerInstanc loggerInstance.Error(fmt.Sprintf(bindPFlagLog, caller, LogsWriterFileFlag), "Error", err) } - // add producers flag + // add producerVotes flag cmd.PersistentFlags().String( ProducerVotesFlag, "", @@ -1019,6 +1241,12 @@ func (c *CustomAppConfig) UpdateWithFlags(v *viper.Viper, loggerInstance logger. c.Custom.BorGRPCUrl = stringConfigValue } + // get bearer token for bor gRPC from viper/cobra + stringConfigValue = v.GetString(BorGRPCTokenFlag) + if stringConfigValue != "" { + c.Custom.BorGRPCToken = stringConfigValue + } + // get endpoint for cometBFT from viper/cobra stringConfigValue = v.GetString(CometBFTNodeURLFlag) if stringConfigValue != "" { @@ -1152,14 +1380,16 @@ func (c *CustomAppConfig) Merge(cc *CustomConfig) { c.Custom.BorRPCUrl = cc.BorRPCUrl } - if !cc.BorGRPCFlag { - c.Custom.BorGRPCFlag = cc.BorGRPCFlag - } + c.Custom.BorGRPCFlag = cc.BorGRPCFlag if cc.BorGRPCUrl != "" { c.Custom.BorGRPCUrl = cc.BorGRPCUrl } + if cc.BorGRPCToken != "" { + c.Custom.BorGRPCToken = cc.BorGRPCToken + } + if cc.CometBFTRPCUrl != "" { c.Custom.CometBFTRPCUrl = cc.CometBFTRPCUrl } @@ -1226,7 +1456,7 @@ func (c *CustomAppConfig) Merge(cc *CustomConfig) { // DecorateWithCometBFTFlags creates cometBFT flags for the desired command and binds them to viper func DecorateWithCometBFTFlags(cmd *cobra.Command, v *viper.Viper, loggerInstance logger.Logger, message string) { - // add seeds flag + // add seedsFlag cmd.PersistentFlags().String( SeedsFlag, "", diff --git a/helper/config_test.go b/helper/config_test.go index b62455097..77e15c941 100644 --- a/helper/config_test.go +++ b/helper/config_test.go @@ -3,15 +3,19 @@ package helper import ( "fmt" "testing" + "time" + logger "cosmossdk.io/log" cfg "github.com/cometbft/cometbft/config" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/stretchr/testify/require" ) // TestHeimdallConfig checks heimdall configs func TestHeimdallConfig(t *testing.T) { - t.Parallel() + // Not t.Parallel(): this test mutates package-level configs via InitTestHeimdallConfig // cli context cometBFTNode := "tcp://localhost:26657" @@ -73,6 +77,86 @@ func TestHeimdallConfigUpdateCometBFTConfig(t *testing.T) { conf.Custom.Chain = oldConf } +// TestVerifyBorGRPCHashParityNilClients checks that verifyBorGRPCHashParity +// returns immediately without spawning a goroutine when either client is nil. +// The happy path (hashes match) and the mismatch path (log.Fatal) both require +// live Bor HTTP/gRPC servers and are integration-test only. +func TestVerifyBorGRPCHashParityNilClients(t *testing.T) { + t.Parallel() + + // Dispatcher must return without panic and without spawning a goroutine + // when either client is nil. + verifyBorGRPCHashParity(nil, nil, time.Second) +} + +// TestUpdateParityMismatchStreak covers the mismatch-streak state machine that gates log.Fatal on +// confirmed parity mismatches. +func TestUpdateParityMismatchStreak(t *testing.T) { + t.Parallel() + + const limit = 3 + + t.Run("transient failure resets streak", func(t *testing.T) { + t.Parallel() + next, fatal := updateParityMismatchStreak(2, false, limit) + require.Equal(t, 0, next) + require.False(t, fatal) + }) + + t.Run("single mismatch does not fatal", func(t *testing.T) { + t.Parallel() + next, fatal := updateParityMismatchStreak(0, true, limit) + require.Equal(t, 1, next) + require.False(t, fatal) + }) + + t.Run("mismatch below threshold does not fatal", func(t *testing.T) { + t.Parallel() + next, fatal := updateParityMismatchStreak(1, true, limit) + require.Equal(t, 2, next) + require.False(t, fatal) + }) + + t.Run("reaching threshold triggers fatal", func(t *testing.T) { + t.Parallel() + next, fatal := updateParityMismatchStreak(2, true, limit) + require.Equal(t, 3, next) + require.True(t, fatal) + }) + + t.Run("transient after mismatches resets cleanly", func(t *testing.T) { + t.Parallel() + // Simulate: mismatch, mismatch, transient-failure (reset), mismatch + n, fatal := updateParityMismatchStreak(0, true, limit) // 0 -> 1 + require.Equal(t, 1, n) + require.False(t, fatal) + n, fatal = updateParityMismatchStreak(n, true, limit) // 1 -> 2 + require.Equal(t, 2, n) + require.False(t, fatal) + n, fatal = updateParityMismatchStreak(n, false, limit) // transient -> reset to 0 + require.Equal(t, 0, n) + require.False(t, fatal) + n, fatal = updateParityMismatchStreak(n, true, limit) // 0 -> 1 (not 3) + require.Equal(t, 1, n) + require.False(t, fatal) + }) + + t.Run("different streakLimit changes fatal threshold", func(t *testing.T) { + t.Parallel() + // Exercises streakLimit as a real parameter rather than a constant: with + // a higher limit, a single mismatch must not fatal even when it would + // under the standard limit=3. + const higher = 5 + next, fatal := updateParityMismatchStreak(2, true, higher) + require.Equal(t, 3, next) + require.False(t, fatal, "limit=5 must not fatal at streak=3") + + next, fatal = updateParityMismatchStreak(4, true, higher) + require.Equal(t, 5, next) + require.True(t, fatal, "limit=5 must fatal at streak=5") + }) +} + func TestGetChainManagerAddressMigration(t *testing.T) { // Backup and defer restore for chainManagerAddressMigrations originalMigrations := make(map[string]map[int64]ChainManagerAddressMigration) @@ -89,7 +173,7 @@ func TestGetChainManagerAddressMigration(t *testing.T) { originalCustom := conf.Custom defer func() { conf.Custom = originalCustom }() - // Backup and restore viper flags + // Back up and restore viper flags originalChain := viper.GetString(ChainFlag) defer viper.Set(ChainFlag, originalChain) @@ -126,3 +210,32 @@ func TestGetChainManagerAddressMigration(t *testing.T) { t.Errorf("Expected migration to not be found") } } + +// TestDecorateWithHeimdallFlags_BorGRPCFlags verifies that DecorateWithHeimdallFlags +// registers the BorGRPC flags so that the flag lookup and viper binding work. +func TestDecorateWithHeimdallFlags_BorGRPCFlags(t *testing.T) { + t.Parallel() + + cmd := &cobra.Command{Use: "test"} + v := viper.New() + log := logger.NewNopLogger() + + DecorateWithHeimdallFlags(cmd, v, log, "test") + + // Verify BorGRPCUrlFlag was registered + flag := cmd.PersistentFlags().Lookup(BorGRPCUrlFlag) + require.NotNil(t, flag, "BorGRPCUrlFlag must be registered by DecorateWithHeimdallFlags") + require.Equal(t, BorGRPCUrlFlag, flag.Name) + + // Verify BorGRPCFlagFlag was registered. + flag = cmd.PersistentFlags().Lookup(BorGRPCFlagFlag) + require.NotNil(t, flag, "BorGRPCFlagFlag must be registered by DecorateWithHeimdallFlags") + + // Verify BorGRPCTokenFlag was registered. + flag = cmd.PersistentFlags().Lookup(BorGRPCTokenFlag) + require.NotNil(t, flag, "BorGRPCTokenFlag must be registered by DecorateWithHeimdallFlags") + + // Verify the viper binding works: set a value and retrieve via viper. + require.NoError(t, cmd.PersistentFlags().Set(BorGRPCUrlFlag, "http://example.com:9090")) + require.Equal(t, "http://example.com:9090", v.GetString(BorGRPCUrlFlag)) +} diff --git a/helper/grpc_vs_http_bench_test.go b/helper/grpc_vs_http_bench_test.go new file mode 100644 index 000000000..d06acf220 --- /dev/null +++ b/helper/grpc_vs_http_bench_test.go @@ -0,0 +1,258 @@ +//go:build bench +// +build bench + +// Benchmark for HTTP vs. gRPC comparison against a running kurtosis devnet. +// Env vars: +// BOR_RPC_URL (required) — bor HTTP RPC URL (e.g., http://127.0.0.1:XXXXX) +// BOR_GRPC_URL (required) — bor gRPC URL (e.g., http://127.0.0.1:XXXXX) +// BOR_GRPC_TOKEN (optional) — bearer token when gRPC auth is enabled +// BENCH_BLOCK_RANGE (optional, default 10:100) — "start:end" for batch tests +// +// Usage: +// BOR_RPC_URL=... BOR_GRPC_URL=... go test -tags bench -run=^$ -bench=. -benchmem -benchtime=5s ./helper/ + +package helper + +import ( + "context" + "math/big" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + + borgrpc "github.com/0xPolygon/heimdall-v2/x/bor/grpc" +) + +var ( + sinkBytes []byte + sinkBool bool + sinkHeader *ethTypes.Header + sinkBlock *ethTypes.Block + sinkAddress *common.Address + sinkUint64 uint64 + sinkErr error +) + +func newBenchCaller(b *testing.B) (httpCC, grpcCC *ContractCaller) { + b.Helper() + borRPC := os.Getenv("BOR_RPC_URL") + borGRPC := os.Getenv("BOR_GRPC_URL") + if borRPC == "" || borGRPC == "" { + b.Skip("BOR_RPC_URL and BOR_GRPC_URL must both be set") + } + token := os.Getenv("BOR_GRPC_TOKEN") + + rpcClient, err := rpc.Dial(borRPC) + if err != nil { + b.Fatalf("dial bor HTTP RPC: %v", err) + } + httpClient := ethclient.NewClient(rpcClient) + + gcl, err := borgrpc.NewBorGRPCClient(borGRPC, token, Logger) + if err != nil { + b.Fatalf("dial bor gRPC: %v", err) + } + + httpCC = &ContractCaller{ + BorChainClient: httpClient, + BorChainRPCClient: rpcClient, + BorChainTimeout: 10 * time.Second, + BorChainGrpcFlag: false, + } + grpcCC = &ContractCaller{ + BorChainClient: httpClient, + BorChainRPCClient: rpcClient, + BorChainTimeout: 10 * time.Second, + BorChainGrpcFlag: true, + BorChainGrpcClient: gcl, + } + return httpCC, grpcCC +} + +func parseBenchRange(b *testing.B) (uint64, uint64) { + b.Helper() + raw := os.Getenv("BENCH_BLOCK_RANGE") + if raw == "" { + return 10, 100 + } + parts := strings.SplitN(raw, ":", 2) + if len(parts) != 2 { + b.Fatalf("BENCH_BLOCK_RANGE must be in the form start:end, got %q", raw) + } + startN, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + b.Fatalf("BENCH_BLOCK_RANGE start %q: %v", parts[0], err) + } + endN, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + b.Fatalf("BENCH_BLOCK_RANGE end %q: %v", parts[1], err) + } + if endN < startN { + b.Fatalf("BENCH_BLOCK_RANGE end (%d) must be >= start (%d)", endN, startN) + } + return startN, endN +} + +func latestHash(b *testing.B, cc *ContractCaller) common.Hash { + b.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + h, err := cc.BorChainClient.HeaderByNumber(ctx, nil) + if err != nil { + b.Fatalf("latest header: %v", err) + } + return h.Hash() +} + +func BenchmarkM_GetBorChainBlock(b *testing.B) { + httpCC, grpcCC := newBenchCaller(b) + b.Run("http", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkHeader, sinkErr = httpCC.GetBorChainBlock(context.Background(), nil) + } + }) + b.Run("grpc", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkHeader, sinkErr = grpcCC.GetBorChainBlock(context.Background(), nil) + } + }) +} + +func BenchmarkM_GetBorChainBlockAuthor(b *testing.B) { + httpCC, grpcCC := newBenchCaller(b) + blockNum := big.NewInt(1) + b.Run("http", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkAddress, sinkErr = httpCC.GetBorChainBlockAuthor(context.Background(), blockNum) + } + }) + b.Run("grpc", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkAddress, sinkErr = grpcCC.GetBorChainBlockAuthor(context.Background(), blockNum) + } + }) +} + +func BenchmarkM_GetBorChainBlockTd(b *testing.B) { + httpCC, grpcCC := newBenchCaller(b) + hash := latestHash(b, httpCC) + b.Run("http", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkUint64, sinkErr = httpCC.GetBorChainBlockTd(context.Background(), hash) + } + }) + b.Run("grpc", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkUint64, sinkErr = grpcCC.GetBorChainBlockTd(context.Background(), hash) + } + }) +} + +func BenchmarkM_GetBorChainBlockInfoInBatch(b *testing.B) { + httpCC, grpcCC := newBenchCaller(b) + startU, endU := parseBenchRange(b) + start, end := int64(startU), int64(endU) + b.Run("http", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, _, sinkErr = httpCC.GetBorChainBlockInfoInBatch(context.Background(), start, end) + } + }) + b.Run("grpc", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, _, sinkErr = grpcCC.GetBorChainBlockInfoInBatch(context.Background(), start, end) + } + }) +} + +func BenchmarkM_GetRootHash(b *testing.B) { + httpCC, grpcCC := newBenchCaller(b) + start, end := parseBenchRange(b) + const checkpointLen = uint64(256) + b.Run("http", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkBytes, sinkErr = httpCC.GetRootHash(start, end, checkpointLen) + } + }) + b.Run("grpc", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkBytes, sinkErr = grpcCC.GetRootHash(start, end, checkpointLen) + } + }) +} + +func BenchmarkM_CheckIfBlocksExist(b *testing.B) { + httpCC, grpcCC := newBenchCaller(b) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + h, err := httpCC.BorChainClient.HeaderByNumber(ctx, nil) + if err != nil { + b.Fatalf("latest header: %v", err) + } + num := h.Number.Uint64() + b.Run("http", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkBool, sinkErr = httpCC.CheckIfBlocksExist(num) + } + }) + b.Run("grpc", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkBool, sinkErr = grpcCC.CheckIfBlocksExist(num) + } + }) +} + +func BenchmarkM_GetBlockByNumber(b *testing.B) { + httpCC, grpcCC := newBenchCaller(b) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + h, err := httpCC.BorChainClient.HeaderByNumber(ctx, nil) + if err != nil { + b.Fatalf("latest header: %v", err) + } + num := h.Number.Uint64() + b.Run("http", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkBlock, sinkErr = httpCC.GetBlockByNumber(context.Background(), num) + } + }) + b.Run("grpc", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkBlock, sinkErr = grpcCC.GetBlockByNumber(context.Background(), num) + } + }) +} diff --git a/helper/mocks/i_contract_caller.go b/helper/mocks/i_contract_caller.go index 6eea93f40..f014c46cb 100644 --- a/helper/mocks/i_contract_caller.go +++ b/helper/mocks/i_contract_caller.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.53.6. DO NOT EDIT. package mocks @@ -540,9 +540,9 @@ func (_m *IContractCaller) GetBorChainBlock(_a0 context.Context, _a1 *big.Int) ( return r0, r1 } -// GetBorChainBlockAuthor provides a mock function with given fields: _a0 -func (_m *IContractCaller) GetBorChainBlockAuthor(_a0 *big.Int) (*common.Address, error) { - ret := _m.Called(_a0) +// GetBorChainBlockAuthor provides a mock function with given fields: ctx, blockNum +func (_m *IContractCaller) GetBorChainBlockAuthor(ctx context.Context, blockNum *big.Int) (*common.Address, error) { + ret := _m.Called(ctx, blockNum) if len(ret) == 0 { panic("no return value specified for GetBorChainBlockAuthor") @@ -550,19 +550,19 @@ func (_m *IContractCaller) GetBorChainBlockAuthor(_a0 *big.Int) (*common.Address var r0 *common.Address var r1 error - if rf, ok := ret.Get(0).(func(*big.Int) (*common.Address, error)); ok { - return rf(_a0) + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*common.Address, error)); ok { + return rf(ctx, blockNum) } - if rf, ok := ret.Get(0).(func(*big.Int) *common.Address); ok { - r0 = rf(_a0) + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *common.Address); ok { + r0 = rf(ctx, blockNum) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*common.Address) } } - if rf, ok := ret.Get(1).(func(*big.Int) error); ok { - r1 = rf(_a0) + if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { + r1 = rf(ctx, blockNum) } else { r1 = ret.Error(1) } diff --git a/helper/parity_check_test.go b/helper/parity_check_test.go new file mode 100644 index 000000000..3e2f820b1 --- /dev/null +++ b/helper/parity_check_test.go @@ -0,0 +1,429 @@ +package helper + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +// stubHTTPFetcher implements parityHTTPFetcher with a configurable call sequence. +type stubHTTPFetcher struct { + calls []*ethTypes.Header // returned in order; nil entry means return an error + errs []error // parallel error slice; takes priority when non-nil + idx int +} + +func (s *stubHTTPFetcher) HeaderByNumber(_ context.Context, _ *big.Int) (*ethTypes.Header, error) { + if s.idx >= len(s.calls) { + return nil, errors.New("stub: no more responses configured") + } + h := s.calls[s.idx] + var e error + if s.idx < len(s.errs) { + e = s.errs[s.idx] + } + s.idx++ + return h, e +} + +// stubGRPCFetcher implements parityGRPCFetcher with a single fixed response. +type stubGRPCFetcher struct { + header *ethTypes.Header + err error +} + +func (s *stubGRPCFetcher) HeaderByNumber(_ context.Context, _ int64) (*ethTypes.Header, error) { + return s.header, s.err +} + +// makeHeader is a minimal helper that produces a non-zero ethTypes.Header with +// a specific block number. Two headers with different numbers will have +// different Hash() values because Hash covers the Number field. +func makeHeader(num int64) *ethTypes.Header { + return ðTypes.Header{ + Number: big.NewInt(num), + Difficulty: big.NewInt(1), + } +} + +// TestResolveParityTargetHeight covers all three logical branches: +// - HTTP error / nil → (0, false) +// - Chain too young (latestNum < borGRPCParityDepth) → (0, false) +// - Happy path → (latestNum - borGRPCParityDepth, true) +func TestResolveParityTargetHeight(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("http_error_returns_false", func(t *testing.T) { + t.Parallel() + + http := &stubHTTPFetcher{ + calls: []*ethTypes.Header{nil}, + errs: []error{errors.New("rpc error")}, + } + num, ok := resolveParityTargetHeight(ctx, http) + require.False(t, ok) + require.Equal(t, int64(0), num) + }) + + t.Run("nil_response_returns_false", func(t *testing.T) { + t.Parallel() + + http := &stubHTTPFetcher{ + calls: []*ethTypes.Header{nil}, + } + num, ok := resolveParityTargetHeight(ctx, http) + require.False(t, ok) + require.Equal(t, int64(0), num) + }) + + t.Run("chain_too_young_returns_false", func(t *testing.T) { + t.Parallel() + + // latestNum=10 < borGRPCParityDepth=32 → too young. + http := &stubHTTPFetcher{calls: []*ethTypes.Header{makeHeader(10)}} + num, ok := resolveParityTargetHeight(ctx, http) + require.False(t, ok) + require.Equal(t, int64(0), num) + }) + + t.Run("exactly_at_depth_boundary_is_allowed", func(t *testing.T) { + t.Parallel() + + http := &stubHTTPFetcher{calls: []*ethTypes.Header{makeHeader(borGRPCParityDepth)}} + num, ok := resolveParityTargetHeight(ctx, http) + require.True(t, ok) + require.Equal(t, int64(0), num) + }) + + t.Run("one_below_depth_boundary_returns_false", func(t *testing.T) { + t.Parallel() + + // latestNum = borGRPCParityDepth - 1 < borGRPCParityDepth → too young. + http := &stubHTTPFetcher{calls: []*ethTypes.Header{makeHeader(borGRPCParityDepth - 1)}} + num, ok := resolveParityTargetHeight(ctx, http) + require.False(t, ok) + require.Equal(t, int64(0), num) + }) + + t.Run("happy_path_returns_target_minus_depth", func(t *testing.T) { + t.Parallel() + + // latestNum=100 > 32 → target = 100 - 32 = 68. + http := &stubHTTPFetcher{calls: []*ethTypes.Header{makeHeader(100)}} + num, ok := resolveParityTargetHeight(ctx, http) + require.True(t, ok) + require.Equal(t, int64(68), num) + }) +} + +// TestParityConstants verifies the parity configuration constants. +func TestParityConstants(t *testing.T) { + t.Parallel() + + require.Equal(t, 5*time.Second, borGRPCParityRetryInterval, + "borGRPCParityRetryInterval must be exactly 5s") + require.Equal(t, 60, borGRPCParityMaxAttempts, + "borGRPCParityMaxAttempts must be 60") + require.Equal(t, int64(32), borGRPCParityDepth, + "borGRPCParityDepth must be 32") + require.Equal(t, 3, borGRPCParityMismatchStreak, + "borGRPCParityMismatchStreak must be 3") +} + +// TestRunBorGRPCHashParityCheckWith covers the loop logic: early exit on success, +// fatal trigger on mismatch streak, and streak reset on transient. +func TestRunBorGRPCHashParityCheckWith(t *testing.T) { + t.Parallel() + + const smallTimeout = 5 * time.Second + + t.Run("exits_early_on_first_success", func(t *testing.T) { + t.Parallel() + + h := makeHeader(100) + http := &stubHTTPFetcher{calls: []*ethTypes.Header{h, h, h, h, h, h}} + grpc := &stubGRPCFetcher{header: h} + + fatalCalled := false + runBorGRPCHashParityCheckWith(http, grpc, smallTimeout, 2, 0, func(msg string, _ ...interface{}) { + fatalCalled = true + }) + + require.False(t, fatalCalled, "success on first attempt must not trigger fatal") + // Exactly 3 HTTP calls consumed (1 for latest + 2 for stability check). + // If the early-return is removed, the second attempt consumes 3 more (idx=6). + require.Equal(t, 3, http.idx, "early-return on ok must exit after exactly one attempt") + }) + + t.Run("max_attempts_one_runs_exactly_once", func(t *testing.T) { + t.Parallel() + + http := &stubHTTPFetcher{ + calls: []*ethTypes.Header{nil}, + errs: []error{errors.New("transient")}, + } + + fatalCalled := false + runBorGRPCHashParityCheckWith(http, &stubGRPCFetcher{header: makeHeader(50)}, smallTimeout, 1, 0, func(msg string, _ ...interface{}) { + fatalCalled = true + }) + + require.False(t, fatalCalled) + // One HTTP call was made for the single attempt (resolveParityTargetHeight). + require.Equal(t, 1, http.idx, "loop body must execute exactly once with maxAttempts=1") + }) + + t.Run("streak_triggers_fatal_after_threshold", func(t *testing.T) { + t.Parallel() + + // Three consecutive mismatches → streak reaches borGRPCParityMismatchStreak (3) → fatal. + // Each parity check uses 3 HTTP calls. For 3 checks, we need 9 HTTP calls. + httpHdr := makeHeader(100) + grpcHdrDiff := makeHeader(100) + grpcHdrDiff.GasLimit = 99999 + + httpCalls := make([]*ethTypes.Header, 9) + for i := range httpCalls { + httpCalls[i] = httpHdr + } + http := &stubHTTPFetcher{calls: httpCalls} + grpc := &stubGRPCFetcher{header: grpcHdrDiff} + + fatalCalled := false + runBorGRPCHashParityCheckWith(http, grpc, smallTimeout, 10, 0, func(msg string, _ ...interface{}) { + fatalCalled = true + }) + + require.True(t, fatalCalled, "3 consecutive mismatches must trigger fatal") + }) + + t.Run("transient_resets_streak_no_fatal", func(t *testing.T) { + t.Parallel() + + httpHdr := makeHeader(100) + grpcHdrDiff := makeHeader(100) + grpcHdrDiff.GasLimit = 12345 + + calls := make([]*ethTypes.Header, 0) + errs := make([]error, 0) + + // Attempt 1 (mismatch): calls 1,2,3 all return httpHdr; grpc returns different + for i := 0; i < 3; i++ { + calls = append(calls, httpHdr) + errs = append(errs, nil) + } + // Attempt 2 (mismatch): calls 4,5,6 + for i := 0; i < 3; i++ { + calls = append(calls, httpHdr) + errs = append(errs, nil) + } + // Attempt 3 (transient, the first HTTP call errors): + calls = append(calls, nil) + errs = append(errs, errors.New("transient")) + // Attempt 4 (success): grpc will return the matching header here + callIdx := 0 + httpFetcher := &funcHTTPFetcher{fn: func(_ context.Context, _ *big.Int) (*ethTypes.Header, error) { + if callIdx < len(calls) { + h := calls[callIdx] + e := errs[callIdx] + callIdx++ + return h, e + } + // Attempt 4: return stable matching header + return httpHdr, nil + }} + + // For grpc: different for first 2 mismatches (6 calls), matching for rest + grpcCallIdx := 0 + grpcFetcher := &funcGRPCFetcher{fn: func(_ context.Context, _ int64) (*ethTypes.Header, error) { + grpcCallIdx++ + if grpcCallIdx <= 2 { + return grpcHdrDiff, nil + } + return httpHdr, nil + }} + + fatalCalled := false + runBorGRPCHashParityCheckWith(httpFetcher, grpcFetcher, smallTimeout, 10, 0, func(msg string, _ ...interface{}) { + fatalCalled = true + }) + require.False(t, fatalCalled, "streak reset by transient must prevent fatal") + }) + + t.Run("exhausted_attempts_does_not_fatal", func(t *testing.T) { + t.Parallel() + + // All attempts return transient (http error) → mismatches stays 0, loop exhausts. + http := &stubHTTPFetcher{calls: make([]*ethTypes.Header, 0)} // will always error + + fatalCalled := false + runBorGRPCHashParityCheckWith(http, &stubGRPCFetcher{header: makeHeader(50)}, smallTimeout, 3, 0, func(msg string, _ ...interface{}) { + fatalCalled = true + }) + require.False(t, fatalCalled, "transient-only attempts must not trigger fatal even after exhaustion") + }) +} + +// funcHTTPFetcher is a test helper for runBorGRPCHashParityCheckWith that +// uses a function closure to produce arbitrary sequential responses. +type funcHTTPFetcher struct { + fn func(ctx context.Context, number *big.Int) (*ethTypes.Header, error) +} + +func (f *funcHTTPFetcher) HeaderByNumber(ctx context.Context, number *big.Int) (*ethTypes.Header, error) { + return f.fn(ctx, number) +} + +// funcGRPCFetcher is the gRPC equivalent. +type funcGRPCFetcher struct { + fn func(ctx context.Context, blockID int64) (*ethTypes.Header, error) +} + +func (f *funcGRPCFetcher) HeaderByNumber(ctx context.Context, blockID int64) (*ethTypes.Header, error) { + return f.fn(ctx, blockID) +} + +// TestCheckBorGRPCHashParityOnceWith covers all return paths of the +// testable core. +func TestCheckBorGRPCHashParityOnceWith(t *testing.T) { + t.Parallel() + + timeout := 5 * time.Second + + t.Run("chain_too_young_returns_false_false", func(t *testing.T) { + t.Parallel() + + h := makeHeader(5) + http := &stubHTTPFetcher{calls: []*ethTypes.Header{h, h, h}} // call1=latest; calls2&3=stable check + grpc := &stubGRPCFetcher{header: h} + + ok, mismatch := checkBorGRPCHashParityOnceWith(http, grpc, timeout) + require.False(t, ok) + require.False(t, mismatch) + }) + + t.Run("unstable_http_returns_false_false", func(t *testing.T) { + t.Parallel() + + h := makeHeader(100) + http := &stubHTTPFetcher{ + calls: []*ethTypes.Header{h, h, nil}, + errs: []error{nil, nil, errors.New("network error")}, + } + grpc := &stubGRPCFetcher{header: h} + + ok, mismatch := checkBorGRPCHashParityOnceWith(http, grpc, timeout) + require.False(t, ok) + require.False(t, mismatch) + }) + + t.Run("hash_mismatch_returns_false_true", func(t *testing.T) { + t.Parallel() + + httpHdr := makeHeader(100) + grpcHdrDiff := makeHeader(100) + grpcHdrDiff.GasLimit = 12345 // different field → different hash + + http := &stubHTTPFetcher{calls: []*ethTypes.Header{httpHdr, httpHdr, httpHdr}} + grpc := &stubGRPCFetcher{header: grpcHdrDiff} + + ok, mismatch := checkBorGRPCHashParityOnceWith(http, grpc, timeout) + require.False(t, ok) + require.True(t, mismatch, "differing hashes must be reported as a mismatch") + }) + + t.Run("hash_match_returns_true_false", func(t *testing.T) { + t.Parallel() + + httpHdr := makeHeader(100) + grpcHdr := makeHeader(100) // same fields → same hash + + http := &stubHTTPFetcher{calls: []*ethTypes.Header{httpHdr, httpHdr, httpHdr}} + grpc := &stubGRPCFetcher{header: grpcHdr} + + ok, mismatch := checkBorGRPCHashParityOnceWith(http, grpc, timeout) + require.True(t, ok, "matching hashes must return ok=true") + require.False(t, mismatch) + }) +} + +// TestFetchStableHeadersAtHeight covers all stability / error branches. +func TestFetchStableHeadersAtHeight(t *testing.T) { + t.Parallel() + + ctx := context.Background() + targetNum := int64(50) + + t.Run("first_http_error_returns_not_stable", func(t *testing.T) { + t.Parallel() + + h := makeHeader(50) + http := &stubHTTPFetcher{ + calls: []*ethTypes.Header{h, h, h}, // non-nil + error on call 1; calls 2,3 for mutant path + errs: []error{errors.New("http error"), nil, nil}, + } + grpc := &stubGRPCFetcher{header: h} + + httpHdr, grpcHdr, stable := fetchStableHeadersAtHeight(ctx, http, grpc, targetNum) + require.False(t, stable) + require.Nil(t, httpHdr) + require.Nil(t, grpcHdr) + }) + + t.Run("grpc_error_returns_not_stable", func(t *testing.T) { + t.Parallel() + + http := &stubHTTPFetcher{calls: []*ethTypes.Header{makeHeader(50), makeHeader(50)}} + grpc := &stubGRPCFetcher{err: errors.New("grpc error")} + + _, _, stable := fetchStableHeadersAtHeight(ctx, http, grpc, targetNum) + require.False(t, stable) + }) + + t.Run("reorg_during_check_returns_not_stable", func(t *testing.T) { + t.Parallel() + + // First HTTP read returns header A; second HTTP read returns header B (different hash). + headerA := makeHeader(50) + // Modify B slightly so its hash differs from A. + headerB := makeHeader(50) + headerB.GasLimit = 999 // same number but different hash + + http := &stubHTTPFetcher{calls: []*ethTypes.Header{headerA, headerB}} + grpc := &stubGRPCFetcher{header: makeHeader(50)} + + _, _, stable := fetchStableHeadersAtHeight(ctx, http, grpc, targetNum) + require.False(t, stable, "diverging http re-reads indicate a reorg; must return not stable") + }) + + t.Run("all_agree_returns_stable", func(t *testing.T) { + t.Parallel() + + // Both HTTP reads return the same header; gRPC also returns a header. + h := makeHeader(50) + http := &stubHTTPFetcher{calls: []*ethTypes.Header{h, h}} + grpc := &stubGRPCFetcher{header: makeHeader(50)} + + httpHdr, grpcHdr, stable := fetchStableHeadersAtHeight(ctx, http, grpc, targetNum) + require.True(t, stable) + require.NotNil(t, httpHdr) + require.NotNil(t, grpcHdr) + }) + + t.Run("nil_grpc_response_returns_not_stable", func(t *testing.T) { + t.Parallel() + + http := &stubHTTPFetcher{calls: []*ethTypes.Header{makeHeader(50), makeHeader(50)}} + grpc := &stubGRPCFetcher{header: nil} + + _, _, stable := fetchStableHeadersAtHeight(ctx, http, grpc, targetNum) + require.False(t, stable) + }) +} diff --git a/helper/toml.go b/helper/toml.go index ac5bf97d9..ce2e599cb 100644 --- a/helper/toml.go +++ b/helper/toml.go @@ -26,6 +26,9 @@ bor_grpc_flag = "{{ .Custom.BorGRPCFlag }}" # GRPC endpoint for bor chain bor_grpc_url = "{{ .Custom.BorGRPCUrl }}" +# Bearer token for bor gRPC authentication (empty disables auth) +bor_grpc_token = "{{ .Custom.BorGRPCToken }}" + # RPC endpoint for cometBFT comet_bft_rpc_url = "{{ .Custom.CometBFTRPCUrl }}" diff --git a/helper/toml_test.go b/helper/toml_test.go index 1b2d946e5..4ddeeb2e5 100644 --- a/helper/toml_test.go +++ b/helper/toml_test.go @@ -21,6 +21,7 @@ func TestDefaultConfigTemplate_ContainsRequiredSections(t *testing.T) { "bor_rpc_url", "bor_grpc_flag", "bor_grpc_url", + "bor_grpc_token", "comet_bft_rpc_url", "sub_graph_url", "amqp_url", @@ -58,6 +59,7 @@ func TestDefaultConfigTemplate_ContainsTemplateVariables(t *testing.T) { "{{ .Custom.BorRPCUrl }}", "{{ .Custom.BorGRPCFlag }}", "{{ .Custom.BorGRPCUrl }}", + "{{ .Custom.BorGRPCToken }}", "{{ .Custom.CometBFTRPCUrl }}", "{{ .Custom.SubGraphUrl }}", "{{ .Custom.AmqpURL }}", diff --git a/helper/tx_test.go b/helper/tx_test.go index bc5fdc6d2..e8085cf68 100644 --- a/helper/tx_test.go +++ b/helper/tx_test.go @@ -94,8 +94,9 @@ func setupMockClient(params mockClientParams) *mockEthClient { } func TestGenerateAuthObj_NormalEIP1559Flow(t *testing.T) { - t.Parallel() - + // Not t.Parallel(): InitTestHeimdallConfig mutates package-level globals + // (conf, privKeyObject, pubKeyObject). Running these in parallel races + // with each other and with other tests that read the same globals. InitTestHeimdallConfig("") baseFee := big.NewInt(20000000000) // 20 Gwei @@ -136,8 +137,9 @@ func TestGenerateAuthObj_NormalEIP1559Flow(t *testing.T) { } func TestGenerateAuthObj_BaseFeeNil(t *testing.T) { - t.Parallel() - + // Not t.Parallel(): InitTestHeimdallConfig mutates package-level globals + // (conf, privKeyObject, pubKeyObject). Running these in parallel races + // with each other and with other tests that read the same globals. InitTestHeimdallConfig("") mockClient := &mockEthClient{ @@ -163,8 +165,9 @@ func TestGenerateAuthObj_BaseFeeNil(t *testing.T) { } func TestGenerateAuthObj_FeeCapExceedsConfiguredMaximum(t *testing.T) { - t.Parallel() - + // Not t.Parallel(): InitTestHeimdallConfig mutates package-level globals + // (conf, privKeyObject, pubKeyObject). Running these in parallel races + // with each other and with other tests that read the same globals. InitTestHeimdallConfig("") // Set a very high base fee that will cause calculated fee cap to exceed config. @@ -188,14 +191,15 @@ func TestGenerateAuthObj_FeeCapExceedsConfiguredMaximum(t *testing.T) { require.NotNil(t, auth) // The calculated fee cap would be (300 * 2) + 1 = 601 Gwei. - // But it should be capped to configured maximum (500 Gwei = DefaultMainChainGasFeeCap). + // But it should be capped to the configured maximum (500 Gwei = DefaultMainChainGasFeeCap). configuredMax := big.NewInt(DefaultMainChainGasFeeCap) assert.Equal(t, configuredMax, auth.GasFeeCap, "GasFeeCap should be capped to configured maximum") } func TestGenerateAuthObj_TipCapExceedsConfiguredMaximum(t *testing.T) { - t.Parallel() - + // Not t.Parallel(): InitTestHeimdallConfig mutates package-level globals + // (conf, privKeyObject, pubKeyObject). Running these in parallel races + // with each other and with other tests that read the same globals. InitTestHeimdallConfig("") baseFee := big.NewInt(20000000000) // 20 Gwei @@ -228,13 +232,14 @@ func TestGenerateAuthObj_TipCapExceedsConfiguredMaximum(t *testing.T) { } func TestGenerateAuthObj_TipCapExceedsFeeCap(t *testing.T) { - t.Parallel() - + // Not t.Parallel(): InitTestHeimdallConfig mutates package-level globals + // (conf, privKeyObject, pubKeyObject). Running these in parallel races + // with each other and with other tests that read the same globals. InitTestHeimdallConfig("") - // Edge case: very high base fee causes fee cap to be clamped, but tip cap is still below fee cap before clamping. - // but tip cap is still below fee cap before clamping. - // After fee cap clamping, tip might exceed fee cap. + // Edge case: a very high base fee causes the fee cap to be clamped, + // but the tip cap is still below the fee cap before clamping. + // After fee cap clamping, the tip might exceed the fee cap. baseFee := big.NewInt(250000000000) // 250 Gwei suggestedTip := big.NewInt(5000000000) // 5 Gwei (below config max of 10 Gwei) @@ -263,8 +268,9 @@ func TestGenerateAuthObj_TipCapExceedsFeeCap(t *testing.T) { } func TestGenerateAuthObj_BlockByNumberError(t *testing.T) { - t.Parallel() - + // Not t.Parallel(): InitTestHeimdallConfig mutates package-level globals + // (conf, privKeyObject, pubKeyObject). Running these in parallel races + // with each other and with other tests that read the same globals. InitTestHeimdallConfig("") mockClient := &mockEthClient{ @@ -283,8 +289,9 @@ func TestGenerateAuthObj_BlockByNumberError(t *testing.T) { } func TestGenerateAuthObj_SuggestGasTipCapError(t *testing.T) { - t.Parallel() - + // Not t.Parallel(): InitTestHeimdallConfig mutates package-level globals + // (conf, privKeyObject, pubKeyObject). Running these in parallel races + // with each other and with other tests that read the same globals. InitTestHeimdallConfig("") baseFee := big.NewInt(20000000000) // 20 Gwei diff --git a/packaging/templates/config/amoy/app.toml b/packaging/templates/config/amoy/app.toml index 577b7ae67..afab20d53 100644 --- a/packaging/templates/config/amoy/app.toml +++ b/packaging/templates/config/amoy/app.toml @@ -257,6 +257,9 @@ bor_grpc_flag = "false" # GRPC endpoint for bor chain bor_grpc_url = "http://localhost:3131" +# Bearer token for bor gRPC authentication (empty disables auth) +bor_grpc_token = "" + # RPC endpoint for cometBFT comet_bft_rpc_url = "http://0.0.0.0:26657" diff --git a/packaging/templates/config/mainnet/app.toml b/packaging/templates/config/mainnet/app.toml index fa069a3ca..2c5d0e72a 100644 --- a/packaging/templates/config/mainnet/app.toml +++ b/packaging/templates/config/mainnet/app.toml @@ -257,6 +257,9 @@ bor_grpc_flag = "false" # GRPC endpoint for bor chain bor_grpc_url = "http://localhost:3131" +# Bearer token for bor gRPC authentication (empty disables auth) +bor_grpc_token = "" + # RPC endpoint for cometBFT comet_bft_rpc_url = "http://0.0.0.0:26657" diff --git a/x/bor/grpc/client.go b/x/bor/grpc/client.go index 5766c9795..66844a729 100644 --- a/x/bor/grpc/client.go +++ b/x/bor/grpc/client.go @@ -23,111 +23,179 @@ type BorGRPCClient struct { client proto.BorApiClient } -func NewBorGRPCClient(address string, logger log.Logger) (*BorGRPCClient, error) { - timeout := 5 * time.Second - addr := address - var dialOpts []grpc.DialOption +// dialTimeout caps the per-attempt timeout for non-HTTP callers (currently just unix). +const dialTimeout = 5 * time.Second - logger.Info("Setting up Bor gRPC client", "address", address) - - // URL mode - if strings.Contains(address, "://") { - // Decide credentials and normalized address based on the provided scheme - u, err := url.Parse(address) - if err != nil { - logger.Error("Invalid Bor gRPC URL", "url", address, "err", err) - return nil, err - } +// MaxBlockInfoBatchSize caps GetBlockInfoInBatch inputs below int64 overflow +// and above realistic checkpoint/milestone spans. Single source of truth +// shared with the helper/ dispatcher so the HTTP and gRPC paths cannot drift. +const MaxBlockInfoBatchSize = 10000 - switch u.Scheme { - case "https": - // Remote secure connection - addr = u.Host - if addr == "" { - err := fmt.Errorf("invalid Bor gRPC https URL %q: empty host", address) - logger.Error("Invalid Bor gRPC https URL", "url", address, "err", err) - return nil, err - } - - tlsCfg := &tls.Config{ - ServerName: strings.Split(addr, ":")[0], - MinVersion: tls.VersionTLS12, - } - dialOpts = append(dialOpts, - grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)), - ) - - case "http": - // plaintext only allowed for local host - addr = u.Host - if !isLocalhost(addr) { - logger.Warn("Using insecure non-local Bor gRPC over http. This is discouraged", "addr", addr) - } - dialOpts = append(dialOpts, - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - - case "unix": - // support unix://path for on-box Bor nodes - path := u.Path - if path == "" { - err := fmt.Errorf("invalid unix Bor gRPC URL %q: empty path", address) - logger.Error("Invalid unix Bor gRPC URL", "url", address, "err", err) - return nil, err - } - addr = "unix://" + path - dialOpts = append(dialOpts, - grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { - return net.DialTimeout("unix", strings.TrimPrefix(addr, "unix://"), timeout) - }), - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - - default: - err := fmt.Errorf("unsupported Bor gRPC URL scheme %q in %q", u.Scheme, address) - logger.Error("Unsupported Bor gRPC URL scheme", "url", address, "scheme", u.Scheme, "err", err) - return nil, err - } +func NewBorGRPCClient(address, token string, logger log.Logger) (*BorGRPCClient, error) { + logger.Info("Setting up Bor gRPC client", "address", address) - } else { - // No scheme provided, treat as host:port, but only allow if local - if !isLocalhost(addr) { - err := fmt.Errorf("insecure non-local Bor gRPC without scheme (addr=%s); use http://localhost:port or https://host:port", addr) - logger.Error("Refusing insecure non-local Bor gRPC without scheme", "addr", addr, "err", err) - return nil, err - } - dialOpts = append(dialOpts, - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) + addr, transportOpts, isTLS, err := resolveTransport(address, token, logger) + if err != nil { + return nil, err } - // Retry options - retryOpts := []grpcRetry.CallOption{ - grpcRetry.WithMax(10000), - grpcRetry.WithBackoff(grpcRetry.BackoffLinear(5 * time.Second)), - grpcRetry.WithCodes(codes.Internal, codes.Unavailable, codes.Aborted, codes.NotFound), + dialOpts := append([]grpc.DialOption(nil), transportOpts...) + // Flipping `!= ""` to `== ""` attaches credentials with an empty token; the token+plaintext reject path is tested via resolveHTTP. + // mutator-disable-next-line token-presence guard + if token != "" { + dialOpts = append(dialOpts, grpc.WithPerRPCCredentials(bearerToken{token: token, requireSecurity: isTLS})) } + dialOpts = append(dialOpts, retryInterceptors()...) - dialOpts = append(dialOpts, - grpc.WithStreamInterceptor(grpcRetry.StreamClientInterceptor(retryOpts...)), - grpc.WithUnaryInterceptor(grpcRetry.UnaryClientInterceptor(retryOpts...)), - ) - - // dial using address and dialOpts conn, err := grpc.NewClient(addr, dialOpts...) + // NewClient only errors on malformed URI, which resolveTransport has already validated. + // mutator-disable-next-line defensive grpc.NewClient error guard if err != nil { + // Operator-log line inside the unreachable error branch. + // mutator-disable-next-line statement-deletion on log logger.Error("Failed to connect to Bor gRPC", "addr", addr, "error", err) + // Return value inside the unreachable error branch; err is carried anyway. + // mutator-disable-next-line return-value in unreachable branch return nil, err } + // Operator-log announcing successful wiring; removal is silent but not a behavior change. + // mutator-disable-next-line statement-deletion on log logger.Info("Connected to Bor gRPC server", "grpcAddress", address, "dialAddr", addr) - return &BorGRPCClient{ conn: conn, client: proto.NewBorApiClient(conn), }, nil } +// resolveTransport parses the address, picks the right credentials, and +// returns (dialAddr, transportDialOpts, isTLS, error). +func resolveTransport(address, token string, logger log.Logger) (string, []grpc.DialOption, bool, error) { + if !strings.Contains(address, "://") { + // Bare host:port — only allowed if localhost. + return resolveNoScheme(address, logger) + } + u, err := url.Parse(address) + if err != nil { + // mutator-disable-next-line operator-log line for an already-error-returning branch + logger.Error("Invalid Bor gRPC URL", "url", address, "err", err) + return "", nil, false, err + } + switch u.Scheme { + case "https": + return resolveHTTPS(u, address, logger) + case "http": + return resolveHTTP(u, token, logger) + case "unix": + return resolveUnix(u, address, logger) + default: + err := fmt.Errorf("unsupported Bor gRPC URL scheme %q in %q", u.Scheme, address) + // mutator-disable-next-line operator-log line; the err returned below carries the same message + logger.Error("Unsupported Bor gRPC URL scheme", "url", address, "scheme", u.Scheme, "err", err) + return "", nil, false, err + } +} + +func resolveHTTPS(u *url.URL, address string, logger log.Logger) (string, []grpc.DialOption, bool, error) { + addr := u.Host + if addr == "" { + err := fmt.Errorf("invalid Bor gRPC https URL %q: empty host", address) + // mutator-disable-next-line operator-log line; err below carries the same message + logger.Error("Invalid Bor gRPC https URL", "url", address, "err", err) + return "", nil, false, err + } + + tlsCfg := &tls.Config{ + ServerName: u.Hostname(), + MinVersion: tls.VersionTLS12, + } + return addr, []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg))}, true, nil +} + +func resolveHTTP(u *url.URL, token string, logger log.Logger) (string, []grpc.DialOption, bool, error) { + addr := u.Host + if addr == "" { + err := fmt.Errorf("invalid Bor gRPC http URL %q: empty host", u.String()) + // Operator-log line; err below carries the same content. + // mutator-disable-next-line operator-log line + logger.Error("Invalid Bor gRPC http URL", "url", u.String(), "err", err) + return "", nil, false, err + } + if !isLocalhost(addr) { + // Bearer token + non-local plaintext would leak the token. + if token != "" { + err := fmt.Errorf("refusing to send bor gRPC bearer token over non-local plaintext http (addr=%s); use https:// for remote hosts", addr) + // mutator-disable-next-line operator-log line; err below carries the same content + logger.Error("Refusing bor gRPC bearer token over plaintext", "addr", addr, "err", err) + return "", nil, false, err + } + // mutator-disable-next-line operator-log line advising against the insecure config path + logger.Warn("Using insecure non-local Bor gRPC over http. This is discouraged", "addr", addr) + } + return addr, []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}, false, nil +} + +func resolveUnix(u *url.URL, address string, logger log.Logger) (string, []grpc.DialOption, bool, error) { + if u.Path == "" { + err := fmt.Errorf("invalid unix Bor gRPC URL %q: empty path", address) + // mutator-disable-next-line operator-log line; err below carries the same content + logger.Error("Invalid unix Bor gRPC URL", "url", address, "err", err) + return "", nil, false, err + } + addr := "unix://" + u.Path + dialer := &net.Dialer{Timeout: dialTimeout} + opts := []grpc.DialOption{ + grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, "unix", strings.TrimPrefix(addr, "unix://")) + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + } + return addr, opts, false, nil +} + +func resolveNoScheme(addr string, logger log.Logger) (string, []grpc.DialOption, bool, error) { + if !isLocalhost(addr) { + err := fmt.Errorf("insecure non-local Bor gRPC without scheme (addr=%s); use http://localhost:port or https://host:port", addr) + // mutator-disable-next-line operator-log line; err below carries the same content + logger.Error("Refusing insecure non-local Bor gRPC without scheme", "addr", addr, "err", err) + return "", nil, false, err + } + return addr, []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}, false, nil +} + +// retryInterceptors returns the standard retry unary+stream client interceptors. +func retryInterceptors() []grpc.DialOption { + retryOpts := []grpcRetry.CallOption{ + grpcRetry.WithMax(4), + grpcRetry.WithBackoff(grpcRetry.BackoffLinear(500 * time.Millisecond)), + grpcRetry.WithCodes(codes.Unavailable, codes.Aborted), + } + return []grpc.DialOption{ + grpc.WithStreamInterceptor(grpcRetry.StreamClientInterceptor(retryOpts...)), + grpc.WithUnaryInterceptor(grpcRetry.UnaryClientInterceptor(retryOpts...)), + } +} + +// bearerToken implements credentials.PerRPCCredentials, attaching the +// configured bearer token to every gRPC call as the `authorization` header. +type bearerToken struct { + token string + requireSecurity bool // true when the underlying transport is TLS +} + +func (b bearerToken) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { + return map[string]string{ + "authorization": "Bearer " + b.token, + }, nil +} + +// RequireTransportSecurity returns whether per-RPC credentials require TLS. +// We only require TLS when heimdall was configured with https:// gRPC URL, +// otherwise we'd refuse to connect to localhost over plaintext. +func (b bearerToken) RequireTransportSecurity() bool { + return b.requireSecurity +} + func (c *BorGRPCClient) Close(logger log.Logger) { if c == nil || c.conn == nil { return diff --git a/x/bor/grpc/client_test.go b/x/bor/grpc/client_test.go index b07b46519..be76e3feb 100644 --- a/x/bor/grpc/client_test.go +++ b/x/bor/grpc/client_test.go @@ -1,10 +1,15 @@ package grpc import ( + "context" + "net/url" "testing" + "time" "cosmossdk.io/log" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) func TestIsLocalhost(t *testing.T) { @@ -89,7 +94,7 @@ func TestNewBorGRPCClient_URLParsing(t *testing.T) { t.Run("rejects invalid URL", func(t *testing.T) { t.Parallel() - client, err := NewBorGRPCClient("://invalid", logger) + client, err := NewBorGRPCClient("://invalid", "", logger) require.Error(t, err) require.Nil(t, client) }) @@ -97,7 +102,7 @@ func TestNewBorGRPCClient_URLParsing(t *testing.T) { t.Run("rejects https URL with empty host", func(t *testing.T) { t.Parallel() - client, err := NewBorGRPCClient("https://", logger) + client, err := NewBorGRPCClient("https://", "", logger) require.Error(t, err) require.Contains(t, err.Error(), "empty host") require.Nil(t, client) @@ -106,7 +111,7 @@ func TestNewBorGRPCClient_URLParsing(t *testing.T) { t.Run("rejects unix URL with empty path", func(t *testing.T) { t.Parallel() - client, err := NewBorGRPCClient("unix://", logger) + client, err := NewBorGRPCClient("unix://", "", logger) require.Error(t, err) require.Contains(t, err.Error(), "empty path") require.Nil(t, client) @@ -115,7 +120,7 @@ func TestNewBorGRPCClient_URLParsing(t *testing.T) { t.Run("rejects unsupported URL scheme", func(t *testing.T) { t.Parallel() - client, err := NewBorGRPCClient("ftp://example.com", logger) + client, err := NewBorGRPCClient("ftp://example.com", "", logger) require.Error(t, err) require.Contains(t, err.Error(), "unsupported") require.Nil(t, client) @@ -124,11 +129,43 @@ func TestNewBorGRPCClient_URLParsing(t *testing.T) { t.Run("rejects non-local address without scheme", func(t *testing.T) { t.Parallel() - client, err := NewBorGRPCClient("example.com:8545", logger) + client, err := NewBorGRPCClient("example.com:8545", "", logger) require.Error(t, err) require.Contains(t, err.Error(), "insecure non-local") require.Nil(t, client) }) + + t.Run("rejects non-local plaintext http when token is set", func(t *testing.T) { + t.Parallel() + + // Bearer token over plaintext http:// to a non-local host would leak the + // token on the network path. Construction must refuse explicitly. + client, err := NewBorGRPCClient("http://remote.example.com:3131", "secret-token", logger) + require.Error(t, err) + require.Contains(t, err.Error(), "plaintext") + require.Nil(t, client) + }) + + t.Run("accepts non-local plaintext http when no token is set", func(t *testing.T) { + t.Parallel() + + // Historical behavior: a non-local http:// with no token just warns. + // Kept backward-compatible for operators who have an unauthenticated + // cross-host setup inside a trusted network. + client, err := NewBorGRPCClient("http://remote.example.com:3131", "", logger) + require.NoError(t, err) + require.NotNil(t, client) + }) + + t.Run("accepts localhost plaintext http with token set", func(t *testing.T) { + t.Parallel() + + // Loopback is trusted — sending the token over local plaintext is + // the recommended same-host validator setup. + client, err := NewBorGRPCClient("http://localhost:3131", "secret-token", logger) + require.NoError(t, err) + require.NotNil(t, client) + }) } func TestBorGRPCClient_Close(t *testing.T) { @@ -154,3 +191,280 @@ func TestBorGRPCClient_Close(t *testing.T) { }) }) } + +// TestResolveHTTPS verifies that resolveHTTPS extracts the host as the dial +// address, sets isTLS=true, returns exactly one DialOption, and rejects an +// empty host. +func TestResolveHTTPS(t *testing.T) { + t.Parallel() + + logger := log.NewNopLogger() + + t.Run("happy path returns host and isTLS=true", func(t *testing.T) { + t.Parallel() + + u := mustParseURL(t, "https://grpc.example.com:443") + addr, opts, isTLS, err := resolveHTTPS(u, "https://grpc.example.com:443", logger) + + require.NoError(t, err) + require.Equal(t, "grpc.example.com:443", addr) + require.True(t, isTLS, "resolveHTTPS must return isTLS=true") + require.Len(t, opts, 1, "exactly one DialOption (TLS credentials)") + }) + + t.Run("empty host returns error and isTLS=false", func(t *testing.T) { + t.Parallel() + + u := mustParseURL(t, "https://") + _, _, isTLS, err := resolveHTTPS(u, "https://", logger) + + require.Error(t, err) + require.Contains(t, err.Error(), "empty host") + require.False(t, isTLS) + }) +} + +// TestResolveHTTP verifies isTLS=false, addr extraction, and the +// token-over-plaintext rejection. +func TestResolveHTTP(t *testing.T) { + t.Parallel() + + logger := log.NewNopLogger() + + t.Run("localhost with token returns isTLS=false", func(t *testing.T) { + t.Parallel() + + u := mustParseURL(t, "http://localhost:3131") + addr, opts, isTLS, err := resolveHTTP(u, "secret", logger) + + require.NoError(t, err) + require.Equal(t, "localhost:3131", addr) + require.False(t, isTLS, "resolveHTTP must return isTLS=false") + require.Len(t, opts, 1, "exactly one DialOption (insecure credentials)") + }) + + t.Run("remote host without token returns isTLS=false", func(t *testing.T) { + t.Parallel() + + u := mustParseURL(t, "http://remote.example.com:3131") + addr, opts, isTLS, err := resolveHTTP(u, "", logger) + + require.NoError(t, err) + require.Equal(t, "remote.example.com:3131", addr) + require.False(t, isTLS) + require.Len(t, opts, 1) + }) + + t.Run("remote host with token returns error", func(t *testing.T) { + t.Parallel() + + u := mustParseURL(t, "http://remote.example.com:3131") + _, _, isTLS, err := resolveHTTP(u, "token", logger) + + require.Error(t, err) + require.Contains(t, err.Error(), "plaintext") + require.False(t, isTLS) + }) + + t.Run("empty host returns error and isTLS=false", func(t *testing.T) { + t.Parallel() + + u := mustParseURL(t, "http://") + _, _, isTLS, err := resolveHTTP(u, "", logger) + + require.Error(t, err) + require.Contains(t, err.Error(), "empty host") + require.False(t, isTLS) + }) +} + +// TestResolveUnix verifies the unix:// path prefix is prepended correctly and +// that isTLS=false. +func TestResolveUnix(t *testing.T) { + t.Parallel() + + logger := log.NewNopLogger() + + t.Run("valid unix path returns unix:// addr and isTLS=false", func(t *testing.T) { + t.Parallel() + + u := mustParseURL(t, "unix:///var/run/bor.sock") + addr, opts, isTLS, err := resolveUnix(u, "unix:///var/run/bor.sock", logger) + + require.NoError(t, err) + require.Equal(t, "unix:///var/run/bor.sock", addr) + require.False(t, isTLS, "resolveUnix must return isTLS=false") + // Two DialOptions: context dialer + insecure credentials. + require.Len(t, opts, 2) + }) + + t.Run("empty path returns error", func(t *testing.T) { + t.Parallel() + + u := mustParseURL(t, "unix://") + _, _, isTLS, err := resolveUnix(u, "unix://", logger) + + require.Error(t, err) + require.Contains(t, err.Error(), "empty path") + require.False(t, isTLS) + }) +} + +// TestResolveNoScheme verifies that bare localhost addresses are accepted with +// isTLS=false and that non-local addresses are rejected. +func TestResolveNoScheme(t *testing.T) { + t.Parallel() + + logger := log.NewNopLogger() + + t.Run("localhost address accepted with isTLS=false", func(t *testing.T) { + t.Parallel() + + addr, opts, isTLS, err := resolveNoScheme("localhost:9090", logger) + + require.NoError(t, err) + require.Equal(t, "localhost:9090", addr) + require.False(t, isTLS, "resolveNoScheme must return isTLS=false") + require.Len(t, opts, 1) + }) + + t.Run("127.0.0.1 address accepted", func(t *testing.T) { + t.Parallel() + + addr, opts, isTLS, err := resolveNoScheme("127.0.0.1:9090", logger) + + require.NoError(t, err) + require.Equal(t, "127.0.0.1:9090", addr) + require.False(t, isTLS) + require.Len(t, opts, 1) + }) + + t.Run("remote address rejected", func(t *testing.T) { + t.Parallel() + + _, _, isTLS, err := resolveNoScheme("remote.example.com:9090", logger) + + require.Error(t, err) + require.Contains(t, err.Error(), "insecure non-local") + require.False(t, isTLS) + }) +} + +// TestBearerToken verifies GetRequestMetadata returns the correct authorization +// header value, and RequireTransportSecurity reflects the requireSecurity field. +func TestBearerToken(t *testing.T) { + t.Parallel() + + t.Run("GetRequestMetadata returns Bearer header", func(t *testing.T) { + t.Parallel() + + bt := bearerToken{token: "my-secret-token", requireSecurity: false} + meta, err := bt.GetRequestMetadata(context.Background()) + + require.NoError(t, err) + require.Equal(t, "Bearer my-secret-token", meta["authorization"]) + }) + + t.Run("RequireTransportSecurity true when requireSecurity=true", func(t *testing.T) { + t.Parallel() + + bt := bearerToken{token: "tok", requireSecurity: true} + require.True(t, bt.RequireTransportSecurity()) + }) + + t.Run("RequireTransportSecurity false when requireSecurity=false", func(t *testing.T) { + t.Parallel() + + bt := bearerToken{token: "tok", requireSecurity: false} + require.False(t, bt.RequireTransportSecurity()) + }) +} + +// TestDialTimeout verifies the dialTimeout constant is 5 seconds and has not +// been inadvertently mutated. +func TestDialTimeout(t *testing.T) { + t.Parallel() + + require.Equal(t, 5*time.Second, dialTimeout, + "dialTimeout constant must be exactly 5 s") +} + +// TestResolveTransport_TokenWithTLS verifies that when resolveTransport picks +// the https scheme and a token is provided, the returned isTLS=true propagates +// into bearerToken. +func TestResolveTransport_TokenWithTLS(t *testing.T) { + t.Parallel() + + logger := log.NewNopLogger() + + addr, _, isTLS, err := resolveTransport("https://grpc.example.com:443", "tok", logger) + require.NoError(t, err) + require.Equal(t, "grpc.example.com:443", addr) + require.True(t, isTLS) +} + +// TestResolveTransport_ErrorPaths verifies that resolveTransport's error +// branches return isTLS=false. The bool is passed through to bearerToken's +// RequireTransportSecurity; a spurious `true` here would reject a valid +// localhost dial on a later retry. +func TestResolveTransport_ErrorPaths(t *testing.T) { + t.Parallel() + + logger := log.NewNopLogger() + + t.Run("malformed URL returns isTLS=false", func(t *testing.T) { + t.Parallel() + + addr, opts, isTLS, err := resolveTransport("://invalid", "", logger) + require.Error(t, err) + require.Empty(t, addr) + require.Nil(t, opts) + require.False(t, isTLS, "error return must keep isTLS=false") + }) + + t.Run("unsupported scheme returns isTLS=false", func(t *testing.T) { + t.Parallel() + + addr, opts, isTLS, err := resolveTransport("ftp://example.com:1234", "", logger) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported") + require.Empty(t, addr) + require.Nil(t, opts) + require.False(t, isTLS, "error return must keep isTLS=false") + }) +} + +// TestNewBorGRPCClient_ErrorPath verifies that a dial error from grpc.NewClient +// is propagated and the function returns nil. +func TestNewBorGRPCClient_ErrorPath(t *testing.T) { + t.Parallel() + + logger := log.NewNopLogger() + client, err := NewBorGRPCClient("://bad", "", logger) + + require.Error(t, err) + require.Nil(t, client) +} + +// TestRetryInterceptors verifies that retryInterceptors returns exactly two +// DialOptions (unary and stream). +func TestRetryInterceptors(t *testing.T) { + t.Parallel() + + opts := retryInterceptors() + require.Len(t, opts, 2, "retryInterceptors must return exactly 2 DialOptions") + + // Verify the options are actually usable by creating a dummy client. + allOpts := append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient("localhost:0", allOpts...) + require.NoError(t, err) + _ = conn.Close() +} + +// mustParseURL is a test helper that parses a URL string or fails the test. +func mustParseURL(t *testing.T, rawURL string) *url.URL { + t.Helper() + u, err := url.Parse(rawURL) + require.NoError(t, err, "test setup: failed to parse URL %q", rawURL) + return u +} diff --git a/x/bor/grpc/query.go b/x/bor/grpc/query.go index fa0d43429..3756f2eb5 100644 --- a/x/bor/grpc/query.go +++ b/x/bor/grpc/query.go @@ -3,15 +3,15 @@ package grpc import ( "context" "fmt" - "math" "math/big" proto "github.com/0xPolygon/polyproto/bor" + commonproto "github.com/0xPolygon/polyproto/common" protoutil "github.com/0xPolygon/polyproto/utils" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethTypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" ) @@ -21,14 +21,13 @@ func (c *BorGRPCClient) GetRootHash(ctx context.Context, startBlock uint64, endB EndBlockNumber: endBlock, } - log.Info("Fetching bor root hash") - res, err := c.client.GetRootHash(ctx, req) if err != nil { return "", err } - - log.Info("Fetched bor root hash") + if res == nil { + return "", ethereum.NotFound + } return res.RootHash, nil } @@ -41,73 +40,68 @@ func (c *BorGRPCClient) GetVoteOnHash(ctx context.Context, startBlock uint64, en MilestoneId: milestoneId, } - log.Info("Fetching vote on hash") - res, err := c.client.GetVoteOnHash(ctx, req) if err != nil { return false, err } - - log.Info("Fetched vote on hash") + if res == nil { + return false, ethereum.NotFound + } return res.Response, nil } func (c *BorGRPCClient) HeaderByNumber(ctx context.Context, blockID int64) (*ethTypes.Header, error) { - if blockID > math.MaxInt64 { - return nil, fmt.Errorf("blockID too large: %d", blockID) - } - blockNumberAsString := ToBlockNumArg(big.NewInt(blockID)) req := &proto.GetHeaderByNumberRequest{ Number: blockNumberAsString, } - log.Info("Fetching header by number") - res, err := c.client.HeaderByNumber(ctx, req) if err != nil { - return ðTypes.Header{}, err + return nil, err } - - log.Info("Fetched header by number") - - resp := ðTypes.Header{ - Number: big.NewInt(int64(res.Header.Number)), - ParentHash: protoutil.ConvertH256ToHash(res.Header.ParentHash), - Time: res.Header.Time, + if res == nil || res.Header == nil { + return nil, ethereum.NotFound } - - return resp, nil + // protoHeaderToEthHeader returns nil for a malformed header + // (e.g., oversized bloom / difficulty / baseFee). + // Surface that as NotFound so callers never nil-deref downstream. + h := protoHeaderToEthHeader(res.Header) + if h == nil { + return nil, ethereum.NotFound + } + if blockID >= 0 && h.Number.Int64() != blockID { + return nil, fmt.Errorf("bor grpc HeaderByNumber: server returned block %d for request %d", h.Number.Int64(), blockID) + } + return h, nil } func (c *BorGRPCClient) BlockByNumber(ctx context.Context, blockID int64) (*ethTypes.Block, error) { - if blockID > math.MaxInt64 { - return nil, fmt.Errorf("blockID too large: %d", blockID) - } - blockNumberAsString := ToBlockNumArg(big.NewInt(blockID)) req := &proto.GetBlockByNumberRequest{ Number: blockNumberAsString, } - log.Info("Fetching block by number") - res, err := c.client.BlockByNumber(ctx, req) if err != nil { - return ðTypes.Block{}, err + return nil, err } - - log.Info("Fetched block by number") - - header := ethTypes.Header{ - Number: big.NewInt(int64(res.Block.Header.Number)), - ParentHash: protoutil.ConvertH256ToHash(res.Block.Header.ParentHash), - Time: res.Block.Header.Time, + if res == nil || res.Block == nil || res.Block.Header == nil { + return nil, ethereum.NotFound + } + // Same malformed-header guard as HeaderByNumber + header := protoHeaderToEthHeader(res.Block.Header) + if header == nil { + return nil, ethereum.NotFound } - return ethTypes.NewBlock(&header, nil, nil, nil), nil + // Same guard as HeaderByNumber. + if blockID >= 0 && header.Number.Int64() != blockID { + return nil, fmt.Errorf("bor grpc BlockByNumber: server returned block %d for request %d", header.Number.Int64(), blockID) + } + return ethTypes.NewBlockWithHeader(header), nil } func (c *BorGRPCClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*ethTypes.Receipt, error) { @@ -115,16 +109,18 @@ func (c *BorGRPCClient) TransactionReceipt(ctx context.Context, txHash common.Ha Hash: protoutil.ConvertHashToH256(txHash), } - log.Info("Fetching transaction receipt") - res, err := c.client.TransactionReceipt(ctx, req) if err != nil { - return ðTypes.Receipt{}, err + return nil, err } - - log.Info("Fetched transaction receipt") - - return receiptResponseToTypesReceipt(res.Receipt), nil + if res == nil || res.Receipt == nil { + return nil, ethereum.NotFound + } + r := receiptResponseToTypesReceipt(res.Receipt) + if r == nil { + return nil, ethereum.NotFound + } + return r, nil } func (c *BorGRPCClient) BorBlockReceipt(ctx context.Context, txHash common.Hash) (*ethTypes.Receipt, error) { @@ -132,32 +128,153 @@ func (c *BorGRPCClient) BorBlockReceipt(ctx context.Context, txHash common.Hash) Hash: protoutil.ConvertHashToH256(txHash), } - log.Info("Fetching bor block receipt") - res, err := c.client.BorBlockReceipt(ctx, req) if err != nil { - return ðTypes.Receipt{}, err + return nil, err + } + if res == nil || res.Receipt == nil { + return nil, ethereum.NotFound + } + r := receiptResponseToTypesReceipt(res.Receipt) + if r == nil { + return nil, ethereum.NotFound + } + return r, nil +} + +// GetAuthor returns the author of the block at blockNum. Nil blockNum resolves +// to the latest block (via ToBlockNumArg, which maps nil to "latest"). +func (c *BorGRPCClient) GetAuthor(ctx context.Context, blockNum *big.Int) (*common.Address, error) { + req := &proto.GetAuthorRequest{Number: ToBlockNumArg(blockNum)} + + res, err := c.client.GetAuthor(ctx, req) + if err != nil { + return nil, err + } + // Missing-author and missing-response are both surfaced as ethereum.NotFound, + // matching the RPC methods (HeaderByNumber, BlockByNumber, GetTdByHash, GetTdByNumber) and + // the HTTP path's ethclient contract. + if res == nil || res.Author == nil { + return nil, ethereum.NotFound + } + + addr := protoH160ToAddress(res.Author) + return &addr, nil +} + +// GetTdByHash returns the total difficulty of the block identified by hash. +func (c *BorGRPCClient) GetTdByHash(ctx context.Context, hash common.Hash) (uint64, error) { + req := &proto.GetTdByHashRequest{Hash: protoutil.ConvertHashToH256(hash)} + res, err := c.client.GetTdByHash(ctx, req) + if err != nil { + return 0, err + } + if res == nil { + return 0, ethereum.NotFound + } + return res.TotalDifficulty, nil +} + +// GetTdByNumber returns the total difficulty of the block at blockNum. +// Nil blockNum resolves to the latest block. +func (c *BorGRPCClient) GetTdByNumber(ctx context.Context, blockNum *big.Int) (uint64, error) { + req := &proto.GetTdByNumberRequest{Number: ToBlockNumArg(blockNum)} + res, err := c.client.GetTdByNumber(ctx, req) + if err != nil { + return 0, err + } + if res == nil { + return 0, ethereum.NotFound + } + return res.TotalDifficulty, nil +} + +// GetBlockInfoInBatch returns headers, total difficulties, and authors for the +// inclusive block range [start, end]. Returns up to (end-start+1) entries; a +// shorter slice means the server encountered a missing block or other error +// mid-range, matching the HTTP eth_getHeaderByNumber batch semantics. +// Returns an error for invalid input ranges. +func (c *BorGRPCClient) GetBlockInfoInBatch(ctx context.Context, start, end int64) ([]*ethTypes.Header, []uint64, []common.Address, error) { + if start < 0 || end < 0 || end < start { + return nil, nil, nil, fmt.Errorf("invalid range [%d,%d]", start, end) + } + if end-start > MaxBlockInfoBatchSize-1 { + return nil, nil, nil, fmt.Errorf("range too large: %d blocks exceeds max %d", end-start+1, MaxBlockInfoBatchSize) + } + + req := &proto.GetBlockInfoInBatchRequest{ + StartBlockNumber: uint64(start), + EndBlockNumber: uint64(end), + } + res, err := c.client.GetBlockInfoInBatch(ctx, req) + if err != nil { + return nil, nil, nil, err + } + if res == nil { + return nil, nil, nil, ethereum.NotFound } - log.Info("Fetched bor block receipt") + n := len(res.Blocks) + headers := make([]*ethTypes.Header, 0, n) + tds := make([]uint64, 0, n) + authors := make([]common.Address, 0, n) + + maxLen := int(end - start + 1) + for _, b := range res.Blocks { + if len(headers) >= maxLen { + break + } + if b == nil || b.Header == nil { + break + } + // Stop on a malformed header, matching the HTTP side + h := protoHeaderToEthHeader(b.Header) + if h == nil { + break + } + // The block number must match the next expected slot in + // the requested range. Otherwise, wrong hashes land in + // downstream milestone propositions. + expected := uint64(start) + uint64(len(headers)) + if h.Number.Uint64() != expected { + break + } + // Match HTTP collateBorBatchResults: a non-genesis block with no + // author entry is a failure. Break instead of appending a + // zero-address placeholder that diverges from the HTTP path. + if b.Author == nil && b.Header.Number != 0 { + break + } + headers = append(headers, h) + tds = append(tds, b.TotalDifficulty) + + // Nil-safe wrapper: handles the (nil author | nil inner Hi) case + // for genesis blocks, where the server is allowed to send nil author. + addr := protoH160ToAddress(b.Author) + authors = append(authors, addr) + } - return receiptResponseToTypesReceipt(res.Receipt), nil + return headers, tds, authors, nil } func receiptResponseToTypesReceipt(receipt *proto.Receipt) *ethTypes.Receipt { + // Reject out-of-range values for fields that would truncate or overflow in ethTypes.Receipt, mapping them to NotFound. + if receipt.Type > 0xff { + return nil + } // Bloom and Logs have been intentionally left out as they are not used in the current implementation return ðTypes.Receipt{ Type: uint8(receipt.Type), PostState: receipt.PostState, Status: receipt.Status, CumulativeGasUsed: receipt.CumulativeGasUsed, - TxHash: protoutil.ConvertH256ToHash(receipt.TxHash), - ContractAddress: protoutil.ConvertH160toAddress(receipt.ContractAddress), + TxHash: protoH256ToHash(receipt.TxHash), + ContractAddress: protoH160ToAddress(receipt.ContractAddress), GasUsed: receipt.GasUsed, EffectiveGasPrice: big.NewInt(receipt.EffectiveGasPrice), BlobGasUsed: receipt.BlobGasUsed, BlobGasPrice: big.NewInt(receipt.BlobGasPrice), - BlockHash: protoutil.ConvertH256ToHash(receipt.BlockHash), + BlockHash: protoH256ToHash(receipt.BlockHash), BlockNumber: big.NewInt(receipt.BlockNumber), TransactionIndex: uint(receipt.TransactionIndex), } @@ -177,3 +294,81 @@ func ToBlockNumArg(number *big.Int) string { // It's negative and large, which is invalid. return fmt.Sprintf("", number) } + +// protoHeaderToEthHeader rebuilds a full ethTypes.Header from the proto wire +// form. Returns nil on a nil or malformed input (e.g., out-of-bounds bloom) so +// callers can map it to ethereum.NotFound. +func protoHeaderToEthHeader(p *proto.Header) *ethTypes.Header { + if p == nil { + return nil + } + + if len(p.Bloom) != 0 && len(p.Bloom) != ethTypes.BloomByteLength { + return nil + } + + if len(p.Difficulty) > 32 || len(p.BaseFee) > 32 { + return nil + } + // ethTypes.Header.Nonce is a fixed BlockNonceLength (8) byte array. + if len(p.Nonce) != 0 && len(p.Nonce) != len(ethTypes.BlockNonce{}) { + return nil + } + + h := ðTypes.Header{ + ParentHash: protoH256ToHash(p.ParentHash), + UncleHash: protoH256ToHash(p.UncleHash), + Coinbase: protoH160ToAddress(p.Coinbase), + Root: protoH256ToHash(p.StateRoot), + TxHash: protoH256ToHash(p.TxRoot), + ReceiptHash: protoH256ToHash(p.ReceiptRoot), + Difficulty: new(big.Int).SetBytes(p.Difficulty), + Number: new(big.Int).SetUint64(p.Number), + GasLimit: p.GasLimit, + GasUsed: p.GasUsed, + Time: p.Time, + Extra: append([]byte(nil), p.ExtraData...), + MixDigest: protoH256ToHash(p.MixDigest), + } + h.Bloom.SetBytes(p.Bloom) + copy(h.Nonce[:], p.Nonce) + + if len(p.BaseFee) > 0 { + h.BaseFee = new(big.Int).SetBytes(p.BaseFee) + } + if p.WithdrawalsHash != nil { + v := protoH256ToHash(p.WithdrawalsHash) + h.WithdrawalsHash = &v + } + // BlobGasUsed and ExcessBlobGas are proto3 `optional`. + // We use *uint64 as a direct pointer, so the copy preserves nil vs. zero. + h.BlobGasUsed = p.BlobGasUsed + h.ExcessBlobGas = p.ExcessBlobGas + if p.ParentBeaconBlockRoot != nil { + v := protoH256ToHash(p.ParentBeaconBlockRoot) + h.ParentBeaconRoot = &v + } + if p.RequestsHash != nil { + v := protoH256ToHash(p.RequestsHash) + h.RequestsHash = &v + } + return h +} + +// protoH256ToHash converts a proto H256 (or nil) to a common.Hash. +func protoH256ToHash(h *commonproto.H256) common.Hash { + if h == nil || h.Hi == nil || h.Lo == nil { + return common.Hash{} + } + b := protoutil.ConvertH256ToHash(h) + return common.BytesToHash(b[:]) +} + +// protoH160ToAddress converts a proto H160 (or nil) to a common.Address. +func protoH160ToAddress(a *commonproto.H160) common.Address { + if a == nil || a.Hi == nil { + return common.Address{} + } + arr := protoutil.ConvertH160toAddress(a) + return common.BytesToAddress(arr[:]) +} diff --git a/x/bor/grpc/query_test.go b/x/bor/grpc/query_test.go index 95ea7d48c..a5f66bf02 100644 --- a/x/bor/grpc/query_test.go +++ b/x/bor/grpc/query_test.go @@ -6,12 +6,15 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + ethTypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc" proto "github.com/0xPolygon/polyproto/bor" + commonproto "github.com/0xPolygon/polyproto/common" protoutil "github.com/0xPolygon/polyproto/utils" ) @@ -76,6 +79,38 @@ func (m *MockBorApiClient) GetStartBlockHeimdallSpanID(ctx context.Context, req return args.Get(0).(*proto.GetStartBlockHeimdallSpanIDResponse), args.Error(1) } +func (m *MockBorApiClient) GetAuthor(ctx context.Context, req *proto.GetAuthorRequest, _ ...grpc.CallOption) (*proto.GetAuthorResponse, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*proto.GetAuthorResponse), args.Error(1) +} + +func (m *MockBorApiClient) GetTdByHash(ctx context.Context, req *proto.GetTdByHashRequest, _ ...grpc.CallOption) (*proto.GetTdResponse, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*proto.GetTdResponse), args.Error(1) +} + +func (m *MockBorApiClient) GetTdByNumber(ctx context.Context, req *proto.GetTdByNumberRequest, _ ...grpc.CallOption) (*proto.GetTdResponse, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*proto.GetTdResponse), args.Error(1) +} + +func (m *MockBorApiClient) GetBlockInfoInBatch(ctx context.Context, req *proto.GetBlockInfoInBatchRequest, _ ...grpc.CallOption) (*proto.GetBlockInfoInBatchResponse, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*proto.GetBlockInfoInBatchResponse), args.Error(1) +} + func TestGetRootHash(t *testing.T) { t.Parallel() @@ -85,7 +120,7 @@ func TestGetRootHash(t *testing.T) { mockClient := new(MockBorApiClient) grpcClient := &BorGRPCClient{client: mockClient} - expectedHash := "0x1234567890abcdef" + expectedHash := "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" mockClient.On("GetRootHash", mock.Anything, mock.MatchedBy(func(req *proto.GetRootHashRequest) bool { return req.StartBlockNumber == 100 && req.EndBlockNumber == 200 })).Return(&proto.GetRootHashResponse{RootHash: expectedHash}, nil) @@ -202,9 +237,6 @@ func TestHeaderByNumber(t *testing.T) { mockClient.AssertExpectations(t) }) - // Note: The "blockID too large" check in the original code is unreachable - // since blockID is int64, so no test for it - t.Run("error retrieving header", func(t *testing.T) { t.Parallel() @@ -217,8 +249,23 @@ func TestHeaderByNumber(t *testing.T) { header, err := grpcClient.HeaderByNumber(context.Background(), 100) require.Error(t, err) require.Contains(t, err.Error(), "header error") - require.NotNil(t, header) // Returns empty header on error - require.Nil(t, header.Number) + require.Nil(t, header, "returns nil on error, matching ethclient convention") + + mockClient.AssertExpectations(t) + }) + + t.Run("nil header in response maps to ethereum.NotFound", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("HeaderByNumber", mock.Anything, mock.Anything). + Return(&proto.GetHeaderByNumberResponse{Header: nil}, nil) + + header, err := grpcClient.HeaderByNumber(context.Background(), 100) + require.ErrorIs(t, err, ethereum.NotFound) + require.Nil(t, header) mockClient.AssertExpectations(t) }) @@ -256,9 +303,6 @@ func TestBlockByNumber(t *testing.T) { mockClient.AssertExpectations(t) }) - // Note: The "blockID too large" check in the original code is unreachable - // since blockID is int64, so no test for it - t.Run("error retrieving block", func(t *testing.T) { t.Parallel() @@ -271,7 +315,39 @@ func TestBlockByNumber(t *testing.T) { block, err := grpcClient.BlockByNumber(context.Background(), 200) require.Error(t, err) require.Contains(t, err.Error(), "block error") - require.NotNil(t, block) // Returns empty block on error + require.Nil(t, block, "returns nil on error, matching ethclient convention") + + mockClient.AssertExpectations(t) + }) + + t.Run("nil block in response maps to ethereum.NotFound", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("BlockByNumber", mock.Anything, mock.Anything). + Return(&proto.GetBlockByNumberResponse{Block: nil}, nil) + + block, err := grpcClient.BlockByNumber(context.Background(), 200) + require.ErrorIs(t, err, ethereum.NotFound) + require.Nil(t, block) + + mockClient.AssertExpectations(t) + }) + + t.Run("block with nil header maps to ethereum.NotFound", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("BlockByNumber", mock.Anything, mock.Anything). + Return(&proto.GetBlockByNumberResponse{Block: &proto.Block{Header: nil}}, nil) + + block, err := grpcClient.BlockByNumber(context.Background(), 200) + require.ErrorIs(t, err, ethereum.NotFound) + require.Nil(t, block) mockClient.AssertExpectations(t) }) @@ -340,7 +416,7 @@ func TestTransactionReceipt(t *testing.T) { receipt, err := grpcClient.TransactionReceipt(context.Background(), txHash) require.Error(t, err) require.Contains(t, err.Error(), "receipt error") - require.NotNil(t, receipt) // Returns empty receipt on error + require.Nil(t, receipt, "returns nil on error, matching ethclient convention") mockClient.AssertExpectations(t) }) @@ -401,7 +477,7 @@ func TestBorBlockReceipt(t *testing.T) { receipt, err := grpcClient.BorBlockReceipt(context.Background(), txHash) require.Error(t, err) require.Contains(t, err.Error(), "bor receipt error") - require.NotNil(t, receipt) + require.Nil(t, receipt, "returns nil on error, matching ethclient convention") mockClient.AssertExpectations(t) }) @@ -482,3 +558,542 @@ func TestReceiptResponseToTypesReceipt(t *testing.T) { require.Equal(t, uint(0), result.TransactionIndex) }) } + +func TestProtoHeaderToEthHeader_RoundTrip_Cancun(t *testing.T) { + t.Parallel() + + src := ðTypes.Header{ + ParentHash: common.HexToHash("0x3333333333333333333333333333333333333333333333333333333333333333"), + UncleHash: ethTypes.EmptyUncleHash, + Coinbase: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + Root: common.HexToHash("0x4444444444444444444444444444444444444444444444444444444444444444"), + TxHash: common.HexToHash("0x5555555555555555555555555555555555555555555555555555555555555555"), + ReceiptHash: common.HexToHash("0x6666666666666666666666666666666666666666666666666666666666666666"), + Bloom: ethTypes.Bloom{0x01, 0x02, 0x03}, + Difficulty: big.NewInt(17), + Number: big.NewInt(1234567), + GasLimit: 30_000_000, + GasUsed: 21_000, + Time: 1_700_000_000, + Extra: []byte{0xde, 0xad, 0xbe, 0xef}, + MixDigest: common.HexToHash("0x7777777777777777777777777777777777777777777777777777777777777777"), + Nonce: ethTypes.BlockNonce{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8}, + BaseFee: big.NewInt(1_000_000_000), + WithdrawalsHash: new(common.HexToHash("0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899")), + BlobGasUsed: new(uint64(131072)), + ExcessBlobGas: new(uint64(262144)), + ParentBeaconRoot: new(common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111")), + RequestsHash: new(common.HexToHash("0x2222222222222222222222222222222222222222222222222222222222222222")), + } + + pb := ethHeaderToProtoForTest(src) + got := protoHeaderToEthHeader(pb) + require.Equal(t, src.Hash(), got.Hash()) +} + +// TestProtoHeaderToEthHeader_RoundTrip_CancunZeroBlobGas proves the nil vs. zero trap on blobGasUsed/excessBlobGas is handled. +func TestProtoHeaderToEthHeader_RoundTrip_CancunZeroBlobGas(t *testing.T) { + t.Parallel() + + zeroHash := common.Hash{} + + src := ðTypes.Header{ + ParentHash: common.HexToHash("0x01"), + UncleHash: ethTypes.EmptyUncleHash, + Coinbase: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + Root: common.HexToHash("0x02"), + TxHash: common.HexToHash("0x03"), + ReceiptHash: common.HexToHash("0x04"), + Difficulty: big.NewInt(1), + Number: big.NewInt(100), + GasLimit: 30_000_000, + Time: 1_700_000_000, + BaseFee: big.NewInt(1_000_000_000), + BlobGasUsed: new(uint64(0)), + ExcessBlobGas: new(uint64(0)), + ParentBeaconRoot: &zeroHash, + } + + pb := ethHeaderToProtoForTest(src) + got := protoHeaderToEthHeader(pb) + require.Equal(t, src.Hash(), got.Hash(), "hash must match for Cancun-with-zero-blob-gas header") + require.NotNil(t, got.BlobGasUsed, "BlobGasUsed must round-trip to &0 (not nil)") + require.NotNil(t, got.ExcessBlobGas, "ExcessBlobGas must round-trip to &0 (not nil)") +} + +func TestProtoHeaderToEthHeader_RoundTrip_PreShanghai(t *testing.T) { + t.Parallel() + + src := ðTypes.Header{ + ParentHash: common.HexToHash("0x01"), + UncleHash: ethTypes.EmptyUncleHash, + Coinbase: common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567"), + Root: common.HexToHash("0x02"), + TxHash: common.HexToHash("0x03"), + ReceiptHash: common.HexToHash("0x04"), + Difficulty: big.NewInt(1), + Number: big.NewInt(100), + GasLimit: 30_000_000, + GasUsed: 0, + Time: 1_600_000_000, + Extra: []byte{}, + MixDigest: common.Hash{}, + Nonce: ethTypes.BlockNonce{}, + } + pb := ethHeaderToProtoForTest(src) + got := protoHeaderToEthHeader(pb) + require.Equal(t, src.Hash(), got.Hash()) +} + +func TestGetAuthor(t *testing.T) { + t.Parallel() + + t.Run("successful author retrieval", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + want := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + mockClient.On("GetAuthor", mock.Anything, mock.MatchedBy(func(req *proto.GetAuthorRequest) bool { + return req.Number == "0x2a" // 42 in hex + })).Return(&proto.GetAuthorResponse{ + Author: protoutil.ConvertAddressToH160(want), + }, nil) + + got, err := grpcClient.GetAuthor(context.Background(), big.NewInt(42)) + require.NoError(t, err) + require.NotNil(t, got) + require.Equal(t, want, *got) + + mockClient.AssertExpectations(t) + }) + + t.Run("rpc error propagated", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("GetAuthor", mock.Anything, mock.Anything). + Return(nil, errors.New("author error")) + + got, err := grpcClient.GetAuthor(context.Background(), big.NewInt(42)) + require.Error(t, err) + require.Nil(t, got) + require.Contains(t, err.Error(), "author error") + + mockClient.AssertExpectations(t) + }) + + t.Run("nil author in response", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("GetAuthor", mock.Anything, mock.Anything). + Return(&proto.GetAuthorResponse{Author: nil}, nil) + + got, err := grpcClient.GetAuthor(context.Background(), big.NewInt(42)) + require.Error(t, err) + require.Nil(t, got) + + mockClient.AssertExpectations(t) + }) + + t.Run("nil block number translates to latest", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + addr := common.HexToAddress("0x1234") + mockClient.On("GetAuthor", mock.Anything, mock.MatchedBy(func(req *proto.GetAuthorRequest) bool { + return req.Number == "latest" + })).Return(&proto.GetAuthorResponse{Author: protoutil.ConvertAddressToH160(addr)}, nil) + + got, err := grpcClient.GetAuthor(context.Background(), nil) + require.NoError(t, err) + require.Equal(t, addr, *got) + + mockClient.AssertExpectations(t) + }) +} + +func TestGetTdByHash(t *testing.T) { + t.Parallel() + + t.Run("successful td retrieval", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + wantHash := common.HexToHash("0x01") + mockClient.On("GetTdByHash", mock.Anything, mock.MatchedBy(func(req *proto.GetTdByHashRequest) bool { + got := protoutil.ConvertH256ToHash(req.Hash) + return common.BytesToHash(got[:]) == wantHash + })).Return(&proto.GetTdResponse{TotalDifficulty: 12345}, nil) + + got, err := grpcClient.GetTdByHash(context.Background(), wantHash) + require.NoError(t, err) + require.Equal(t, uint64(12345), got) + + mockClient.AssertExpectations(t) + }) + + t.Run("rpc error propagated", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("GetTdByHash", mock.Anything, mock.Anything). + Return(nil, errors.New("td error")) + + got, err := grpcClient.GetTdByHash(context.Background(), common.Hash{}) + require.Error(t, err) + require.Equal(t, uint64(0), got) + require.Contains(t, err.Error(), "td error") + + mockClient.AssertExpectations(t) + }) +} + +func TestGetTdByNumber(t *testing.T) { + t.Parallel() + + t.Run("successful td retrieval", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("GetTdByNumber", mock.Anything, mock.MatchedBy(func(req *proto.GetTdByNumberRequest) bool { + return req.Number == "0x63" // 99 in hex + })).Return(&proto.GetTdResponse{TotalDifficulty: 54321}, nil) + + got, err := grpcClient.GetTdByNumber(context.Background(), big.NewInt(99)) + require.NoError(t, err) + require.Equal(t, uint64(54321), got) + + mockClient.AssertExpectations(t) + }) + + t.Run("rpc error propagated", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("GetTdByNumber", mock.Anything, mock.Anything). + Return(nil, errors.New("td number error")) + + got, err := grpcClient.GetTdByNumber(context.Background(), big.NewInt(99)) + require.Error(t, err) + require.Equal(t, uint64(0), got) + + mockClient.AssertExpectations(t) + }) +} + +func TestGetBlockInfoInBatch(t *testing.T) { + t.Parallel() + + t.Run("successful batch retrieval", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + hdr100 := makeTestHeader(t, 100) + hdr101 := makeTestHeader(t, 101) + author100 := common.HexToAddress("0x1111111111111111111111111111111111111111") + author101 := common.HexToAddress("0x2222222222222222222222222222222222222222") + + mockClient.On("GetBlockInfoInBatch", mock.Anything, mock.MatchedBy(func(req *proto.GetBlockInfoInBatchRequest) bool { + return req.StartBlockNumber == 100 && req.EndBlockNumber == 101 + })).Return(&proto.GetBlockInfoInBatchResponse{ + Blocks: []*proto.BlockInfo{ + {Header: ethHeaderToProtoForTest(hdr100), TotalDifficulty: 500, Author: protoutil.ConvertAddressToH160(author100)}, + {Header: ethHeaderToProtoForTest(hdr101), TotalDifficulty: 600, Author: protoutil.ConvertAddressToH160(author101)}, + }, + }, nil) + + headers, tds, authors, err := grpcClient.GetBlockInfoInBatch(context.Background(), 100, 101) + require.NoError(t, err) + require.Len(t, headers, 2) + require.Len(t, tds, 2) + require.Len(t, authors, 2) + require.Equal(t, hdr100.Hash(), headers[0].Hash()) + require.Equal(t, hdr101.Hash(), headers[1].Hash()) + require.Equal(t, uint64(500), tds[0]) + require.Equal(t, uint64(600), tds[1]) + require.Equal(t, author100, authors[0]) + require.Equal(t, author101, authors[1]) + + mockClient.AssertExpectations(t) + }) + + t.Run("empty response", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("GetBlockInfoInBatch", mock.Anything, mock.Anything). + Return(&proto.GetBlockInfoInBatchResponse{Blocks: nil}, nil) + + headers, tds, authors, err := grpcClient.GetBlockInfoInBatch(context.Background(), 100, 105) + require.NoError(t, err) + require.Empty(t, headers) + require.Empty(t, tds) + require.Empty(t, authors) + }) + + t.Run("rpc error propagated", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + mockClient.On("GetBlockInfoInBatch", mock.Anything, mock.Anything). + Return(nil, errors.New("batch error")) + + _, _, _, err := grpcClient.GetBlockInfoInBatch(context.Background(), 100, 101) + require.Error(t, err) + require.Contains(t, err.Error(), "batch error") + }) + + t.Run("invalid range rejected locally", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + _, _, _, err := grpcClient.GetBlockInfoInBatch(context.Background(), -1, 5) + require.Error(t, err) + + _, _, _, err = grpcClient.GetBlockInfoInBatch(context.Background(), 10, 5) + require.Error(t, err) + + mockClient.AssertNotCalled(t, "GetBlockInfoInBatch", mock.Anything, mock.Anything) + }) +} + +// TestGetBlockInfoInBatch_RangeBoundary verifies the conditional boundary on the +// range validation, ensuring each boundary is covered. +func TestGetBlockInfoInBatch_RangeBoundary(t *testing.T) { + t.Parallel() + + // return a mock that responds to any GetBlockInfoInBatch call. + makeClientWithEmptyResponse := func() (*MockBorApiClient, *BorGRPCClient) { + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + mockClient.On("GetBlockInfoInBatch", mock.Anything, mock.Anything). + Return(&proto.GetBlockInfoInBatchResponse{Blocks: nil}, nil) + return mockClient, grpcClient + } + + // end <= start would reject start==end=50. + t.Run("start_equals_end_allowed single block range", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + hdr := makeTestHeader(t, 50) + + authorAddr := common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567") + mockClient.On("GetBlockInfoInBatch", mock.Anything, mock.MatchedBy(func(req *proto.GetBlockInfoInBatchRequest) bool { + return req.StartBlockNumber == 50 && req.EndBlockNumber == 50 + })).Return(&proto.GetBlockInfoInBatchResponse{ + Blocks: []*proto.BlockInfo{ + {Header: ethHeaderToProtoForTest(hdr), TotalDifficulty: 100, Author: protoutil.ConvertAddressToH160(authorAddr)}, + }, + }, nil) + + headers, tds, authors, err := grpcClient.GetBlockInfoInBatch(context.Background(), 50, 50) + require.NoError(t, err, "start==end is a valid single-block range and must not be rejected") + require.Len(t, headers, 1) + require.Len(t, tds, 1) + require.Len(t, authors, 1) + require.Equal(t, hdr.Hash(), headers[0].Hash()) + require.Equal(t, uint64(100), tds[0]) + mockClient.AssertExpectations(t) + }) + + // start <= 0 would reject start=0 (genesis block range). + t.Run("zero_start_allowed genesis range", func(t *testing.T) { + t.Parallel() + + _, grpcClient := makeClientWithEmptyResponse() + _, _, _, err := grpcClient.GetBlockInfoInBatch(context.Background(), 0, 5) + require.NoError(t, err, "start=0 is valid (genesis block) and must not be rejected") + }) + + // end <= 0 would reject end=0. + t.Run("zero_end_allowed single genesis block", func(t *testing.T) { + t.Parallel() + + _, grpcClient := makeClientWithEmptyResponse() + _, _, _, err := grpcClient.GetBlockInfoInBatch(context.Background(), 0, 0) + require.NoError(t, err, "end=0 with start=0 is valid (single genesis block) and must not be rejected") + }) + + t.Run("negative_start_rejected", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + _, _, _, err := grpcClient.GetBlockInfoInBatch(context.Background(), -1, 10) + require.Error(t, err, "negative start must be rejected") + mockClient.AssertNotCalled(t, "GetBlockInfoInBatch") + }) + + t.Run("negative_end_rejected", func(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + + _, _, _, err := grpcClient.GetBlockInfoInBatch(context.Background(), 0, -1) + require.Error(t, err, "negative end must be rejected") + mockClient.AssertNotCalled(t, "GetBlockInfoInBatch") + }) +} + +// TestGetBlockInfoInBatch_NilHeaderBreak verifies that when the server returns a +// BlockInfo with nil Header inside the list, iteration stops at that point. +func TestGetBlockInfoInBatch_NilHeaderBreak(t *testing.T) { + t.Parallel() + + mockClient := new(MockBorApiClient) + grpcClient := &BorGRPCClient{client: mockClient} + hdr100 := makeTestHeader(t, 100) + + // Server returns block 100 with a valid header, then block 101 with a nil Header. + // Only block 100 must be returned. + authorAddr := common.HexToAddress("0x0123456789abcdef0123456789abcdef01234567") + mockClient.On("GetBlockInfoInBatch", mock.Anything, mock.Anything). + Return(&proto.GetBlockInfoInBatchResponse{ + Blocks: []*proto.BlockInfo{ + {Header: ethHeaderToProtoForTest(hdr100), TotalDifficulty: 500, Author: protoutil.ConvertAddressToH160(authorAddr)}, + {Header: nil, TotalDifficulty: 600, Author: nil}, // nil Header — must trigger break + }, + }, nil) + + headers, tds, authors, err := grpcClient.GetBlockInfoInBatch(context.Background(), 100, 101) + require.NoError(t, err) + require.Len(t, headers, 1, "iteration must stop at the nil-header block") + require.Len(t, tds, 1) + require.Len(t, authors, 1) + require.Equal(t, hdr100.Hash(), headers[0].Hash()) +} + +// TestProtoHeaderToEthHeader_NilInput verifies that protoHeaderToEthHeader(nil) returns nil. +func TestProtoHeaderToEthHeader_NilInput(t *testing.T) { + t.Parallel() + + result := protoHeaderToEthHeader(nil) + require.Nil(t, result, "protoHeaderToEthHeader(nil) must return nil") +} + +// TestProtoHeaderToEthHeader_OversizedBloom verifies that a server response +// with a Bloom larger than 256 bytes is rejected as nil rather than panicking +// inside ethTypes.Bloom.SetBytes. +func TestProtoHeaderToEthHeader_OversizedBloom(t *testing.T) { + t.Parallel() + + p := &proto.Header{Bloom: make([]byte, ethTypes.BloomByteLength+1)} + require.Nil(t, protoHeaderToEthHeader(p), "oversized bloom must return nil") +} + +// TestProtoH256ToHash_InnerNil verifies that a non-nil outer H256 with a nil +// Hi or Lo sub-message decodes to the zero hash rather than panicking inside +// protoutil.ConvertH256ToHash. +func TestProtoH256ToHash_InnerNil(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + h *commonproto.H256 + }{ + {"nil Hi", &commonproto.H256{Hi: nil, Lo: &commonproto.H128{Hi: 1, Lo: 2}}}, + {"nil Lo", &commonproto.H256{Hi: &commonproto.H128{Hi: 1, Lo: 2}, Lo: nil}}, + {"both nil", &commonproto.H256{Hi: nil, Lo: nil}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, common.Hash{}, protoH256ToHash(tc.h)) + }) + } +} + +// TestProtoH160ToAddress_InnerNil verifies that a non-nil outer H160 with a +// nil Hi sub-message decodes to the zero address rather than panicking inside +// protoutil.ConvertH160toAddress. +func TestProtoH160ToAddress_InnerNil(t *testing.T) { + t.Parallel() + + a := &commonproto.H160{Hi: nil, Lo: 0} + require.Equal(t, common.Address{}, protoH160ToAddress(a)) +} + +// makeTestHeader builds a header for batch tests. +func makeTestHeader(t *testing.T, num uint64) *ethTypes.Header { + t.Helper() + return ðTypes.Header{ + ParentHash: common.BigToHash(big.NewInt(int64(num - 1))), + UncleHash: ethTypes.EmptyUncleHash, + Root: common.BigToHash(big.NewInt(int64(num + 1000))), + TxHash: common.BigToHash(big.NewInt(int64(num + 2000))), + ReceiptHash: common.BigToHash(big.NewInt(int64(num + 3000))), + Difficulty: big.NewInt(1), + Number: new(big.Int).SetUint64(num), + GasLimit: 30_000_000, + Time: 1_700_000_000 + num, + Extra: []byte("test"), + } +} + +func ethHeaderToProtoForTest(h *ethTypes.Header) *proto.Header { + out := &proto.Header{ + Number: h.Number.Uint64(), + ParentHash: protoutil.ConvertHashToH256(h.ParentHash), + Time: h.Time, + UncleHash: protoutil.ConvertHashToH256(h.UncleHash), + Coinbase: protoutil.ConvertAddressToH160(h.Coinbase), + StateRoot: protoutil.ConvertHashToH256(h.Root), + TxRoot: protoutil.ConvertHashToH256(h.TxHash), + ReceiptRoot: protoutil.ConvertHashToH256(h.ReceiptHash), + Bloom: h.Bloom.Bytes(), + GasLimit: h.GasLimit, + GasUsed: h.GasUsed, + ExtraData: append([]byte(nil), h.Extra...), + MixDigest: protoutil.ConvertHashToH256(h.MixDigest), + Nonce: h.Nonce[:], + } + if h.Difficulty != nil { + out.Difficulty = h.Difficulty.Bytes() + } + if h.BaseFee != nil { + out.BaseFee = h.BaseFee.Bytes() + } + if h.WithdrawalsHash != nil { + out.WithdrawalsHash = protoutil.ConvertHashToH256(*h.WithdrawalsHash) + } + // BlobGasUsed / ExcessBlobGas are proto3 optional + // We use *uint64 as the pointer copy to preserve nil vs. zero. + out.BlobGasUsed = h.BlobGasUsed + out.ExcessBlobGas = h.ExcessBlobGas + if h.ParentBeaconRoot != nil { + out.ParentBeaconBlockRoot = protoutil.ConvertHashToH256(*h.ParentBeaconRoot) + } + if h.RequestsHash != nil { + out.RequestsHash = protoutil.ConvertHashToH256(*h.RequestsHash) + } + return out +} diff --git a/x/bor/keeper/grpc_query_test.go b/x/bor/keeper/grpc_query_test.go index ae47edc53..755cb6203 100644 --- a/x/bor/keeper/grpc_query_test.go +++ b/x/bor/keeper/grpc_query_test.go @@ -47,18 +47,17 @@ func (s *KeeperTestSuite) TestGetNextSpan() { require.NoError(err) for _, v := range vals { - add := common.HexToAddress(v.GetOperator()) - err = keeper.StoreSeedProducer(ctx, v.ValId, &add) + err = keeper.StoreSeedProducer(ctx, v.ValId, new(common.HexToAddress(v.GetOperator()))) + require.NoError(err) } firstSpan := s.genTestSpans(1) err = keeper.AddNewSpan(ctx, firstSpan[0]) require.NoError(err) - valAddr := common.HexToAddress(vals[1].GetOperator()) - lastBorBlockHeader := ðTypes.Header{Number: big.NewInt(0)} contractCaller.On("GetBorChainBlock", mock.Anything, big.NewInt(0)).Return(lastBorBlockHeader, nil).Times(1) - contractCaller.On("GetBorChainBlockAuthor", big.NewInt(0)).Return(&valAddr, nil).Times(1) + contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(0)). + Return(new(common.HexToAddress(vals[1].GetOperator())), nil).Times(1) stakeKeeper.EXPECT().GetValidatorSet(ctx).Return(valSet, nil).Times(1) stakeKeeper.EXPECT().GetSpanEligibleValidators(ctx).Return(vals).Times(1) @@ -100,7 +99,7 @@ func (s *KeeperTestSuite) TestGetNextSpanSeed() { lastBorBlockHeader := ðTypes.Header{Number: big.NewInt(1)} contractCaller.On("GetBorChainBlock", mock.Anything, big.NewInt(1)).Return(lastBorBlockHeader, nil).Times(1) - contractCaller.On("GetBorChainBlockAuthor", big.NewInt(1)).Return(&valAddr, nil).Times(1) + contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(1)).Return(&valAddr, nil).Times(1) res, err := queryClient.GetNextSpanSeed(ctx, &types.QueryNextSpanSeedRequest{Id: 1}) require.NoError(err) diff --git a/x/bor/keeper/keeper.go b/x/bor/keeper/keeper.go index a1851ecf9..3eb52d893 100644 --- a/x/bor/keeper/keeper.go +++ b/x/bor/keeper/keeper.go @@ -475,7 +475,7 @@ func (k *Keeper) getBorBlockForSpanSeed(ctx context.Context, seedSpan *types.Spa if proposedSpanID == 1 { borBlock = 1 - author, err = k.contractCaller.GetBorChainBlockAuthor(big.NewInt(int64(borBlock))) + author, err = k.contractCaller.GetBorChainBlockAuthor(ctx, big.NewInt(int64(borBlock))) if err != nil { logger.Error("Error fetching first block for span seed", "error", err, "block", borBlock) return 0, nil, err @@ -528,7 +528,7 @@ func (k *Keeper) getBorBlockForSpanSeed(ctx context.Context, seedSpan *types.Spa } for borBlock = seedSpan.EndBlock; borBlock >= seedSpan.StartBlock; borBlock -= borParams.SprintDuration { - author, err = k.contractCaller.GetBorChainBlockAuthor(big.NewInt(int64(borBlock))) + author, err = k.contractCaller.GetBorChainBlockAuthor(ctx, big.NewInt(int64(borBlock))) if err != nil { logger.Error("Error fetching block author from bor chain while calculating next span seed", "error", err, "block", borBlock) return 0, nil, err @@ -550,7 +550,7 @@ func (k *Keeper) getBorBlockForSpanSeed(ctx context.Context, seedSpan *types.Spa borBlock = seedSpan.EndBlock } - author, err = k.contractCaller.GetBorChainBlockAuthor(big.NewInt(int64(borBlock))) + author, err = k.contractCaller.GetBorChainBlockAuthor(ctx, big.NewInt(int64(borBlock))) if err != nil { logger.Error("Error fetching end block author from bor chain while calculating next span seed", "error", err, "block", borBlock) return 0, nil, err diff --git a/x/bor/keeper/keeper_test.go b/x/bor/keeper/keeper_test.go index 31a6cc019..79dc3f9da 100644 --- a/x/bor/keeper/keeper_test.go +++ b/x/bor/keeper/keeper_test.go @@ -370,25 +370,25 @@ func (s *KeeperTestSuite) TestFetchNextSpanSeed() { val3Addr := common.HexToAddress(vals[2].GetOperator()) seedBlock1 := spans[0].EndBlock - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(seedBlock1))).Return(&val2Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(seedBlock1))).Return(&val2Addr, nil) seedBlock2 := spans[1].EndBlock - borParams.SprintDuration - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(spans[1].EndBlock))).Return(&val2Addr, nil) - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(seedBlock2))).Return(&val1Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(spans[1].EndBlock))).Return(&val2Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(seedBlock2))).Return(&val1Addr, nil) for block := spans[1].EndBlock - (2 * borParams.SprintDuration); block >= spans[1].StartBlock; block -= borParams.SprintDuration { - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(block))).Return(&val1Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(block))).Return(&val1Addr, nil) } seedBlock3 := spans[2].EndBlock - (2 * borParams.SprintDuration) - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(spans[2].EndBlock))).Return(&val1Addr, nil) - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(spans[2].EndBlock-borParams.SprintDuration))).Return(&val2Addr, nil) - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(seedBlock3))).Return(&val3Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(spans[2].EndBlock))).Return(&val1Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(spans[2].EndBlock-borParams.SprintDuration))).Return(&val2Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(seedBlock3))).Return(&val3Addr, nil) seedBlock4 := spans[3].EndBlock - borParams.SprintDuration - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(spans[3].EndBlock))).Return(&val1Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(spans[3].EndBlock))).Return(&val1Addr, nil) for block := spans[3].EndBlock; block >= spans[3].StartBlock; block -= borParams.SprintDuration { - s.contractCaller.On("GetBorChainBlockAuthor", big.NewInt(int64(block))).Return(&val2Addr, nil) + s.contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(int64(block))).Return(&val2Addr, nil) } blockHeader1 := ethTypes.Header{Number: big.NewInt(int64(seedBlock1))} @@ -451,9 +451,7 @@ func (s *KeeperTestSuite) TestFetchNextSpanSeed() { lastSpanID = tc.lastSpanId } - v1A := val1Addr - - err = borKeeper.StoreSeedProducer(ctx, lastSpanID+1, &v1A) + err = borKeeper.StoreSeedProducer(ctx, lastSpanID+1, new(val1Addr)) require.NoError(err) for _, tc := range testcases { @@ -484,7 +482,7 @@ func (s *KeeperTestSuite) TestProposeSpanOne() { val1Addr := common.HexToAddress(vals[0].GetOperator()) seedBlock1 := int64(1) - contractCaller.On("GetBorChainBlockAuthor", big.NewInt(seedBlock1)).Return(&val1Addr, nil) + contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(seedBlock1)).Return(&val1Addr, nil) blockHeader1 := ethTypes.Header{Number: big.NewInt(seedBlock1)} blockHash1 := blockHeader1.Hash() diff --git a/x/bor/keeper/side_msg_server_test.go b/x/bor/keeper/side_msg_server_test.go index 4951586aa..25240ea8f 100644 --- a/x/bor/keeper/side_msg_server_test.go +++ b/x/bor/keeper/side_msg_server_test.go @@ -107,7 +107,7 @@ func (s *KeeperTestSuite) TestSideHandleMsgSpan() { expSeed: blockHash1, expVote: sidetxs.Vote_VOTE_NO, mockFn: func() { - contractCaller.On("GetBorChainBlockAuthor", mock.Anything).Return(&val1Addr, nil) + contractCaller.On("GetBorChainBlockAuthor", mock.Anything, mock.Anything).Return(&val1Addr, nil) contractCaller.On("GetBorChainBlock", mock.Anything, mock.Anything).Return(&blockHeader1, nil) }, }, @@ -143,7 +143,7 @@ func (s *KeeperTestSuite) TestSideHandleMsgSpan() { expSeed: blockHash1, expVote: sidetxs.Vote_VOTE_NO, mockFn: func() { - contractCaller.On("GetBorChainBlockAuthor", mock.Anything).Return(&val1Addr, nil) + contractCaller.On("GetBorChainBlockAuthor", mock.Anything, mock.Anything).Return(&val1Addr, nil) contractCaller.On("GetBorChainBlock", mock.Anything, big.NewInt(16656)).Return(&blockHeader1, nil).Times(1) }, }, @@ -163,7 +163,7 @@ func (s *KeeperTestSuite) TestSideHandleMsgSpan() { expSeed: blockHash2, expVote: sidetxs.Vote_VOTE_YES, mockFn: func() { - contractCaller.On("GetBorChainBlockAuthor", mock.Anything).Return(&val1Addr, nil) + contractCaller.On("GetBorChainBlockAuthor", mock.Anything, mock.Anything).Return(&val1Addr, nil) contractCaller.On("GetBorChainBlock", mock.Anything, mock.Anything).Return(&blockHeader2, nil) }, }, @@ -213,8 +213,8 @@ func (s *KeeperTestSuite) TestPostHandleMsgEventSpan() { lastBorBlockHeader := ðTypes.Header{Number: big.NewInt(0)} contractCaller.On("GetBorChainBlock", mock.Anything, big.NewInt(0)).Return(lastBorBlockHeader, nil).Times(1) - contractCaller.On("GetBorChainBlockAuthor", big.NewInt(0)).Return(&producer1, nil).Times(1) - contractCaller.On("GetBorChainBlockAuthor", big.NewInt(100)).Return(&producer2, nil).Times(1) + contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(0)).Return(&producer1, nil).Times(1) + contractCaller.On("GetBorChainBlockAuthor", mock.Anything, big.NewInt(100)).Return(&producer2, nil).Times(1) testChainParams := chainmanagertypes.DefaultParams() spans := s.genTestSpans(1)