From 8c7ba04635242eee16a253d16b930b8c5f72b675 Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Tue, 4 Nov 2025 17:45:41 +0700 Subject: [PATCH] chore: fix max_fee type mismatch in request_attestation action The `request_attestation` action previously used `INT8` for the `$max_fee` parameter, limiting fee caps to ~9.2 TRUF (9,223,372,036,854,775,807 wei). Since the attestation fee is 40 TRUF (40,000,000,000,000,000,000 wei), users could not set meaningful max_fee limits. This change updates the parameter type to `NUMERIC(78, 0)` for consistency with the token amount system and implements proper validation logic. resolves: https://github.com/trufnetwork/truf-network/issues/1323 --- .../migrations/024-attestation-actions.sql | 10 +- .../attestation/attestation_max_fee_test.go | 255 ++++++++++++++++++ .../attestation/attestation_request_test.go | 4 +- .../request_attestation_fee_test.go | 4 +- tests/streams/attestation/test_helpers.go | 4 +- 5 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 tests/streams/attestation/attestation_max_fee_test.go diff --git a/internal/migrations/024-attestation-actions.sql b/internal/migrations/024-attestation-actions.sql index b22d923e4..c333f9e45 100644 --- a/internal/migrations/024-attestation-actions.sql +++ b/internal/migrations/024-attestation-actions.sql @@ -17,7 +17,7 @@ CREATE OR REPLACE ACTION request_attestation( $action_name TEXT, $args_bytes BYTEA, $encrypt_sig BOOLEAN, -$max_fee INT8 + $max_fee NUMERIC(78, 0) ) PUBLIC RETURNS (request_tx_id TEXT, attestation_hash BYTEA) { -- Capture transaction ID for primary key $request_tx_id := @txid; @@ -52,6 +52,14 @@ $max_fee INT8 -- ===== FEE COLLECTION ===== -- Collect 40 TRUF flat fee for attestation request $attestation_fee := '40000000000000000000'::NUMERIC(78, 0); -- 40 TRUF with 18 decimals + + -- Validate max_fee if provided + IF $max_fee IS NOT NULL AND $max_fee > 0::NUMERIC(78, 0) { + IF $attestation_fee > $max_fee { + ERROR('Attestation fee (40 TRUF) exceeds caller max_fee limit: ' || ($max_fee / 1000000000000000000::NUMERIC(78, 0))::TEXT || ' TRUF'); + } + } + $caller_balance := ethereum_bridge.balance(@caller); IF $caller_balance < $attestation_fee { diff --git a/tests/streams/attestation/attestation_max_fee_test.go b/tests/streams/attestation/attestation_max_fee_test.go new file mode 100644 index 000000000..f1f95f8f1 --- /dev/null +++ b/tests/streams/attestation/attestation_max_fee_test.go @@ -0,0 +1,255 @@ +//go:build kwiltest + +package tests + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/trufnetwork/kwil-db/common" + "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" + testutils "github.com/trufnetwork/node/tests/streams/utils" +) + +// TestMaxFeeValidation verifies that max_fee parameter works correctly +func TestMaxFeeValidation(t *testing.T) { + const testActionName = "test_max_fee_action" + addrs := NewTestAddresses() + + testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ + Name: "ATTESTATION_MAX_FEE_Validation", + SeedStatements: migrations.GetSeedScriptStatements(), + Owner: addrs.Owner.Address(), + FunctionTests: []kwilTesting.TestFunc{ + func(ctx context.Context, platform *kwilTesting.Platform) error { + platform.Deployer = addrs.Owner.Bytes() + helper := NewAttestationTestHelper(t, ctx, platform) + + require.NoError(t, helper.SetupTestAction(testActionName, TestActionIDRequest)) + + t.Run("MaxFeeNull_Success", func(t *testing.T) { + testMaxFeeNull(t, helper, testActionName) + }) + + t.Run("MaxFeeZero_Success", func(t *testing.T) { + testMaxFeeZero(t, helper, testActionName) + }) + + t.Run("MaxFeeExactly40TRUF_Success", func(t *testing.T) { + testMaxFeeExactly40TRUF(t, helper, testActionName) + }) + + t.Run("MaxFeeAbove40TRUF_Success", func(t *testing.T) { + testMaxFeeAbove40TRUF(t, helper, testActionName) + }) + + t.Run("MaxFeeBelow40TRUF_Fails", func(t *testing.T) { + testMaxFeeBelow40TRUF(t, helper, testActionName) + }) + + t.Run("MaxFeeVeryLarge_Success", func(t *testing.T) { + testMaxFeeVeryLarge(t, helper, testActionName) + }) + + t.Run("MaxFeeNegative_Success", func(t *testing.T) { + testMaxFeeNegative(t, helper, testActionName) + }) + + return nil + }, + }, + }, testutils.GetTestOptionsWithCache()) +} + +// testMaxFeeNull verifies that NULL max_fee allows attestation (no limit) +func testMaxFeeNull(t *testing.T, h *AttestationTestHelper, actionName string) { + argsBytes, err := tn_utils.EncodeActionArgs([]any{int64(42)}) + require.NoError(t, err, "encode action args") + + engineCtx := h.NewEngineContext() + + var requestTxID string + res, err := h.platform.Engine.Call(engineCtx, h.platform.DB, "", "request_attestation", []any{ + TestDataProviderHex, + TestStreamID, + actionName, + argsBytes, + false, + nil, // max_fee = NULL (no limit) + }, func(row *common.Row) error { + requestTxID = row.Values[0].(string) + return nil + }) + + require.NoError(t, err, "call should not error at engine level") + require.Nil(t, res.Error, "NULL max_fee should allow attestation") + require.NotEmpty(t, requestTxID, "should return request_tx_id") +} + +// testMaxFeeZero verifies that zero max_fee allows attestation (treated as no limit) +func testMaxFeeZero(t *testing.T, h *AttestationTestHelper, actionName string) { + argsBytes, err := tn_utils.EncodeActionArgs([]any{int64(43)}) + require.NoError(t, err, "encode action args") + + engineCtx := h.NewEngineContext() + + maxFee := types.MustParseDecimalExplicit("0", 78, 0) // max_fee = 0 (treated as no limit per validation logic) + + var requestTxID string + res, err := h.platform.Engine.Call(engineCtx, h.platform.DB, "", "request_attestation", []any{ + TestDataProviderHex, + TestStreamID, + actionName, + argsBytes, + false, + maxFee, + }, func(row *common.Row) error { + requestTxID = row.Values[0].(string) + return nil + }) + + require.NoError(t, err, "call should not error at engine level") + require.Nil(t, res.Error, "Zero max_fee should be ignored (treated as no limit)") + require.NotEmpty(t, requestTxID, "should return request_tx_id") +} + +// testMaxFeeExactly40TRUF verifies that max_fee of exactly 40 TRUF succeeds +func testMaxFeeExactly40TRUF(t *testing.T, h *AttestationTestHelper, actionName string) { + argsBytes, err := tn_utils.EncodeActionArgs([]any{int64(44)}) + require.NoError(t, err, "encode action args") + + engineCtx := h.NewEngineContext() + + maxFee := types.MustParseDecimalExplicit("40000000000000000000", 78, 0) // Exactly 40 TRUF in wei + + var requestTxID string + res, err := h.platform.Engine.Call(engineCtx, h.platform.DB, "", "request_attestation", []any{ + TestDataProviderHex, + TestStreamID, + actionName, + argsBytes, + false, + maxFee, + }, func(row *common.Row) error { + requestTxID = row.Values[0].(string) + return nil + }) + + require.NoError(t, err, "call should not error at engine level") + require.Nil(t, res.Error, "max_fee of exactly 40 TRUF should succeed") + require.NotEmpty(t, requestTxID, "should return request_tx_id") +} + +// testMaxFeeAbove40TRUF verifies that max_fee above 40 TRUF succeeds +func testMaxFeeAbove40TRUF(t *testing.T, h *AttestationTestHelper, actionName string) { + argsBytes, err := tn_utils.EncodeActionArgs([]any{int64(45)}) + require.NoError(t, err, "encode action args") + + engineCtx := h.NewEngineContext() + + maxFee := types.MustParseDecimalExplicit("50000000000000000000", 78, 0) // 50 TRUF in wei + + var requestTxID string + res, err := h.platform.Engine.Call(engineCtx, h.platform.DB, "", "request_attestation", []any{ + TestDataProviderHex, + TestStreamID, + actionName, + argsBytes, + false, + maxFee, + }, func(row *common.Row) error { + requestTxID = row.Values[0].(string) + return nil + }) + + require.NoError(t, err, "call should not error at engine level") + require.Nil(t, res.Error, "max_fee above 40 TRUF should succeed") + require.NotEmpty(t, requestTxID, "should return request_tx_id") +} + +// testMaxFeeBelow40TRUF verifies that max_fee below 40 TRUF fails +func testMaxFeeBelow40TRUF(t *testing.T, h *AttestationTestHelper, actionName string) { + argsBytes, err := tn_utils.EncodeActionArgs([]any{int64(46)}) + require.NoError(t, err, "encode action args") + + engineCtx := h.NewEngineContext() + + maxFee := types.MustParseDecimalExplicit("30000000000000000000", 78, 0) // 30 TRUF in wei + + res, err := h.platform.Engine.Call(engineCtx, h.platform.DB, "", "request_attestation", []any{ + TestDataProviderHex, + TestStreamID, + actionName, + argsBytes, + false, + maxFee, + }, func(row *common.Row) error { + return nil + }) + + require.NoError(t, err, "call should not error at engine level") + require.NotNil(t, res.Error, "max_fee below 40 TRUF should fail") + require.Contains(t, res.Error.Error(), "exceeds caller max_fee limit", + "error should indicate fee exceeds max_fee") +} + +// testMaxFeeVeryLarge verifies that very large max_fee succeeds (tests NUMERIC(78,0) capacity) +func testMaxFeeVeryLarge(t *testing.T, h *AttestationTestHelper, actionName string) { + argsBytes, err := tn_utils.EncodeActionArgs([]any{int64(47)}) + require.NoError(t, err, "encode action args") + + engineCtx := h.NewEngineContext() + + // Use a very large number that wouldn't fit in INT8 (max ~9.2 TRUF) + // This is 1 billion TRUF in wei + maxFee := types.MustParseDecimalExplicit("1000000000000000000000000000", 78, 0) // 1 billion TRUF in wei + + var requestTxID string + res, err := h.platform.Engine.Call(engineCtx, h.platform.DB, "", "request_attestation", []any{ + TestDataProviderHex, + TestStreamID, + actionName, + argsBytes, + false, + maxFee, + }, func(row *common.Row) error { + requestTxID = row.Values[0].(string) + return nil + }) + + require.NoError(t, err, "call should not error at engine level") + require.Nil(t, res.Error, "very large max_fee should succeed (NUMERIC(78,0) should handle it)") + require.NotEmpty(t, requestTxID, "should return request_tx_id") +} + +// testMaxFeeNegative verifies that negative max_fee is treated as no limit +func testMaxFeeNegative(t *testing.T, h *AttestationTestHelper, actionName string) { + argsBytes, err := tn_utils.EncodeActionArgs([]any{int64(48)}) + require.NoError(t, err, "encode action args") + + engineCtx := h.NewEngineContext() + + maxFee := types.MustParseDecimalExplicit("-1000000000000000000", 78, 0) // -1 TRUF in wei + + var requestTxID string + res, err := h.platform.Engine.Call(engineCtx, h.platform.DB, "", "request_attestation", []any{ + TestDataProviderHex, + TestStreamID, + actionName, + argsBytes, + false, + maxFee, + }, func(row *common.Row) error { + requestTxID = row.Values[0].(string) + return nil + }) + + require.NoError(t, err, "call should not error at engine level") + require.Nil(t, res.Error, "negative max_fee should be treated as no limit") + require.NotEmpty(t, requestTxID, "should return request_tx_id") +} diff --git a/tests/streams/attestation/attestation_request_test.go b/tests/streams/attestation/attestation_request_test.go index bd8916042..fc0d6d505 100644 --- a/tests/streams/attestation/attestation_request_test.go +++ b/tests/streams/attestation/attestation_request_test.go @@ -68,7 +68,7 @@ func runAttestationHappyPath(helper *AttestationTestHelper, actionName string, a actionName, argsBytes, false, - int64(0), + nil, // max_fee = NULL (no limit) }, func(row *common.Row) error { require.Len(helper.t, row.Values, 2, "expected request_attestation to return request_tx_id and attestation_hash") txID, ok := row.Values[0].(string) @@ -162,7 +162,7 @@ func runAttestationUnauthorizedBlocked(t *testing.T, ctx context.Context, platfo actionName, argsBytes, false, - int64(0), + nil, // max_fee = NULL (no limit) }, func(row *common.Row) error { return nil }) diff --git a/tests/streams/attestation/request_attestation_fee_test.go b/tests/streams/attestation/request_attestation_fee_test.go index 47b85080c..5ee488712 100644 --- a/tests/streams/attestation/request_attestation_fee_test.go +++ b/tests/streams/attestation/request_attestation_fee_test.go @@ -411,8 +411,8 @@ func callRequestAttestationActionWithTimeRange(ctx context.Context, platform *kw streamID, actionName, argsBytes, - false, // encrypt_sig - int64(0), // max_fee (unused) + false, // encrypt_sig + nil, // max_fee = NULL (no limit) }, func(row *common.Row) error { return nil }, ) diff --git a/tests/streams/attestation/test_helpers.go b/tests/streams/attestation/test_helpers.go index b764efb1e..965b90b77 100644 --- a/tests/streams/attestation/test_helpers.go +++ b/tests/streams/attestation/test_helpers.go @@ -198,7 +198,7 @@ func (h *AttestationTestHelper) RequestAttestation(actionName string, value int6 actionName, argsBytes, false, - int64(0), + nil, // max_fee = NULL (no limit) }, func(row *common.Row) error { requestTxID = row.Values[0].(string) attestationHash = append([]byte(nil), row.Values[1].([]byte)...) @@ -268,7 +268,7 @@ func (h *AttestationTestHelper) CreateAttestationForRequester(actionName string, actionName, argsBytes, false, - int64(0), + nil, // max_fee = NULL (no limit) }, func(row *common.Row) error { return nil }) require.NoError(h.t, err, "request_attestation engine call")