Skip to content
Open
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
19 changes: 18 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,15 @@ start-postgres: ## Run the PostgreSQL 16 docker container
@docker run --rm --name postgres -p 5432:5432 -d -e POSTGRES_PASSWORD=password -e POSTGRES_DB=rollupsdb -v $(CURDIR)/test/postgres/init-test-db.sh:/docker-entrypoint-initdb.d/init-test-db.sh postgres:18-alpine
@$(MAKE) migrate

start-aws: ## Run the AWS LocakStack docker container
@echo "Starting AWS localstack"
@docker run --rm --name awslocalstack -p 127.0.0.1:4566:4566 -d -e SERVICES=kms localstack/localstack:4.14.0
@echo "Add the following variables to run integration test with AWS services:"
@echo " export AWS_ACCESS_KEY_ID=test"
@echo " export AWS_SECRET_ACCESS_KEY=test"
@echo " export LOCALSTACK_KMS_ENDPOINT=http://localhost:4566"
@echo " export LOCALSTACK_KMS_REQUIRED=true"

start: start-postgres start-devnet ## Start the anvil devnet and PostgreSQL 16 docker containers

stop-devnet: ## Stop the anvil devnet docker container
Expand All @@ -506,6 +515,9 @@ stop-devnet: ## Stop the anvil devnet docker container
stop-postgres: ## Stop the PostgreSQL 16 docker container
@docker stop postgres || true

stop-aws: ## Stop the AWS LocalStack docker container
@docker stop awslocalstack || true

stop: stop-devnet stop-postgres ## Stop all running docker containers

restart-devnet: ## Restart the anvil devnet docker container
Expand All @@ -516,6 +528,10 @@ restart-postgres: ## Restart the PostgreSQL 16 docker container and migrate it
@$(MAKE) stop-postgres
@$(MAKE) start-postgres

restart-aws: ## Restart the AWS LocalStack docker container and migrate it
@$(MAKE) stop-aws
@$(MAKE) start-aws

restart: ## Restart all running docker containers
@$(MAKE) stop-devnet
@$(MAKE) stop-postgres
Expand Down Expand Up @@ -553,14 +569,15 @@ check-license: ## Verify license headers on Go source files
# Discovery (integration-test-shard-check) lists tests with a plain Go
# toolchain, so the integration package must stay free of the Cartesi CGo
# dependency for the check to build on the CI setup runner.
INTEGRATION_SHARDS := basic quorum prt replay restart withdrawal
INTEGRATION_SHARDS := basic quorum prt replay restart withdrawal awskms

INTEGRATION_SHARD_basic := ^Test(EchoAuthority|RejectException|MultiApp|EchoAuthorityStaging)$$
INTEGRATION_SHARD_quorum := ^Test(EchoQuorum|SameBlockInputs)$$
INTEGRATION_SHARD_prt := ^Test(EchoPrt|RejectExceptionPrt|ForeclosePrt)$$
INTEGRATION_SHARD_replay := ^Test(Foreclose|ForecloseReplay|DivergentClaim)$$
INTEGRATION_SHARD_restart := ^Test(Restart|SnapshotPolicy)$$
INTEGRATION_SHARD_withdrawal := ^TestWithdrawalLifecycle$$
INTEGRATION_SHARD_awskms := ^TestLocalStackAWS(SignTx|TransactionOptsFactory)$$

# -----------------------------------------------------------------------------
# Node topology axis — orthogonal to shards.
Expand Down
2 changes: 1 addition & 1 deletion internal/config/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func GetTransactOptsFactory(ctx context.Context, chainId *big.Int) (ethutil.Tran
ctx,
kmsConfig,
aws.String(authAwsKmsKeyId.Value),
types.NewEIP155Signer(chainId),
types.LatestSignerForChainID(chainId),
)
default:
return nil, fmt.Errorf("no valid authentication method found")
Expand Down
111 changes: 31 additions & 80 deletions internal/kms/signtx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,95 +11,15 @@ import (
"math/big"
"testing"

"github.com/cartesi/rollups-node/pkg/ethutil"

"github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"

awscfg "github.com/aws/aws-sdk-go-v2/config"
awskms "github.com/aws/aws-sdk-go-v2/service/kms"
kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/stretchr/testify/require"
)

var ARN = ""

/* Create a SignTxFn from a private key. Useful for testing */
func CreateSignTxFnFromPrivateKey(privateKey *ecdsa.PrivateKey) SignTxFn {
return func(_ context.Context, tx *ethtypes.Transaction, s ethtypes.Signer) (*ethtypes.Transaction, error) {
return ethtypes.SignTx(tx, s, privateKey)
}
}

