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
15 changes: 14 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-awslocalstack: ## Run the AWS LocalStack 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-awslocalstack: ## 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 Down Expand Up @@ -553,14 +565,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 := ^TestLocalStackAWSIntegration$$

# -----------------------------------------------------------------------------
# Node topology axis — orthogonal to shards.
Expand Down
28 changes: 22 additions & 6 deletions internal/config/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package auth

import (
"context"
"errors"
"fmt"
"math/big"

Expand Down Expand Up @@ -60,20 +61,35 @@ func GetTransactOptsFactory(ctx context.Context, chainId *big.Int) (ethutil.Tran
}
return ethutil.NewStaticTransactOptsFactory(txOpts), nil
case AuthKindAWS:
awsc, err := aws_cfg.LoadDefaultConfig(ctx)
keyId, err := GetAuthAwsKmsKeyId()
if err != nil {
return nil, err
}
kmsConfig := aws_kms.NewFromConfig(awsc)
authAwsKmsKeyId, err := GetAuthAwsKmsKeyId()
awsOpts := make([]func (*aws_cfg.LoadOptions) error, 0, 2)
kmsRegion, err := GetAuthAwsKmsRegion()
if !errors.Is(err, ErrNotDefined) {
if err != nil {
return nil, err
}
awsOpts = append(awsOpts, aws_cfg.WithRegion(kmsRegion.Value))
}
kmsEndpoint, err := GetAuthAwsKmsEndpoint()
if !errors.Is(err, ErrNotDefined) {
if err != nil {
return nil, err
}
awsOpts = append(awsOpts, aws_cfg.WithBaseEndpoint(kmsEndpoint.Value))
}
awsCfg, err := aws_cfg.LoadDefaultConfig(ctx, awsOpts...)
if err != nil {
return nil, err
}
kmsClient := aws_kms.NewFromConfig(awsCfg)
return signtx.CreateAWSTransactOptsFactory(
ctx,
kmsConfig,
aws.String(authAwsKmsKeyId.Value),
types.NewEIP155Signer(chainId),
kmsClient,
aws.String(keyId.Value),
types.LatestSignerForChainID(chainId),
)
default:
return nil, fmt.Errorf("no valid authentication method found")
Expand Down
9 changes: 9 additions & 0 deletions internal/config/generate/Config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,15 @@ Must be set alongside `CARTESI_AUTH_AWS_KMS_KEY_ID`."""
omit = true
used-by = ["claimer", "node", "cli", "prt"]

[auth.CARTESI_AUTH_AWS_KMS_ENDPOINT]
go-type = "RedactedString"
description = """
An AWS KMS Endpoint.

When not provided, the default endpoint for the AWS region defined by `CARTESI_AUTH_AWS_KMS_REGION` is automatically used."""
omit = true
used-by = ["claimer", "node", "cli", "prt"]

#
# Database
#
Expand Down
16 changes: 16 additions & 0 deletions internal/config/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
Loading
Loading