diff --git a/internal/migrations/024-attestation-actions.sql b/internal/migrations/024-attestation-actions.sql index 1da32871e..25abe5669 100644 --- a/internal/migrations/024-attestation-actions.sql +++ b/internal/migrations/024-attestation-actions.sql @@ -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. */ @@ -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'); diff --git a/tests/streams/attestation/attestation_request_test.go b/tests/streams/attestation/attestation_request_test.go index 083724125..fd6579813 100644 --- a/tests/streams/attestation/attestation_request_test.go +++ b/tests/streams/attestation/attestation_request_test.go @@ -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 }, }, @@ -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() diff --git a/tests/streams/attestation/test_helpers.go b/tests/streams/attestation/test_helpers.go index c253dc7c0..fb5f9c5eb 100644 --- a/tests/streams/attestation/test_helpers.go +++ b/tests/streams/attestation/test_helpers.go @@ -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" ) @@ -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 @@ -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")