Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion internal/migrations/024-attestation-actions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
255 changes: 255 additions & 0 deletions tests/streams/attestation/attestation_max_fee_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
4 changes: 2 additions & 2 deletions tests/streams/attestation/attestation_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
})
Expand Down
4 changes: 2 additions & 2 deletions tests/streams/attestation/request_attestation_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
)
Expand Down
4 changes: 2 additions & 2 deletions tests/streams/attestation/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)...)
Expand Down Expand Up @@ -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")
Expand Down
Loading