Skip to content

Commit 54a39a0

Browse files
authored
NONEVM-3227: logpoller defensive validations (#452)
* fix: logpoller defensive validations * fix: simplify * chore: better test pattern
1 parent 897a04f commit 54a39a0

File tree

7 files changed

+147
-1
lines changed

7 files changed

+147
-1
lines changed

pkg/logpoller/block.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"time"
88

9+
"github.com/xssnick/tonutils-go/address"
910
"github.com/xssnick/tonutils-go/ton"
1011

1112
"github.com/smartcontractkit/chainlink-ton/pkg/logpoller/models"
@@ -24,6 +25,13 @@ func (lp *service) getMasterchainBlockRange(ctx context.Context) (*models.BlockR
2425
return nil, fmt.Errorf("failed to get current masterchain info: %w", err)
2526
}
2627

28+
// validate that the returned block belongs to the masterchain.
29+
// a compromised or faulty liteserver could return valid blocks from the wrong workchain,
30+
// which would cause the logpoller to track incorrect chain data.
31+
if toBlock.Workchain != address.MasterchainID {
32+
return nil, fmt.Errorf("expected masterchain block (workchain %d), got workchain %d", address.MasterchainID, toBlock.Workchain)
33+
}
34+
2735
lastProcessedBlock, err := lp.getLastProcessedBlock(toBlock)
2836
if err != nil {
2937
return nil, fmt.Errorf("failed to get last processed block: %w", err)

pkg/logpoller/block_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,66 @@
11
package logpoller
22

33
import (
4+
"context"
45
"testing"
56
"time"
67

78
"github.com/stretchr/testify/require"
9+
10+
"github.com/xssnick/tonutils-go/address"
11+
"github.com/xssnick/tonutils-go/ton"
12+
13+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
814
)
915

16+
// mockAPIClient is a minimal mock for ton.APIClientWrapped used in unit tests
17+
type mockAPIClient struct {
18+
ton.APIClientWrapped // embed to satisfy interface
19+
masterchainInfo *ton.BlockIDExt
20+
masterchainErr error
21+
}
22+
23+
func (m *mockAPIClient) CurrentMasterchainInfo(_ context.Context) (*ton.BlockIDExt, error) {
24+
return m.masterchainInfo, m.masterchainErr
25+
}
26+
27+
func TestGetMasterchainBlockRange_WorkchainValidation(t *testing.T) {
28+
t.Run("rejects non-masterchain workchain", func(t *testing.T) {
29+
mock := &mockAPIClient{
30+
masterchainInfo: &ton.BlockIDExt{Workchain: 0, SeqNo: 100}, // workchain 0 is base chain, not masterchain
31+
}
32+
33+
lp := &service{
34+
lggr: logger.Sugared(logger.Nop()),
35+
clientProvider: func(_ context.Context) (ton.APIClientWrapped, error) {
36+
return mock, nil
37+
},
38+
}
39+
40+
_, err := lp.getMasterchainBlockRange(context.Background())
41+
require.Error(t, err)
42+
})
43+
44+
t.Run("accepts masterchain workchain", func(t *testing.T) {
45+
mock := &mockAPIClient{
46+
masterchainInfo: &ton.BlockIDExt{Workchain: address.MasterchainID, SeqNo: 100},
47+
}
48+
49+
lp := &service{
50+
lggr: logger.Sugared(logger.Nop()),
51+
clientProvider: func(_ context.Context) (ton.APIClientWrapped, error) {
52+
return mock, nil
53+
},
54+
lastProcessedBlock: 100, // same as SeqNo, so no new blocks
55+
}
56+
57+
// should return nil (no new blocks) without error
58+
blockRange, err := lp.getMasterchainBlockRange(context.Background())
59+
require.NoError(t, err)
60+
require.Nil(t, blockRange, "no new blocks when seqno matches lastProcessedBlock")
61+
})
62+
}
63+
1064
func TestComputeLookbackWindow(t *testing.T) {
1165
t.Run("Basic lookback calculation", func(t *testing.T) {
1266
currentSeqNo := uint32(1000)

pkg/logpoller/loader/loader.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ func (l *rawTxLoader) listTransactionsWithBlock(ctx context.Context, addr *addre
244244
return nil, nil, fmt.Errorf("failed to parse cell from transaction bytes: %w", err)
245245
}
246246

247+
if err = validateTransactionListResponse(len(txList), len(t.IDs), limit); err != nil {
248+
return nil, nil, err
249+
}
250+
247251
resTxs := make([]*tlb.Transaction, len(txList))
248252
resBlocks := make([]*ton.BlockIDExt, len(txList))
249253

@@ -289,3 +293,15 @@ func (l *rawTxLoader) listTransactionsWithBlock(ctx context.Context, addr *addre
289293

290294
return nil, nil, errors.New("unknown response type")
291295
}
296+
297+
// validateTransactionListResponse validates liteserver response to prevent DoS attacks.
298+
// checks that response doesn't exceed requested limit and that block IDs array matches transaction count (runtime panic prevention).
299+
func validateTransactionListResponse(txCount, idsCount int, limit uint32) error {
300+
if txCount > int(limit) {
301+
return fmt.Errorf("liteserver returned %d transactions, exceeding requested limit %d", txCount, limit)
302+
}
303+
if idsCount != txCount {
304+
return fmt.Errorf("block IDs count (%d) does not match transaction count (%d)", idsCount, txCount)
305+
}
306+
return nil
307+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package loader
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestValidateTransactionListResponse(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
txCount int
13+
idsCount int
14+
limit uint32
15+
expectErr bool
16+
}{
17+
{"valid response within limit", 10, 10, 100, false},
18+
{"response at exact limit", 100, 100, 100, false},
19+
{"response exceeding limit", 101, 101, 100, true},
20+
{"mismatched IDs count (fewer)", 10, 5, 100, true},
21+
{"mismatched IDs count (more)", 5, 10, 100, true},
22+
}
23+
24+
for _, tt := range tests {
25+
t.Run(tt.name, func(t *testing.T) {
26+
err := validateTransactionListResponse(tt.txCount, tt.idsCount, tt.limit)
27+
if tt.expectErr {
28+
require.Error(t, err)
29+
} else {
30+
require.NoError(t, err)
31+
}
32+
})
33+
}
34+
}

pkg/logpoller/parser.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ func (lp *service) parseTx(tx *tlb.Transaction, block *ton.BlockIDExt, chainID s
6666
return nil, errors.New("transaction is nil")
6767
}
6868

69+
// validate block metadata is present - required for log storage
70+
if block == nil {
71+
return nil, errors.New("block is nil")
72+
}
73+
6974
if tx.IO.Out == nil {
7075
// this should never happen, since we filter out transactions without output messages in the loader
7176
return nil, errors.New("transaction has no output messages")

pkg/logpoller/parser_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package logpoller
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"github.com/xssnick/tonutils-go/tlb"
8+
"github.com/xssnick/tonutils-go/ton"
9+
10+
"github.com/smartcontractkit/chainlink-ton/pkg/logpoller/models"
11+
)
12+
13+
func TestParseTxValidation(t *testing.T) {
14+
// minimal service for testing - only need to test nil checks
15+
lp := &service{}
16+
filterIndex := models.FilterIndex{}
17+
18+
t.Run("rejects nil transaction", func(t *testing.T) {
19+
_, err := lp.parseTx(nil, &ton.BlockIDExt{}, "chainID", filterIndex)
20+
require.Error(t, err)
21+
})
22+
23+
t.Run("rejects nil block", func(t *testing.T) {
24+
tx := &tlb.Transaction{}
25+
_, err := lp.parseTx(tx, nil, "chainID", filterIndex)
26+
require.Error(t, err)
27+
})
28+
}

pkg/ton/tvm/wallet.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"time"
99

10+
"github.com/xssnick/tonutils-go/address"
1011
"github.com/xssnick/tonutils-go/ton"
1112
"github.com/xssnick/tonutils-go/ton/wallet"
1213
)
@@ -70,7 +71,7 @@ func MyLocalTONWalletDefault(client ton.APIClientWrapped) (*wallet.Wallet, error
7071
if err != nil {
7172
return nil, fmt.Errorf("failed to create highload wallet: %w", err)
7273
}
73-
mcFunderWallet, err := wallet.FromPrivateKeyWithOptions(client, rawHlWallet.PrivateKey(), walletVersion, wallet.WithWorkchain(-1))
74+
mcFunderWallet, err := wallet.FromPrivateKeyWithOptions(client, rawHlWallet.PrivateKey(), walletVersion, wallet.WithWorkchain(int8(address.MasterchainID)))
7475
if err != nil {
7576
return nil, fmt.Errorf("failed to create highload wallet: %w", err)
7677
}

0 commit comments

Comments
 (0)