From f2db06584aeafdd3d2c19de9057fde8b82d2062d Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Mon, 30 Mar 2026 19:57:03 +0700 Subject: [PATCH 1/3] chore: reqest attestation bounded --- extensions/tn_utils/precompiles.go | 132 +++++++++++++- internal/migrations/005-primitive-query.sql | 3 +- internal/migrations/006-composed-query.sql | 3 +- .../007-composed-query-derivate.sql | 3 +- internal/migrations/009-truflation-query.sql | 9 +- .../migrations/024-attestation-actions.sql | 7 + .../attestation_date_range_test.go | 161 ++++++++++++++++++ .../request_attestation_fee_test.go | 87 ++++++++++ 8 files changed, 391 insertions(+), 14 deletions(-) create mode 100644 tests/streams/attestation/attestation_date_range_test.go diff --git a/extensions/tn_utils/precompiles.go b/extensions/tn_utils/precompiles.go index 709031485..d53f10680 100644 --- a/extensions/tn_utils/precompiles.go +++ b/extensions/tn_utils/precompiles.go @@ -35,6 +35,7 @@ func buildPrecompile() precompiles.Precompile { encodeUintMethod("encode_uint64", 64), canonicalToDataPointsABIMethod(), forceLastArgFalseMethod(), + validateAttestationDateRangeMethod(), parseAttestationBooleanMethod(), computeAttestationHashMethod(), unpackQueryComponentsMethod(), @@ -93,18 +94,18 @@ func getCallerBytesMethod() precompiles.Method { if ctx == nil || ctx.TxContext == nil { return resultFn([]any{[]byte{}}) } - + // Return normalized address bytes instead of raw public key bytes. // Caller is the string identifier (hex address for EVM). caller := ctx.TxContext.Caller if strings.HasPrefix(caller, "0x") || strings.HasPrefix(caller, "0X") { caller = caller[2:] } - + if b, err := hex.DecodeString(caller); err == nil && len(b) == 20 { return resultFn([]any{b}) } - + // Fallback to Signer (public key) if not a hex address return resultFn([]any{ctx.TxContext.Signer}) }, @@ -127,11 +128,11 @@ func getLeaderHexMethod() precompiles.Method { if ctx == nil || ctx.TxContext == nil || ctx.TxContext.BlockContext == nil || ctx.TxContext.BlockContext.Proposer == nil { return resultFn([]any{""}) } - + // For prediction markets, we usually want the Ethereum address of the leader // to transfer fees via the bridge. pubkey := ctx.TxContext.BlockContext.Proposer - + if pubkey.Type() == crypto.KeyTypeSecp256k1 { // Manually unmarshal to ensure we have the concrete type secp, err := crypto.UnmarshalSecp256k1PublicKey(pubkey.Bytes()) @@ -140,7 +141,7 @@ func getLeaderHexMethod() precompiles.Method { return resultFn([]any{"0x" + hex.EncodeToString(addr)}) } } - + // Fallback to raw hex of the public key return resultFn([]any{"0x" + hex.EncodeToString(pubkey.Bytes())}) }, @@ -163,7 +164,7 @@ func getLeaderBytesMethod() precompiles.Method { if ctx == nil || ctx.TxContext == nil || ctx.TxContext.BlockContext == nil || ctx.TxContext.BlockContext.Proposer == nil { return resultFn([]any{[]byte{}}) } - + pubkey := ctx.TxContext.BlockContext.Proposer if pubkey.Type() == crypto.KeyTypeSecp256k1 { // Manually unmarshal to ensure we have the concrete type @@ -173,7 +174,7 @@ func getLeaderBytesMethod() precompiles.Method { return resultFn([]any{addr}) } } - + // Fallback to raw bytes of the public key return resultFn([]any{pubkey.Bytes()}) }, @@ -721,6 +722,121 @@ func forceLastArgFalseHandler(ctx *common.EngineContext, app *common.App, inputs return resultFn([]any{modifiedArgsBytes}) } +// MaxAttestationDateRangeSeconds is the maximum allowed date range for attestation +// queries (90 days). This prevents unbounded queries from scanning the entire +// primitive_events table during on-chain block execution. +// +// 90 days is generous for all legitimate attestation use cases: +// - Settlement only needs the latest value (single point) +// - Proof of history typically spans days or weeks, not years +// - 90 days of daily data = 90 rows, hourly = 2,160 rows (both safe) +const MaxAttestationDateRangeSeconds int64 = 90 * 24 * 60 * 60 // 7,776,000 seconds + +// validateAttestationDateRangeMethod checks that the date range in attestation +// query args does not exceed 90 days. Only applies to range-based actions +// (action_id 1-3: get_record, get_index, get_change_over_time) where args +// contain $from at index 2 and $to at index 3. +// +// Actions 4-5 (get_last_record, get_first_record) are single-point queries +// with LIMIT 1 and do not need date range validation. +// Actions 6-9 (binary) return a single boolean and are inherently safe. +func validateAttestationDateRangeMethod() precompiles.Method { + return precompiles.Method{ + Name: "validate_attestation_date_range", + AccessModifiers: []precompiles.Modifier{precompiles.VIEW, precompiles.PUBLIC}, + Parameters: []precompiles.PrecompileValue{ + precompiles.NewPrecompileValue("action_id", types.IntType, false), + precompiles.NewPrecompileValue("args_bytes", types.ByteaType, false), + }, + Returns: nil, // void — errors if invalid + Handler: validateAttestationDateRangeHandler, + } +} + +func validateAttestationDateRangeHandler(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + actionID, err := toInt64(inputs[0]) + if err != nil { + return fmt.Errorf("action_id: %w", err) + } + + // Only validate range-based actions (1-3: get_record, get_index, get_change_over_time). + // Actions 4-5 are single-point (LIMIT 1), actions 6-9 are binary (single bool). + if actionID < 1 || actionID > 3 { + return nil // no validation needed + } + + argsBytes, ok := inputs[1].([]byte) + if !ok { + return fmt.Errorf("args_bytes must be []byte, got %T", inputs[1]) + } + + args, err := DecodeActionArgs(argsBytes) + if err != nil { + return fmt.Errorf("failed to decode action args: %w", err) + } + + // Range-based actions have signature: ($data_provider, $stream_id, $from, $to, ...) + // $from is at index 2, $to is at index 3 + if len(args) < 4 { + return fmt.Errorf("range-based attestation action requires at least 4 args, got %d", len(args)) + } + + // If both from and to are nil, the action returns the latest record (LIMIT 1) — safe + fromVal := derefIntPtr(args[2]) + toVal := derefIntPtr(args[3]) + + if fromVal == nil && toVal == nil { + return nil + } + + // If only one is provided, the range is effectively unbounded — reject + if fromVal == nil || toVal == nil { + return fmt.Errorf("attestation queries with range-based actions (get_record, get_index) must specify both 'from' and 'to' parameters") + } + + fromTS, err := toInt64(*fromVal) + if err != nil { + return fmt.Errorf("failed to parse 'from' parameter: %w", err) + } + + toTS, err := toInt64(*toVal) + if err != nil { + return fmt.Errorf("failed to parse 'to' parameter: %w", err) + } + + dateRange := toTS - fromTS + if dateRange > MaxAttestationDateRangeSeconds { + return fmt.Errorf("attestation date range of %d seconds exceeds maximum of %d seconds (90 days)", dateRange, MaxAttestationDateRangeSeconds) + } + + return nil +} + +// derefIntPtr dereferences a pointer to an integer type, returning nil if the input is nil. +// DecodeActionArgs may return *int64 for nullable INT8 parameters. +func derefIntPtr(v any) *any { + if v == nil { + return nil + } + switch ptr := v.(type) { + case *int64: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + case *int: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + default: + // Not a pointer — return as-is + return &v + } +} + // parseAttestationBooleanMethod extracts a boolean result from an attestation's // result_canonical field. This is used for prediction market settlement where // attestations return boolean outcomes (YES=true, NO=false). diff --git a/internal/migrations/005-primitive-query.sql b/internal/migrations/005-primitive-query.sql index f248fbd45..a8814f492 100644 --- a/internal/migrations/005-primitive-query.sql +++ b/internal/migrations/005-primitive-query.sql @@ -86,7 +86,8 @@ CREATE OR REPLACE ACTION get_record_primitive( ) -- Final selection with fallback SELECT event_time, value FROM combined_results - ORDER BY event_time ASC; + ORDER BY event_time ASC + LIMIT 10000; }; /** diff --git a/internal/migrations/006-composed-query.sql b/internal/migrations/006-composed-query.sql index adda0ff91..2194c911f 100644 --- a/internal/migrations/006-composed-query.sql +++ b/internal/migrations/006-composed-query.sql @@ -677,5 +677,6 @@ RETURNS TABLE( SELECT event_time, value FROM anchor_hit ) SELECT DISTINCT event_time, value FROM result - ORDER BY 1; + ORDER BY 1 + LIMIT 10000; }; diff --git a/internal/migrations/007-composed-query-derivate.sql b/internal/migrations/007-composed-query-derivate.sql index e44e38b3d..b2fbf35cc 100644 --- a/internal/migrations/007-composed-query-derivate.sql +++ b/internal/migrations/007-composed-query-derivate.sql @@ -964,7 +964,8 @@ RETURNS TABLE( SELECT event_time, value FROM anchor_hit ) SELECT DISTINCT event_time, value FROM result - ORDER BY 1; + ORDER BY 1 + LIMIT 10000; }; diff --git a/internal/migrations/009-truflation-query.sql b/internal/migrations/009-truflation-query.sql index a8c985c61..50541c0b9 100644 --- a/internal/migrations/009-truflation-query.sql +++ b/internal/migrations/009-truflation-query.sql @@ -240,7 +240,8 @@ CREATE OR REPLACE ACTION truflation_get_record_primitive( ) -- Final selection with fallback SELECT event_time, value FROM combined_results - ORDER BY event_time ASC; + ORDER BY event_time ASC + LIMIT 10000; }; /** @@ -1221,7 +1222,8 @@ RETURNS TABLE( SELECT event_time, value FROM anchor_hit ) SELECT DISTINCT event_time, value FROM result - ORDER BY 1; + ORDER BY 1 + LIMIT 10000; }; CREATE OR REPLACE ACTION truflation_last_rc_composed( @@ -2174,5 +2176,6 @@ RETURNS TABLE( SELECT event_time, value FROM anchor_hit ) SELECT DISTINCT event_time, value FROM result - ORDER BY 1; + ORDER BY 1 + LIMIT 10000; }; diff --git a/internal/migrations/024-attestation-actions.sql b/internal/migrations/024-attestation-actions.sql index b027b3314..22c81f98e 100644 --- a/internal/migrations/024-attestation-actions.sql +++ b/internal/migrations/024-attestation-actions.sql @@ -103,6 +103,13 @@ CREATE OR REPLACE ACTION request_attestation( } $stream_bytes := $stream_id::BYTEA; + -- Validate date range for range-based attestation actions (IDs 1-3) BEFORE + -- executing the query. This prevents unbounded queries from scanning the entire + -- primitive_events table during block execution. Max range: 90 days. + -- This check runs before call_dispatch to reject expensive queries early, + -- before kwil-db buffers all result rows into memory. + tn_utils.validate_attestation_date_range($action_id, $args_bytes); + -- Force deterministic execution by overriding non-deterministic parameters. -- Query actions (IDs 1-5) all have use_cache as their last parameter. -- Force use_cache=false to ensure all validators compute identical results diff --git a/tests/streams/attestation/attestation_date_range_test.go b/tests/streams/attestation/attestation_date_range_test.go new file mode 100644 index 000000000..5d629e546 --- /dev/null +++ b/tests/streams/attestation/attestation_date_range_test.go @@ -0,0 +1,161 @@ +//go:build kwiltest + +package tests + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + kwilTypes "github.com/trufnetwork/kwil-db/core/types" + erc20bridge "github.com/trufnetwork/kwil-db/node/exts/erc20-bridge/erc20" + kwilTesting "github.com/trufnetwork/kwil-db/testing" + "github.com/trufnetwork/node/extensions/tn_utils" + "github.com/trufnetwork/node/internal/migrations" + testutils "github.com/trufnetwork/node/tests/streams/utils" + "github.com/trufnetwork/node/tests/streams/utils/setup" + "github.com/trufnetwork/sdk-go/core/types" + "github.com/trufnetwork/sdk-go/core/util" +) + +func TestAttestationDateRangeValidation(t *testing.T) { + testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ + Name: "ATTESTATION_DATE_RANGE_Validation", + SeedStatements: migrations.GetSeedScriptStatements(), + FunctionTests: []kwilTesting.TestFunc{ + testAllDateRangeValidations(t), + }, + }, testutils.GetTestOptionsWithCache()) +} + +func testAllDateRangeValidations(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + // === Setup === + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + platform.Deployer = systemAdmin.Bytes() + streamID := "st000000000000000000000000000000" + + // Grant roles and create stream + err := setup.AddMemberToRoleBypass(ctx, platform, "system", "network_writers_manager", systemAdmin.Address()) + require.NoError(t, err) + + err = setup.CreateDataProvider(ctx, platform, systemAdmin.Address()) + require.NoError(t, err) + + streamLocator := types.StreamLocator{ + StreamId: util.GenerateStreamId(streamID), + DataProvider: systemAdmin, + } + err = setup.CreateStream(ctx, platform, setup.StreamInfo{ + Type: setup.ContractTypePrimitive, + Locator: streamLocator, + }) + require.NoError(t, err) + + // Initialize ERC20 and give balance for attestation fees + err = erc20bridge.ForTestingInitializeExtension(ctx, platform) + require.NoError(t, err) + + err = giveAttestationBalance(ctx, platform, systemAdmin.Address(), "1000000000000000000000") + require.NoError(t, err) + + // Insert a data point for binary action test + err = insertTestDataPoint(ctx, platform, &systemAdmin, streamID, 1000000, "75.000000000000000000") + require.NoError(t, err) + + // === Test 1: 30-day range should succeed === + t.Log("Test 1: get_record with 30-day range should succeed") + from30 := int64(1000000) + to30 := int64(1000000 + 30*24*60*60) + err = requestAttestationWithTimeRange(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_record", from30, to30) + require.NoError(t, err, "30-day range should succeed") + + // === Test 2: 180-day range should fail === + t.Log("Test 2: get_record with 180-day range should fail") + from180 := int64(1000000) + to180 := int64(1000000 + 180*24*60*60) + err = requestAttestationWithTimeRange(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_record", from180, to180) + require.Error(t, err, "180-day range should fail") + require.Contains(t, err.Error(), "exceeds maximum", "should mention exceeding maximum") + + // === Test 3: Both from and to nil should succeed (latest record, safe) === + t.Log("Test 3: get_record with both null dates should succeed") + nullArgs := []any{ + systemAdmin.Address(), streamID, + nil, nil, // from, to both nil + nil, false, // frozen_at, use_cache + } + argsBytes, err := tn_utils.EncodeActionArgs(nullArgs) + require.NoError(t, err) + err = requestAttestationWithArgsBytes(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_record", argsBytes) + require.NoError(t, err, "both null dates should succeed (returns latest record)") + + // === Test 4: Only one date param should fail (unbounded range) === + t.Log("Test 4: get_record with only 'to' should fail") + oneNullArgs := []any{ + systemAdmin.Address(), streamID, + nil, int64(99999), // from=nil (unbounded), to=specified + nil, false, + } + argsBytes, err = tn_utils.EncodeActionArgs(oneNullArgs) + require.NoError(t, err) + err = requestAttestationWithArgsBytes(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_record", argsBytes) + require.Error(t, err, "one null date should fail") + require.Contains(t, err.Error(), "must specify both", "should require both from and to") + + // === Test 5: Exactly 90 days should succeed (boundary) === + t.Log("Test 5: get_record with exactly 90-day range should succeed") + from90 := int64(1000000) + to90 := int64(1000000 + 90*24*60*60) + err = requestAttestationWithTimeRange(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_record", from90, to90) + require.NoError(t, err, "exactly 90-day range should succeed") + + // === Test 6: 91 days should fail (just over boundary) === + t.Log("Test 6: get_record with 91-day range should fail") + from91 := int64(1000000) + to91 := int64(1000000 + 91*24*60*60) + err = requestAttestationWithTimeRange(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_record", from91, to91) + require.Error(t, err, "91-day range should fail") + require.Contains(t, err.Error(), "exceeds maximum") + + // === Test 7: Binary action (price_above_threshold) skips validation === + t.Log("Test 7: binary action should skip date range validation") + thresholdVal, err := kwilTypes.ParseDecimal("50.000000000000000000") + require.NoError(t, err) + thresholdVal.SetPrecisionAndScale(36, 18) + binaryArgs := []any{ + systemAdmin.Address(), streamID, + int64(1000000), // timestamp + thresholdVal, // threshold as NUMERIC(36,18) + nil, // frozen_at + } + argsBytes, err = tn_utils.EncodeActionArgs(binaryArgs) + require.NoError(t, err) + err = requestAttestationWithArgsBytes(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "price_above_threshold", argsBytes) + require.NoError(t, err, "binary action should skip date range validation") + + // === Test 8: get_index with 180-day range should also fail === + t.Log("Test 8: get_index with 180-day range should fail") + err = requestAttestationWithTimeRange(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_index", from180, to180) + require.Error(t, err, "get_index with 180-day range should fail") + require.Contains(t, err.Error(), "exceeds maximum") + + // === Test 9: get_last_record (action_id=4) should skip validation === + // Signature: get_last_record($data_provider TEXT, $stream_id TEXT, $before INT8, $frozen_at INT8) + t.Log("Test 9: get_last_record should skip date range validation") + lastRecordArgs := []any{ + systemAdmin.Address(), streamID, + nil, // before + nil, // frozen_at + false, // use_cache + } + argsBytes, err = tn_utils.EncodeActionArgs(lastRecordArgs) + require.NoError(t, err) + err = requestAttestationWithArgsBytes(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_last_record", argsBytes) + require.NoError(t, err, "get_last_record should skip date range validation") + + fmt.Println("All date range validation tests passed") + return nil + } +} diff --git a/tests/streams/attestation/request_attestation_fee_test.go b/tests/streams/attestation/request_attestation_fee_test.go index d6951fc47..79080f3d1 100644 --- a/tests/streams/attestation/request_attestation_fee_test.go +++ b/tests/streams/attestation/request_attestation_fee_test.go @@ -13,6 +13,7 @@ import ( "github.com/trufnetwork/kwil-db/common" "github.com/trufnetwork/kwil-db/core/crypto" coreauth "github.com/trufnetwork/kwil-db/core/crypto/auth" + kwilTypes "github.com/trufnetwork/kwil-db/core/types" kwilTesting "github.com/trufnetwork/kwil-db/testing" "github.com/trufnetwork/node/extensions/tn_utils" "github.com/trufnetwork/node/internal/migrations" @@ -434,3 +435,89 @@ func requestAttestationWithTimeRange(ctx context.Context, platform *kwilTesting. func requestAttestationWithLeader(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, leaderPub *crypto.Secp256k1PublicKey, dataProvider string, streamID string, actionName string) error { return callRequestAttestationAction(ctx, platform, signer, leaderPub, dataProvider, streamID, actionName) } + +// requestAttestationWithArgsBytes requests attestation with pre-encoded args bytes +func requestAttestationWithArgsBytes(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, dataProvider string, streamID string, actionName string, argsBytes []byte) error { + _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) + if err != nil { + return err + } + pub := pubGeneric.(*crypto.Secp256k1PublicKey) + + tx := &common.TxContext{ + Ctx: ctx, + BlockContext: &common.BlockContext{ + Height: 1, + Proposer: pub, + }, + Signer: signer.Bytes(), + Caller: signer.Address(), + TxID: platform.Txid(), + Authenticator: coreauth.EthPersonalSignAuth, + } + engineCtx := &common.EngineContext{TxContext: tx} + + res, err := platform.Engine.Call( + engineCtx, + platform.DB, + "", + "request_attestation", + []any{ + dataProvider, + streamID, + actionName, + argsBytes, + false, // encrypt_sig + nil, // max_fee + }, + func(row *common.Row) error { return nil }, + ) + if err != nil { + return err + } + if res != nil && res.Error != nil { + return res.Error + } + return nil +} + +// insertTestDataPoint inserts a single data point into a primitive stream +func insertTestDataPoint(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, streamID string, eventTime int64, value string) error { + tx := &common.TxContext{ + Ctx: ctx, + BlockContext: &common.BlockContext{Height: 1}, + Signer: signer.Bytes(), + Caller: signer.Address(), + TxID: platform.Txid(), + Authenticator: coreauth.EthPersonalSignAuth, + } + engineCtx := &common.EngineContext{TxContext: tx} + + decVal, err := kwilTypes.ParseDecimal(value) + if err != nil { + return fmt.Errorf("failed to parse decimal value: %w", err) + } + // Set precision to match NUMERIC(36,18) expected by insert_records + decVal.SetPrecisionAndScale(36, 18) + + res, err := platform.Engine.Call( + engineCtx, + platform.DB, + "", + "insert_records", + []any{ + []string{signer.Address()}, + []string{streamID}, + []int64{eventTime}, + []*kwilTypes.Decimal{decVal}, + }, + func(row *common.Row) error { return nil }, + ) + if err != nil { + return err + } + if res != nil && res.Error != nil { + return res.Error + } + return nil +} From a06417940b31b63ef63a72480a10daad73c6010d Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Mon, 30 Mar 2026 20:52:19 +0700 Subject: [PATCH 2/3] chore: apply suggestion --- extensions/tn_utils/precompiles.go | 5 ++++- .../attestation/attestation_date_range_test.go | 17 ++++++++++++++--- .../attestation/request_attestation_fee_test.go | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/extensions/tn_utils/precompiles.go b/extensions/tn_utils/precompiles.go index d53f10680..ae3e18fae 100644 --- a/extensions/tn_utils/precompiles.go +++ b/extensions/tn_utils/precompiles.go @@ -791,7 +791,7 @@ func validateAttestationDateRangeHandler(ctx *common.EngineContext, app *common. // If only one is provided, the range is effectively unbounded — reject if fromVal == nil || toVal == nil { - return fmt.Errorf("attestation queries with range-based actions (get_record, get_index) must specify both 'from' and 'to' parameters") + return fmt.Errorf("attestation queries with range-based actions (get_record, get_index, get_change_over_time) must specify both 'from' and 'to' parameters") } fromTS, err := toInt64(*fromVal) @@ -805,6 +805,9 @@ func validateAttestationDateRangeHandler(ctx *common.EngineContext, app *common. } dateRange := toTS - fromTS + if dateRange < 0 { + return fmt.Errorf("attestation date range invalid: 'from' (%d) must be less than or equal to 'to' (%d)", fromTS, toTS) + } if dateRange > MaxAttestationDateRangeSeconds { return fmt.Errorf("attestation date range of %d seconds exceeds maximum of %d seconds (90 days)", dateRange, MaxAttestationDateRangeSeconds) } diff --git a/tests/streams/attestation/attestation_date_range_test.go b/tests/streams/attestation/attestation_date_range_test.go index 5d629e546..d0ac2ef84 100644 --- a/tests/streams/attestation/attestation_date_range_test.go +++ b/tests/streams/attestation/attestation_date_range_test.go @@ -4,7 +4,6 @@ package tests import ( "context" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -142,7 +141,7 @@ func testAllDateRangeValidations(t *testing.T) func(ctx context.Context, platfor require.Contains(t, err.Error(), "exceeds maximum") // === Test 9: get_last_record (action_id=4) should skip validation === - // Signature: get_last_record($data_provider TEXT, $stream_id TEXT, $before INT8, $frozen_at INT8) + // Signature: get_last_record($data_provider TEXT, $stream_id TEXT, $before INT8, $frozen_at INT8, $use_cache BOOL) t.Log("Test 9: get_last_record should skip date range validation") lastRecordArgs := []any{ systemAdmin.Address(), streamID, @@ -155,7 +154,19 @@ func testAllDateRangeValidations(t *testing.T) func(ctx context.Context, platfor err = requestAttestationWithArgsBytes(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_last_record", argsBytes) require.NoError(t, err, "get_last_record should skip date range validation") - fmt.Println("All date range validation tests passed") + // === Test 10: from == to (single-point query, dateRange=0) should succeed === + t.Log("Test 10: get_record with from == to should succeed") + samePoint := int64(1000000) + err = requestAttestationWithTimeRange(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_record", samePoint, samePoint) + require.NoError(t, err, "single-point query (from == to) should succeed") + + // === Test 11: from > to (negative range) should fail === + t.Log("Test 11: get_record with from > to should fail") + err = requestAttestationWithTimeRange(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_record", int64(2000000), int64(1000000)) + require.Error(t, err, "negative range (from > to) should fail") + require.Contains(t, err.Error(), "must be less than or equal", "should reject inverted range") + + t.Log("All date range validation tests passed") return nil } } diff --git a/tests/streams/attestation/request_attestation_fee_test.go b/tests/streams/attestation/request_attestation_fee_test.go index 79080f3d1..80989c81b 100644 --- a/tests/streams/attestation/request_attestation_fee_test.go +++ b/tests/streams/attestation/request_attestation_fee_test.go @@ -463,7 +463,7 @@ func requestAttestationWithArgsBytes(ctx context.Context, platform *kwilTesting. "", "request_attestation", []any{ - dataProvider, + strings.ToLower(dataProvider), streamID, actionName, argsBytes, From d2e7eb0980e2aa565e1a26f35711fa433eed68d5 Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Mon, 30 Mar 2026 21:32:55 +0700 Subject: [PATCH 3/3] chore: apply suggestion --- extensions/tn_utils/precompiles.go | 55 ++++++++++++++++++- .../attestation_date_range_test.go | 6 ++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/extensions/tn_utils/precompiles.go b/extensions/tn_utils/precompiles.go index ae3e18fae..9a131d96f 100644 --- a/extensions/tn_utils/precompiles.go +++ b/extensions/tn_utils/precompiles.go @@ -815,8 +815,9 @@ func validateAttestationDateRangeHandler(ctx *common.EngineContext, app *common. return nil } -// derefIntPtr dereferences a pointer to an integer type, returning nil if the input is nil. -// DecodeActionArgs may return *int64 for nullable INT8 parameters. +// derefIntPtr dereferences a pointer to any integer type, returning nil if the +// input is nil or a nil pointer. DecodeActionArgs may return pointer variants +// (*int64, *int32, *int, *uint64, etc.) for nullable parameters. func derefIntPtr(v any) *any { if v == nil { return nil @@ -834,8 +835,56 @@ func derefIntPtr(v any) *any { } val := any(*ptr) return &val + case *int32: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + case *int16: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + case *int8: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + case *uint64: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + case *uint32: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + case *uint16: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + case *uint8: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val + case *uint: + if ptr == nil { + return nil + } + val := any(*ptr) + return &val default: - // Not a pointer — return as-is + // Not a pointer — return as-is (toInt64 will handle value types) return &v } } diff --git a/tests/streams/attestation/attestation_date_range_test.go b/tests/streams/attestation/attestation_date_range_test.go index d0ac2ef84..d79e827d1 100644 --- a/tests/streams/attestation/attestation_date_range_test.go +++ b/tests/streams/attestation/attestation_date_range_test.go @@ -140,6 +140,12 @@ func testAllDateRangeValidations(t *testing.T) func(ctx context.Context, platfor require.Error(t, err, "get_index with 180-day range should fail") require.Contains(t, err.Error(), "exceeds maximum") + // === Test 8b: get_change_over_time with 180-day range should also fail === + t.Log("Test 8b: get_change_over_time with 180-day range should fail") + err = requestAttestationWithTimeRange(ctx, platform, &systemAdmin, systemAdmin.Address(), streamID, "get_change_over_time", from180, to180) + require.Error(t, err, "get_change_over_time with 180-day range should fail") + require.Contains(t, err.Error(), "exceeds maximum") + // === Test 9: get_last_record (action_id=4) should skip validation === // Signature: get_last_record($data_provider TEXT, $stream_id TEXT, $before INT8, $frozen_at INT8, $use_cache BOOL) t.Log("Test 9: get_last_record should skip date range validation")