func sendFunds(
value *big.Int,
SignTx SignTxFn,
ctx context.Context,
sender common.Address,
recipient common.Address,
) {
client, err := ethclient.Dial("http://127.0.0.1:8545") // anvil
if err != nil {
panic(err)
}

nonce, err := client.PendingNonceAt(context.Background(), sender)
if err != nil {
panic(err)
}
gasLimit := uint64(21000)
gasPrice, err := client.SuggestGasPrice(ctx)
if err != nil {
panic(err)
}
var data []byte
tx := ethtypes.NewTransaction(nonce, recipient, value, gasLimit, gasPrice, data)
chainID, err := client.NetworkID(context.Background())
if err != nil {
panic(err)
}
signedTx, err := SignTx(ctx, tx, ethtypes.NewEIP155Signer(chainID))
if err != nil {
panic(err)
}
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
panic(err)
}
}

func TestSignTx(t *testing.T) {
if len(ARN) == 0 {
t.Skip("Skipping test, ARN for KMS key is unset")
}
value20 := big.NewInt(2000000000000000000) // in wei (2 eth)
value10 := big.NewInt(1000000000000000000) // in wei (1 eth)

anvilPrivateKey, err := ethutil.MnemonicToPrivateKey(ethutil.FoundryMnemonic, 0)
if err != nil {
panic(err)
}
anvilPublicKey := anvilPrivateKey.Public().(*ecdsa.PublicKey)
anvilAddress := crypto.PubkeyToAddress(*anvilPublicKey)

config, err := awscfg.LoadDefaultConfig(context.Background())
if err != nil {
panic(err)
}
kms := awskms.NewFromConfig(config)
SignTx, _, KMSAddress, err := CreateAWSSignTxFn(context.Background(), kms, &ARN)
if err != nil {
panic(err)
}

sendFunds(value20, CreateSignTxFnFromPrivateKey(anvilPrivateKey),
context.Background(), anvilAddress, KMSAddress)
sendFunds(value10, SignTx,
context.Background(), KMSAddress, anvilAddress)
}

func TestAWSTransactOptsFactorySignsWithSubmitContext(t *testing.T) {
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
Expand Down Expand Up @@ -128,6 +48,37 @@ func TestAWSTransactOptsFactorySignsWithSubmitContext(t *testing.T) {
require.NoError(t, client.signContext.Err())
}

func TestAWSTransactOptsFactorySignsDynamicFeeTransaction(t *testing.T) {
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)

chainID := big.NewInt(31337)
client := newFakeKMSClient(t, privateKey)
keyID := "alias/test-key"
factory, err := CreateAWSTransactOptsFactory(
context.Background(), client, &keyID, ethtypes.LatestSignerForChainID(chainID),
)
require.NoError(t, err)

opts, err := factory.NewTransactOpts(context.Background())
require.NoError(t, err)
tx := ethtypes.NewTx(&ethtypes.DynamicFeeTx{
ChainID: chainID,
Nonce: 1,
GasTipCap: big.NewInt(1),
GasFeeCap: big.NewInt(2),
Gas: 21000,
To: &common.Address{0x01},
Value: big.NewInt(3),
})
signed, err := opts.Signer(opts.From, tx)
require.NoError(t, err)

sender, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(chainID), signed)
require.NoError(t, err)
require.Equal(t, crypto.PubkeyToAddress(privateKey.PublicKey), sender)
}

