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
16 changes: 16 additions & 0 deletions internal/migrations/024-attestation-actions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
/**
* request_attestation: Request signed attestation of query results
*
* Permissions:
* - Only wallets with the 'system:network_writer' role can request attestations.
*
* Validates action is allowed, executes query deterministically, calculates
* attestation hash, stores unsigned attestation, and queues for signing.
*/
Expand All @@ -19,6 +22,19 @@ $max_fee INT8
-- Capture transaction ID for primary key
$request_tx_id := @txid;

-- Permission Check: Ensure caller has the 'system:network_writer' role.
$lower_caller TEXT := LOWER(@caller);
$has_permission BOOL := false;
for $row in are_members_of('system', 'network_writer', ARRAY[$lower_caller]) {
if $row.wallet = $lower_caller AND $row.is_member {
$has_permission := true;
break;
}
}
if NOT $has_permission {
ERROR('Caller does not have the required system:network_writer role to request attestation.');
}

-- Validate encryption flag (must be false in MVP)
if $encrypt_sig = true {
ERROR('Encryption not implemented');
Expand Down
48 changes: 47 additions & 1 deletion tests/streams/attestation/attestation_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ func TestRequestAttestationInsertsCanonicalPayload(t *testing.T) {
helper := NewAttestationTestHelper(t, ctx, platform)

require.NoError(t, helper.SetupTestAction(testActionName, TestActionIDRequest))
runAttestationHappyPath(helper, testActionName, TestActionIDRequest)

t.Run("HappyPath", func(t *testing.T) {
runAttestationHappyPath(helper, testActionName, TestActionIDRequest)
})

t.Run("UnauthorizedUserBlocked", func(t *testing.T) {
runAttestationUnauthorizedBlocked(t, ctx, platform, helper, testActionName)
})

return nil
},
},
Expand Down Expand Up @@ -124,6 +132,44 @@ type attestationRow struct {
createdHeight int64
}

func runAttestationUnauthorizedBlocked(t *testing.T, ctx context.Context, platform *kwilTesting.Platform, helper *AttestationTestHelper, actionName string) {
// Create an unauthorized user that does NOT have network_writer role
unauthorizedAddr := util.Unsafe_NewEthereumAddressFromString("0x0000000000000000000000000000000000009999")

argsBytes, err := tn_utils.EncodeActionArgs([]any{int64(999)})
require.NoError(t, err, "encode action args")

// Create a context for the unauthorized user
unauthorizedCtx := &common.EngineContext{
TxContext: &common.TxContext{
Ctx: ctx,
BlockContext: &common.BlockContext{
Height: 1,
},
Signer: unauthorizedAddr.Bytes(),
Caller: unauthorizedAddr.Address(),
TxID: platform.Txid(),
},
}

// Try to request attestation as unauthorized user - should fail
res, err := platform.Engine.Call(unauthorizedCtx, platform.DB, "", "request_attestation", []any{
TestDataProviderHex,
TestStreamID,
actionName,
argsBytes,
false,
int64(0),
}, func(row *common.Row) error {
return nil
})

require.NoError(t, err, "call should not error at engine level")
require.NotNil(t, res.Error, "action should return error for unauthorized user")
require.Contains(t, res.Error.Error(), "does not have the required system:network_writer role",
"error should indicate missing network_writer role")
}

func fetchAttestationRow(helper *AttestationTestHelper, hash []byte) attestationRow {
engineCtx := helper.NewEngineContext()

Expand Down
19 changes: 18 additions & 1 deletion tests/streams/attestation/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/trufnetwork/kwil-db/core/crypto/auth"
kwilTesting "github.com/trufnetwork/kwil-db/testing"
"github.com/trufnetwork/node/extensions/tn_utils"
testsetup "github.com/trufnetwork/node/tests/streams/utils/setup"
"github.com/trufnetwork/sdk-go/core/util"
)

Expand Down Expand Up @@ -73,11 +74,18 @@ type AttestationTestHelper struct {

// NewAttestationTestHelper creates a new helper
func NewAttestationTestHelper(t *testing.T, ctx context.Context, platform *kwilTesting.Platform) *AttestationTestHelper {
return &AttestationTestHelper{
helper := &AttestationTestHelper{
t: t,
ctx: ctx,
platform: platform,
}

// Grant network_writer role to deployer by default
deployer, err := util.NewEthereumAddressFromBytes(platform.Deployer)
require.NoError(t, err, "create deployer address")
helper.GrantNetworkWriterRole(deployer.Address())

return helper
}

// NewEngineContext creates a standard engine context
Expand Down Expand Up @@ -204,8 +212,17 @@ func (h *AttestationTestHelper) SignAttestation(requestTxID string) {
require.Nil(h.t, res.Error, "sign_attestation should succeed")
}

// GrantNetworkWriterRole grants the network_writer role to a wallet
func (h *AttestationTestHelper) GrantNetworkWriterRole(walletAddr string) {
err := testsetup.AddMemberToRoleBypass(h.ctx, h.platform, "system", "network_writer", walletAddr)
require.NoError(h.t, err, "grant network_writer role to %s", walletAddr)
}

// CreateAttestationForRequester creates an attestation for a specific requester
func (h *AttestationTestHelper) CreateAttestationForRequester(actionName string, requester *util.EthereumAddress, value int64) {
// Grant network_writer role to requester first
h.GrantNetworkWriterRole(requester.Address())

argsBytes, err := tn_utils.EncodeActionArgs([]any{value})
require.NoError(h.t, err, "encode args")

Expand Down
Loading