type fakeKMSClient struct {
t *testing.T
privateKey *ecdsa.PrivateKey
Expand Down
20 changes: 20 additions & 0 deletions test/compose/compose.integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ services:
<<: *env
restart: "no"

localstack:
image: localstack/localstack:4.14.0
networks:
- devnet
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:4566/_localstack/health"]
interval: 2s
timeout: 2s
retries: 30
environment:
SERVICES: kms

# The node is started and managed by TestMain inside the test process.
# This ensures all tests (including restart and snapshot policy tests)
# run with the same infrastructure in both local and CI environments.
Expand All @@ -99,6 +111,8 @@ services:
condition: service_healthy
dapp-builder:
condition: service_completed_successfully
localstack:
condition: service_healthy
volumes:
- dapp_images:/var/lib/cartesi-rollups-node/dapps:ro
- node_logs:/var/lib/cartesi-rollups-node/logs
Expand All @@ -125,6 +139,12 @@ services:
CARTESI_TEST_ERC20_WITHDRAWAL_DAPP_PATH: /var/lib/cartesi-rollups-node/dapps/erc20-withdrawal-dapp
CARTESI_TEST_NODE_LOG_FILE: /var/lib/cartesi-rollups-node/logs/node.log
CARTESI_INSPECT_URL: http://localhost:10012/
# test/integration/localstack_integration_test.go
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_REGION: us-east-1
LOCALSTACK_KMS_ENDPOINT: http://localstack:4566
LOCALSTACK_KMS_REQUIRED: "true"

volumes:
dapp_images:
Expand Down
147 changes: 147 additions & 0 deletions test/integration/localstack_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

//go:build endtoendtests

package integration

import (
"context"
"crypto/ecdsa"
"math/big"
"os"
"testing"

"github.com/cartesi/rollups-node/internal/config"
"github.com/cartesi/rollups-node/internal/kms"
"github.com/cartesi/rollups-node/pkg/ethutil"

"github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awskms "github.com/aws/aws-sdk-go-v2/service/kms"
kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)

func setupTesting(t *testing.T) (*awskms.Client, *awskms.CreateKeyOutput) {
t.Helper()

endpoint := os.Getenv("LOCALSTACK_KMS_ENDPOINT")
if endpoint == "" {
t.Skip("LOCALSTACK_KMS_ENDPOINT is not set; skipping LocalStack KMS integration test")
}
cfg, err := awscfg.LoadDefaultConfig(t.Context(),
awscfg.WithRegion("us-east-1"),
awscfg.WithBaseEndpoint(endpoint),
)
require.NoError(t, err)
client := awskms.NewFromConfig(cfg)

created, err := client.CreateKey(t.Context(), &awskms.CreateKeyInput{
KeyUsage: kmstypes.KeyUsageTypeSignVerify,
KeySpec: kmstypes.KeySpecEccSecgP256k1,
})
if err != nil {
message := "unable to create key on LocalStack"
if os.Getenv("LOCALSTACK_KMS_REQUIRED") == "true" {
t.Fatalf("%s: %v", message, err)
}
t.Skipf("%s: %v", message, err)
}
require.NotNil(t, created.KeyMetadata)
require.NotNil(t, created.KeyMetadata.KeyId)

return client, created
}

func sendFunds(
t *testing.T,
value *big.Int,
SignTx kms.SignTxFn,
sender common.Address,
recipient common.Address,
) {
ethEndpoint, err := config.GetBlockchainHttpEndpoint()
require.NoError(t, err)
client, err := ethclient.Dial(ethEndpoint.Raw()) // anvil
require.NoError(t, err)

nonce, err := client.PendingNonceAt(t.Context(), sender)
require.NoError(t, err)
gasLimit := uint64(21000)
gasPrice, err := client.SuggestGasPrice(t.Context())
require.NoError(t, err)
var data []byte
tx := types.NewTransaction(nonce, recipient, value, gasLimit, gasPrice, data)
chainID, err := client.NetworkID(t.Context())
require.NoError(t, err)
signedTx, err := SignTx(t.Context(), tx, types.LatestSignerForChainID(chainID))
require.NoError(t, err)
err = client.SendTransaction(t.Context(), signedTx)
require.NoError(t, err)
}

func TestLocalStackAWSSignTx(t *testing.T) {
client, created := setupTesting(t)

awsSignTx, _, awsAddress, err := kms.CreateAWSSignTxFn(
t.Context(), client, created.KeyMetadata.KeyId,
)
require.NoError(t, err)

value20 := big.NewInt(2000000000000000000) // in wei (2 eth)
value10 := big.NewInt(1000000000000000000) // in wei (1 eth)
anvilPrivateKey, err := ethutil.MnemonicToPrivateKey(ethutil.FoundryMnemonic, 0)
if err != nil {
panic(err)
}
anvilPublicKey := anvilPrivateKey.Public().(*ecdsa.PublicKey)
anvilAddress := crypto.PubkeyToAddress(*anvilPublicKey)
anvilSignTx := func(_ context.Context, tx *types.Transaction, s types.Signer) (*types.Transaction, error) {
return types.SignTx(tx, s, anvilPrivateKey)
}

sendFunds(t, value20, anvilSignTx, anvilAddress, awsAddress)
sendFunds(t, value10, awsSignTx, awsAddress, anvilAddress)
}

func TestLocalStackAWSTransactionOptsFactory(t *testing.T) {
client, created := setupTesting(t)

chainID := big.NewInt(31337)
factory, err := kms.CreateAWSTransactOptsFactory(
t.Context(), client, created.KeyMetadata.KeyId, types.LatestSignerForChainID(chainID),
)
require.NoError(t, err)
require.NotEqual(t, common.Address{}, factory.From())
opts, err := factory.NewTransactOpts(t.Context())
require.NoError(t, err)

to := common.Address{0x01}
tests := map[string]*types.Transaction{
"legacy": types.NewTx(&types.LegacyTx{
Nonce: 1, GasPrice: big.NewInt(2), Gas: 21000, To: &to, Value: big.NewInt(3),
}),
"dynamic fee": types.NewTx(&types.DynamicFeeTx{
ChainID: chainID, Nonce: 2, GasTipCap: big.NewInt(1), GasFeeCap: big.NewInt(2),
Gas: 21000, To: &to, Value: big.NewInt(3),
}),
}
for name, tx := range tests {
t.Run(name, func(t *testing.T) {
signed, err := opts.Signer(opts.From, tx)
require.NoError(t, err)
sender, err := types.Sender(types.LatestSignerForChainID(chainID), signed)
require.NoError(t, err)
require.Equal(t, factory.From(), sender)
})
}

_, _ = client.ScheduleKeyDeletion(t.Context(), &awskms.ScheduleKeyDeletionInput{
KeyId: created.KeyMetadata.KeyId, PendingWindowInDays: aws.Int32(7), //nolint:mnd
})
}