From 91ee582c0acc58d693426e92f43563024a6254f7 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 14 Jun 2024 08:23:54 +0200 Subject: [PATCH 01/13] Refactor integration tests --- .github/workflows/soroban-rpc.yml | 2 +- .../{test => integrationtest}/archive_test.go | 10 +- .../{test => integrationtest}/cors_test.go | 8 +- .../get_fee_stats_test.go | 50 +- .../get_ledger_entries_test.go | 64 +- .../get_ledger_entry_test.go | 46 +- .../get_network_test.go | 9 +- .../get_transactions_test.go | 24 +- .../get_version_info_test.go | 20 +- .../{test => integrationtest}/health_test.go | 17 +- .../integrationtest/infrastructure/client.go | 161 +++ .../infrastructure/contract.go | 118 ++ .../captive-core-integration-tests.cfg | 0 .../infrastructure/docker}/core-start.sh | 0 .../docker}/docker-compose.rpc.yml | 0 .../infrastructure/docker}/docker-compose.yml | 0 .../stellar-core-integration-tests.cfg | 0 .../infrastructure/test.go} | 245 ++-- .../integrationtest/infrastructure/util.go | 26 + .../{test => integrationtest}/metrics_test.go | 28 +- .../{test => integrationtest}/migrate_test.go | 44 +- .../simulate_transaction_test.go | 631 +++++++++ .../transaction_test.go | 219 +--- .../internal/integrationtest/upgrade_test.go | 53 + .../test/simulate_transaction_test.go | 1137 ----------------- cmd/soroban-rpc/internal/test/upgrade_test.go | 122 -- 26 files changed, 1270 insertions(+), 1764 deletions(-) rename cmd/soroban-rpc/internal/{test => integrationtest}/archive_test.go (75%) rename cmd/soroban-rpc/internal/{test => integrationtest}/cors_test.go (71%) rename cmd/soroban-rpc/internal/{test => integrationtest}/get_fee_stats_test.go (66%) rename cmd/soroban-rpc/internal/{test => integrationtest}/get_ledger_entries_test.go (67%) rename cmd/soroban-rpc/internal/{test => integrationtest}/get_ledger_entry_test.go (62%) rename cmd/soroban-rpc/internal/{test => integrationtest}/get_network_test.go (60%) rename cmd/soroban-rpc/internal/{test => integrationtest}/get_transactions_test.go (85%) rename cmd/soroban-rpc/internal/{test => integrationtest}/get_version_info_test.go (79%) rename cmd/soroban-rpc/internal/{test => integrationtest}/health_test.go (60%) create mode 100644 cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go create mode 100644 cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go rename cmd/soroban-rpc/internal/{test => integrationtest/infrastructure/docker}/captive-core-integration-tests.cfg (100%) rename cmd/soroban-rpc/internal/{test => integrationtest/infrastructure/docker}/core-start.sh (100%) rename cmd/soroban-rpc/internal/{test => integrationtest/infrastructure/docker}/docker-compose.rpc.yml (100%) rename cmd/soroban-rpc/internal/{test => integrationtest/infrastructure/docker}/docker-compose.yml (100%) rename cmd/soroban-rpc/internal/{test => integrationtest/infrastructure/docker}/stellar-core-integration-tests.cfg (100%) rename cmd/soroban-rpc/internal/{test/integration.go => integrationtest/infrastructure/test.go} (68%) create mode 100644 cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go rename cmd/soroban-rpc/internal/{test => integrationtest}/metrics_test.go (64%) rename cmd/soroban-rpc/internal/{test => integrationtest}/migrate_test.go (68%) create mode 100644 cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go rename cmd/soroban-rpc/internal/{test => integrationtest}/transaction_test.go (50%) create mode 100644 cmd/soroban-rpc/internal/integrationtest/upgrade_test.go delete mode 100644 cmd/soroban-rpc/internal/test/simulate_transaction_test.go delete mode 100644 cmd/soroban-rpc/internal/test/upgrade_test.go diff --git a/.github/workflows/soroban-rpc.yml b/.github/workflows/soroban-rpc.yml index fabc9835..c3104ca5 100644 --- a/.github/workflows/soroban-rpc.yml +++ b/.github/workflows/soroban-rpc.yml @@ -194,4 +194,4 @@ jobs: - name: Run Soroban RPC Integration Tests run: | make install_rust - go test -race -timeout 60m -v ./cmd/soroban-rpc/internal/test/... + go test -race -timeout 60m -v ./cmd/soroban-rpc/internal/integrationtest/... diff --git a/cmd/soroban-rpc/internal/test/archive_test.go b/cmd/soroban-rpc/internal/integrationtest/archive_test.go similarity index 75% rename from cmd/soroban-rpc/internal/test/archive_test.go rename to cmd/soroban-rpc/internal/integrationtest/archive_test.go index eaa4578e..bbc10ad9 100644 --- a/cmd/soroban-rpc/internal/test/archive_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/archive_test.go @@ -1,4 +1,4 @@ -package test +package integrationtest import ( "net" @@ -11,10 +11,12 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" ) func TestArchiveUserAgent(t *testing.T) { - archiveHost := net.JoinHostPort("localhost", strconv.Itoa(StellarCoreArchivePort)) + archiveHost := net.JoinHostPort("localhost", strconv.Itoa(infrastructure.StellarCoreArchivePort)) proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: archiveHost}) userAgents := sync.Map{} historyArchiveProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -23,11 +25,11 @@ func TestArchiveUserAgent(t *testing.T) { })) defer historyArchiveProxy.Close() - cfg := &TestConfig{ + cfg := &infrastructure.TestConfig{ HistoryArchiveURL: historyArchiveProxy.URL, } - NewTest(t, cfg) + infrastructure.NewTest(t, cfg) _, ok := userAgents.Load("soroban-rpc/0.0.0") assert.True(t, ok, "rpc service should set user agent for history archives") diff --git a/cmd/soroban-rpc/internal/test/cors_test.go b/cmd/soroban-rpc/internal/integrationtest/cors_test.go similarity index 71% rename from cmd/soroban-rpc/internal/test/cors_test.go rename to cmd/soroban-rpc/internal/integrationtest/cors_test.go index ede91fd8..d2226b19 100644 --- a/cmd/soroban-rpc/internal/test/cors_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/cors_test.go @@ -1,4 +1,4 @@ -package test +package integrationtest import ( "bytes" @@ -7,15 +7,17 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" ) // TestCORS ensures that we receive the correct CORS headers as a response to an HTTP request. // Specifically, when we include an Origin header in the request, a soroban-rpc should response // with a corresponding Access-Control-Allow-Origin. func TestCORS(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) - request, err := http.NewRequest("POST", test.sorobanRPCURL(), bytes.NewBufferString("{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"getHealth\"}")) + request, err := http.NewRequest("POST", test.GetSorobanRPCURL(), bytes.NewBufferString("{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"getHealth\"}")) require.NoError(t, err) request.Header.Set("Content-Type", "application/json") origin := "testorigin.com" diff --git a/cmd/soroban-rpc/internal/test/get_fee_stats_test.go b/cmd/soroban-rpc/internal/integrationtest/get_fee_stats_test.go similarity index 66% rename from cmd/soroban-rpc/internal/test/get_fee_stats_test.go rename to cmd/soroban-rpc/internal/integrationtest/get_fee_stats_test.go index 15969aea..407ebb13 100644 --- a/cmd/soroban-rpc/internal/test/get_fee_stats_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_fee_stats_test.go @@ -1,42 +1,22 @@ -package test +package integrationtest import ( "context" "testing" - "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) func TestGetFeeStats(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) - client := test.GetRPCLient() - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - // Submit soroban transaction - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sorobanTxResponse := sendSuccessfulTransaction(t, client, sourceAccount, tx) + sorobanTxResponse, _ := test.UploadHelloWorldContract() var sorobanTxResult xdr.TransactionResult require.NoError(t, xdr.SafeUnmarshalBase64(sorobanTxResponse.ResultXdr, &sorobanTxResult)) sorobanTotalFee := sorobanTxResult.FeeCharged @@ -46,28 +26,18 @@ func TestGetFeeStats(t *testing.T) { sorobanResourceFeeCharged := sorobanFees.TotalRefundableResourceFeeCharged + sorobanFees.TotalNonRefundableResourceFeeCharged sorobanInclusionFee := uint64(sorobanTotalFee - sorobanResourceFeeCharged) + seq, err := test.MasterAccount().GetSequenceNumber() + require.NoError(t, err) // Submit classic transaction - params = txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.BumpSequence{BumpTo: account.Sequence + 100}, - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - classicTxResponse := sendSuccessfulTransaction(t, client, sourceAccount, tx) + classicTxResponse := test.SendMasterOperation( + &txnbuild.BumpSequence{BumpTo: seq + 100}, + ) var classicTxResult xdr.TransactionResult require.NoError(t, xdr.SafeUnmarshalBase64(classicTxResponse.ResultXdr, &classicTxResult)) classicFee := uint64(classicTxResult.FeeCharged) var result methods.GetFeeStatsResult - if err := client.CallResult(context.Background(), "getFeeStats", nil, &result); err != nil { + if err := test.GetRPCLient().CallResult(context.Background(), "getFeeStats", nil, &result); err != nil { t.Fatalf("rpc call failed: %v", err) } expectedResult := methods.GetFeeStatsResult{ diff --git a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go b/cmd/soroban-rpc/internal/integrationtest/get_ledger_entries_test.go similarity index 67% rename from cmd/soroban-rpc/internal/test/get_ledger_entries_test.go rename to cmd/soroban-rpc/internal/integrationtest/get_ledger_entries_test.go index 835f183f..65b819b2 100644 --- a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_ledger_entries_test.go @@ -1,35 +1,30 @@ -package test +package integrationtest import ( "context" - "crypto/sha256" "testing" "github.com/creachadair/jrpc2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) func TestGetLedgerEntriesNotFound(t *testing.T) { - test := NewTest(t, nil) - + test := infrastructure.NewTest(t, nil) client := test.GetRPCLient() - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) - contractIDHash := xdr.Hash(contractID) + hash := xdr.Hash{0xa, 0xb} keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ Type: xdr.LedgerEntryTypeContractData, ContractData: &xdr.LedgerKeyContractData{ Contract: xdr.ScAddress{ Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractIDHash, + ContractId: &hash, }, Key: xdr.ScVal{ Type: xdr.ScValTypeScvLedgerKeyContractInstance, @@ -54,7 +49,7 @@ func TestGetLedgerEntriesNotFound(t *testing.T) { } func TestGetLedgerEntriesInvalidParams(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) client := test.GetRPCLient() @@ -71,48 +66,9 @@ func TestGetLedgerEntriesInvalidParams(t *testing.T) { } func TestGetLedgerEntriesSucceeds(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) + test := infrastructure.NewTest(t, nil) + _, contractID, contractHash := test.CreateHelloWorldContract() - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - - contractHash := sha256.Sum256(contractBinary) contractCodeKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ Type: xdr.LedgerEntryTypeContractCode, ContractCode: &xdr.LedgerKeyContractCode{ @@ -146,7 +102,7 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { } var result methods.GetLedgerEntriesResponse - err = client.CallResult(context.Background(), "getLedgerEntries", request, &result) + err = test.GetRPCLient().CallResult(context.Background(), "getLedgerEntries", request, &result) require.NoError(t, err) require.Equal(t, 2, len(result.Entries)) require.Greater(t, result.LatestLedger, uint32(0)) @@ -159,7 +115,7 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { var firstEntry xdr.LedgerEntryData require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &firstEntry)) require.Equal(t, xdr.LedgerEntryTypeContractCode, firstEntry.Type) - require.Equal(t, contractBinary, firstEntry.MustContractCode().Code) + require.Equal(t, infrastructure.GetHelloWorldContract(), firstEntry.MustContractCode().Code) require.Greater(t, result.Entries[1].LastModifiedLedger, uint32(0)) require.LessOrEqual(t, result.Entries[1].LastModifiedLedger, result.LatestLedger) diff --git a/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go b/cmd/soroban-rpc/internal/integrationtest/get_ledger_entry_test.go similarity index 62% rename from cmd/soroban-rpc/internal/test/get_ledger_entry_test.go rename to cmd/soroban-rpc/internal/integrationtest/get_ledger_entry_test.go index 007dd0f2..2554776f 100644 --- a/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_ledger_entry_test.go @@ -1,29 +1,23 @@ -package test +package integrationtest import ( "context" - "crypto/sha256" "testing" "github.com/creachadair/jrpc2" - "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stellar/go/keypair" "github.com/stellar/go/xdr" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) func TestGetLedgerEntryNotFound(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) - contractIDHash := xdr.Hash(contractID) + contractIDHash := xdr.Hash{0x1, 0x2} keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ Type: xdr.LedgerEntryTypeContractData, ContractData: &xdr.LedgerKeyContractData{ @@ -43,13 +37,14 @@ func TestGetLedgerEntryNotFound(t *testing.T) { } var result methods.GetLedgerEntryResponse + client := test.GetRPCLient() jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntry", request, &result).(*jrpc2.Error) assert.Contains(t, jsonRPCErr.Message, "not found") assert.Equal(t, jrpc2.InvalidRequest, jsonRPCErr.Code) } func TestGetLedgerEntryInvalidParams(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) client := test.GetRPCLient() @@ -64,31 +59,10 @@ func TestGetLedgerEntryInvalidParams(t *testing.T) { } func TestGetLedgerEntrySucceeds(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - kp := keypair.Root(StandaloneNetworkPassphrase) - account := txnbuild.NewSimpleAccount(kp.Address(), 0) - - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) + test := infrastructure.NewTest(t, nil) - sendSuccessfulTransaction(t, client, kp, tx) + _, contractHash := test.UploadHelloWorldContract() - contractHash := sha256.Sum256(contractBinary) keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ Type: xdr.LedgerEntryTypeContractCode, ContractCode: &xdr.LedgerKeyContractCode{ @@ -101,11 +75,11 @@ func TestGetLedgerEntrySucceeds(t *testing.T) { } var result methods.GetLedgerEntryResponse - err = client.CallResult(context.Background(), "getLedgerEntry", request, &result) + err = test.GetRPCLient().CallResult(context.Background(), "getLedgerEntry", request, &result) assert.NoError(t, err) assert.Greater(t, result.LatestLedger, uint32(0)) assert.GreaterOrEqual(t, result.LatestLedger, result.LastModifiedLedger) var entry xdr.LedgerEntryData assert.NoError(t, xdr.SafeUnmarshalBase64(result.XDR, &entry)) - assert.Equal(t, contractBinary, entry.MustContractCode().Code) + assert.Equal(t, infrastructure.GetHelloWorldContract(), entry.MustContractCode().Code) } diff --git a/cmd/soroban-rpc/internal/test/get_network_test.go b/cmd/soroban-rpc/internal/integrationtest/get_network_test.go similarity index 60% rename from cmd/soroban-rpc/internal/test/get_network_test.go rename to cmd/soroban-rpc/internal/integrationtest/get_network_test.go index 777a48e4..b8623973 100644 --- a/cmd/soroban-rpc/internal/test/get_network_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_network_test.go @@ -1,4 +1,4 @@ -package test +package integrationtest import ( "context" @@ -6,11 +6,12 @@ import ( "github.com/stretchr/testify/assert" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) func TestGetNetworkSucceeds(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) client := test.GetRPCLient() @@ -19,7 +20,7 @@ func TestGetNetworkSucceeds(t *testing.T) { var result methods.GetNetworkResponse err := client.CallResult(context.Background(), "getNetwork", request, &result) assert.NoError(t, err) - assert.Equal(t, friendbotURL, result.FriendbotURL) - assert.Equal(t, StandaloneNetworkPassphrase, result.Passphrase) + assert.Equal(t, infrastructure.FriendbotURL, result.FriendbotURL) + assert.Equal(t, infrastructure.StandaloneNetworkPassphrase, result.Passphrase) assert.GreaterOrEqual(t, result.ProtocolVersion, 20) } diff --git a/cmd/soroban-rpc/internal/test/get_transactions_test.go b/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go similarity index 85% rename from cmd/soroban-rpc/internal/test/get_transactions_test.go rename to cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go index 48371cea..90869bfa 100644 --- a/cmd/soroban-rpc/internal/test/get_transactions_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go @@ -1,4 +1,4 @@ -package test +package integrationtest import ( "context" @@ -9,6 +9,7 @@ import ( "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/assert" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) @@ -18,16 +19,10 @@ import ( // // Returns a fully populated TransactionParams structure. func buildSetOptionsTxParams(account txnbuild.SimpleAccount) txnbuild.TransactionParams { - params := txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.SetOptions{HomeDomain: txnbuild.NewHomeDomain("soroban.com")}, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()}, - } - return params + return infrastructure.CreateTransactionParams( + &account, + &txnbuild.SetOptions{HomeDomain: txnbuild.NewHomeDomain("soroban.com")}, + ) } // sendTransactions sends multiple transactions for testing purposes. @@ -39,7 +34,7 @@ func buildSetOptionsTxParams(account txnbuild.SimpleAccount) txnbuild.Transactio // // Returns a slice of ledger numbers corresponding to where each transaction was recorded. func sendTransactions(t *testing.T, client *jrpc2.Client) []uint32 { - kp := keypair.Root(StandaloneNetworkPassphrase) + kp := keypair.Root(infrastructure.StandaloneNetworkPassphrase) address := kp.Address() var ledgers []uint32 @@ -48,18 +43,19 @@ func sendTransactions(t *testing.T, client *jrpc2.Client) []uint32 { tx, err := txnbuild.NewTransaction(buildSetOptionsTxParams(account)) assert.NoError(t, err) - txResponse := sendSuccessfulTransaction(t, client, kp, tx) + txResponse := infrastructure.SendSuccessfulTransaction(t, client, kp, tx) ledgers = append(ledgers, txResponse.Ledger) } return ledgers } func TestGetTransactions(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) client := test.GetRPCLient() ledgers := sendTransactions(t, client) + test.MasterAccount() // Get transactions across multiple ledgers var result methods.GetTransactionsResponse request := methods.GetTransactionsRequest{ diff --git a/cmd/soroban-rpc/internal/test/get_version_info_test.go b/cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go similarity index 79% rename from cmd/soroban-rpc/internal/test/get_version_info_test.go rename to cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go index 25a43f62..c7ebf413 100644 --- a/cmd/soroban-rpc/internal/test/get_version_info_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go @@ -1,4 +1,4 @@ -package test +package integrationtest import ( "context" @@ -6,7 +6,10 @@ import ( "os/exec" "testing" + "github.com/stretchr/testify/require" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stretchr/testify/assert" @@ -14,11 +17,11 @@ import ( ) func TestGetVersionInfoSucceeds(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) version, commitHash, buildTimeStamp := config.Version, config.CommitHash, config.BuildTimestamp - populateVersionInfo(test) + populateVersionInfo(t) // reset to previous config values t.Cleanup(func() { @@ -36,26 +39,23 @@ func TestGetVersionInfoSucceeds(t *testing.T) { assert.Equal(t, config.Version, result.Version) assert.Equal(t, config.BuildTimestamp, result.BuildTimestamp) assert.Equal(t, config.CommitHash, result.CommitHash) - assert.Equal(t, test.protocolVersion, result.ProtocolVersion) + assert.Equal(t, test.GetProtocolVersion(), result.ProtocolVersion) assert.NotEmpty(t, result.CaptiveCoreVersion) } // Runs git commands to fetch version information -func populateVersionInfo(test *Test) { +func populateVersionInfo(t *testing.T) { execFunction := func(command string, args ...string) string { cmd := exec.Command(command, args...) - test.t.Log("Running", cmd.Env, cmd.Args) + t.Log("Running", cmd.Env, cmd.Args) out, innerErr := cmd.Output() if exitErr, ok := innerErr.(*exec.ExitError); ok { fmt.Printf("stdout:\n%s\n", string(out)) fmt.Printf("stderr:\n%s\n", string(exitErr.Stderr)) } - - if innerErr != nil { - test.t.Fatalf("Command %s failed: %v", cmd.Env, innerErr) - } + require.NoError(t, innerErr) return string(out) } diff --git a/cmd/soroban-rpc/internal/test/health_test.go b/cmd/soroban-rpc/internal/integrationtest/health_test.go similarity index 60% rename from cmd/soroban-rpc/internal/test/health_test.go rename to cmd/soroban-rpc/internal/integrationtest/health_test.go index adae006c..2fe9ab86 100644 --- a/cmd/soroban-rpc/internal/test/health_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/health_test.go @@ -1,24 +1,19 @@ -package test +package integrationtest import ( - "context" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) func TestHealth(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - var result methods.HealthCheckResult - if err := client.CallResult(context.Background(), "getHealth", nil, &result); err != nil { - t.Fatalf("rpc call failed: %v", err) - } + test := infrastructure.NewTest(t, nil) + result, err := test.GetRPCHealth() + require.NoError(t, err) assert.Equal(t, "healthy", result.Status) assert.Equal(t, uint32(ledgerbucketwindow.OneDayOfLedgers), result.LedgerRetentionWindow) assert.Greater(t, result.OldestLedger, uint32(0)) diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go new file mode 100644 index 00000000..1f536b43 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go @@ -0,0 +1,161 @@ +package infrastructure + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/creachadair/jrpc2" + "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/stellarcore" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" +) + +func getTransaction(t *testing.T, client *jrpc2.Client, hash string) methods.GetTransactionResponse { + var result methods.GetTransactionResponse + for i := 0; i < 60; i++ { + request := methods.GetTransactionRequest{Hash: hash} + err := client.CallResult(context.Background(), "getTransaction", request, &result) + assert.NoError(t, err) + + if result.Status == methods.TransactionStatusNotFound { + time.Sleep(time.Second) + continue + } + + return result + } + t.Fatal("GetTransaction timed out") + return result +} + +func SendSuccessfulTransaction(t *testing.T, client *jrpc2.Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { + tx, err := transaction.Sign(StandaloneNetworkPassphrase, kp) + assert.NoError(t, err) + b64, err := tx.Base64() + assert.NoError(t, err) + + request := methods.SendTransactionRequest{Transaction: b64} + var result methods.SendTransactionResponse + assert.NoError(t, client.CallResult(context.Background(), "sendTransaction", request, &result)) + + expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) + assert.NoError(t, err) + + assert.Equal(t, expectedHashHex, result.Hash) + if !assert.Equal(t, stellarcore.TXStatusPending, result.Status) { + var txResult xdr.TransactionResult + err := xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &txResult) + assert.NoError(t, err) + t.Logf("error: %#v\n", txResult) + } + assert.NotZero(t, result.LatestLedger) + assert.NotZero(t, result.LatestLedgerCloseTime) + + response := getTransaction(t, client, expectedHashHex) + if !assert.Equal(t, methods.TransactionStatusSuccess, response.Status) { + var txResult xdr.TransactionResult + assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &txResult)) + t.Logf("error: %#v\n", txResult) + + var txMeta xdr.TransactionMeta + assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXdr, &txMeta)) + + if txMeta.V == 3 && txMeta.V3.SorobanMeta != nil { + if len(txMeta.V3.SorobanMeta.Events) > 0 { + t.Log("Contract events:") + for i, e := range txMeta.V3.SorobanMeta.Events { + t.Logf(" %d: %s\n", i, e) + } + } + + if len(txMeta.V3.SorobanMeta.DiagnosticEvents) > 0 { + t.Log("Diagnostic events:") + for i, d := range txMeta.V3.SorobanMeta.DiagnosticEvents { + t.Logf(" %d: %s\n", i, d) + } + } + } + } + + require.NotNil(t, response.ResultXdr) + assert.Greater(t, response.Ledger, result.LatestLedger) + assert.Greater(t, response.LedgerCloseTime, result.LatestLedgerCloseTime) + assert.GreaterOrEqual(t, response.LatestLedger, response.Ledger) + assert.GreaterOrEqual(t, response.LatestLedgerCloseTime, response.LedgerCloseTime) + return response +} + +func SimulateTransactionFromTxParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) methods.SimulateTransactionResponse { + savedAutoIncrement := params.IncrementSequenceNum + params.IncrementSequenceNum = false + tx, err := txnbuild.NewTransaction(params) + require.NoError(t, err) + params.IncrementSequenceNum = savedAutoIncrement + txB64, err := tx.Base64() + require.NoError(t, err) + request := methods.SimulateTransactionRequest{Transaction: txB64} + var response methods.SimulateTransactionResponse + err = client.CallResult(context.Background(), "simulateTransaction", request, &response) + require.NoError(t, err) + return response +} + +func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.TransactionParams, response methods.SimulateTransactionResponse) txnbuild.TransactionParams { + if !assert.Empty(t, response.Error) { + fmt.Println(response.Error) + } + var transactionData xdr.SorobanTransactionData + err := xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) + require.NoError(t, err) + + op := params.Operations[0] + switch v := op.(type) { + case *txnbuild.InvokeHostFunction: + require.Len(t, response.Results, 1) + v.Ext = xdr.TransactionExt{ + V: 1, + SorobanData: &transactionData, + } + var auth []xdr.SorobanAuthorizationEntry + for _, b64 := range response.Results[0].Auth { + var a xdr.SorobanAuthorizationEntry + err := xdr.SafeUnmarshalBase64(b64, &a) + assert.NoError(t, err) + auth = append(auth, a) + } + v.Auth = auth + case *txnbuild.ExtendFootprintTtl: + require.Len(t, response.Results, 0) + v.Ext = xdr.TransactionExt{ + V: 1, + SorobanData: &transactionData, + } + case *txnbuild.RestoreFootprint: + require.Len(t, response.Results, 0) + v.Ext = xdr.TransactionExt{ + V: 1, + SorobanData: &transactionData, + } + default: + t.Fatalf("Wrong operation type %v", op) + } + + params.Operations = []txnbuild.Operation{op} + + params.BaseFee += response.MinResourceFee + return params +} + +func PreflightTransactionParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { + response := SimulateTransactionFromTxParams(t, client, params) + // The preamble should be zero except for the special restore case + assert.Nil(t, response.RestorePreamble) + return PreflightTransactionParamsLocally(t, params, response) +} diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go new file mode 100644 index 00000000..7fa49d90 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go @@ -0,0 +1,118 @@ +package infrastructure + +import ( + "crypto/sha256" + "fmt" + "os" + "path" + "testing" + + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/require" +) + +var testSalt = sha256.Sum256([]byte("a1")) + +func GetHelloWorldContract() []byte { + testDirName := GetCurrentDirectory() + contractFile := path.Join(testDirName, helloWorldContractPath) + ret, err := os.ReadFile(contractFile) + if err != nil { + str := fmt.Sprintf( + "unable to read test_hello_world.wasm (%v) please run `make build-test-wasms` at the project root directory", + err) + panic(str) + } + return ret +} + +func CreateInvokeHostOperation(sourceAccount string, contractID xdr.Hash, method string, args ...xdr.ScVal) *txnbuild.InvokeHostFunction { + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contractID, + }, + FunctionName: xdr.ScSymbol(method), + Args: args, + }, + }, + Auth: nil, + SourceAccount: sourceAccount, + } +} + +func getContractID(t *testing.T, sourceAccount string, salt [32]byte, networkPassphrase string) [32]byte { + sourceAccountID := xdr.MustAddress(sourceAccount) + preImage := xdr.HashIdPreimage{ + Type: xdr.EnvelopeTypeEnvelopeTypeContractId, + ContractId: &xdr.HashIdPreimageContractId{ + NetworkId: sha256.Sum256([]byte(networkPassphrase)), + ContractIdPreimage: xdr.ContractIdPreimage{ + Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAddress, + FromAddress: &xdr.ContractIdPreimageFromAddress{ + Address: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeAccount, + AccountId: &sourceAccountID, + }, + Salt: salt, + }, + }, + }, + } + + xdrPreImageBytes, err := preImage.MarshalBinary() + require.NoError(t, err) + hashedContractID := sha256.Sum256(xdrPreImageBytes) + return hashedContractID +} + +func CreateUploadHelloWorldOperation(sourceAccount string) *txnbuild.InvokeHostFunction { + return CreateUploadWasmOperation(sourceAccount, GetHelloWorldContract()) +} + +func CreateUploadWasmOperation(sourceAccount string, contractCode []byte) *txnbuild.InvokeHostFunction { + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm, + Wasm: &contractCode, + }, + SourceAccount: sourceAccount, + } +} + +func CreateCreateHelloWorldContractOperation(sourceAccount string) *txnbuild.InvokeHostFunction { + contractHash := xdr.Hash(sha256.Sum256(GetHelloWorldContract())) + salt := xdr.Uint256(testSalt) + return createCreateContractOperation(sourceAccount, salt, contractHash) +} + +func createCreateContractOperation(sourceAccount string, salt xdr.Uint256, contractHash xdr.Hash) *txnbuild.InvokeHostFunction { + sourceAccountID := xdr.MustAddress(sourceAccount) + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, + CreateContract: &xdr.CreateContractArgs{ + ContractIdPreimage: xdr.ContractIdPreimage{ + Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAddress, + FromAddress: &xdr.ContractIdPreimageFromAddress{ + Address: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeAccount, + AccountId: &sourceAccountID, + }, + Salt: salt, + }, + }, + Executable: xdr.ContractExecutable{ + Type: xdr.ContractExecutableTypeContractExecutableWasm, + WasmHash: &contractHash, + }, + }, + }, + Auth: []xdr.SorobanAuthorizationEntry{}, + SourceAccount: sourceAccount, + } +} diff --git a/cmd/soroban-rpc/internal/test/captive-core-integration-tests.cfg b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg similarity index 100% rename from cmd/soroban-rpc/internal/test/captive-core-integration-tests.cfg rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg diff --git a/cmd/soroban-rpc/internal/test/core-start.sh b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh similarity index 100% rename from cmd/soroban-rpc/internal/test/core-start.sh rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh diff --git a/cmd/soroban-rpc/internal/test/docker-compose.rpc.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml similarity index 100% rename from cmd/soroban-rpc/internal/test/docker-compose.rpc.yml rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml diff --git a/cmd/soroban-rpc/internal/test/docker-compose.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml similarity index 100% rename from cmd/soroban-rpc/internal/test/docker-compose.yml rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml diff --git a/cmd/soroban-rpc/internal/test/stellar-core-integration-tests.cfg b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg similarity index 100% rename from cmd/soroban-rpc/internal/test/stellar-core-integration-tests.cfg rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg diff --git a/cmd/soroban-rpc/internal/test/integration.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go similarity index 68% rename from cmd/soroban-rpc/internal/test/integration.go rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go index 531a162f..40b4da3e 100644 --- a/cmd/soroban-rpc/internal/test/integration.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -1,14 +1,14 @@ -package test +package infrastructure import ( "context" + "crypto/sha256" "fmt" "os" "os/exec" "os/signal" "path" "path/filepath" - "runtime" "strconv" "strings" "sync" @@ -20,7 +20,10 @@ import ( "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/clients/stellarcore" "github.com/stellar/go/keypair" + proto "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" @@ -35,12 +38,12 @@ const ( MaxSupportedProtocolVersion = 21 StellarCoreArchivePort = 1570 stellarCorePort = 11626 - friendbotURL = "http://localhost:8000/friendbot" + FriendbotURL = "http://localhost:8000/friendbot" // Needed when Core is run with ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true checkpointFrequency = 8 sorobanRPCPort = 8000 adminPort = 8080 - helloWorldContractPath = "../../../../wasms/test_hello_world.wasm" + helloWorldContractPath = "../../../../../wasms/test_hello_world.wasm" ) type TestConfig struct { @@ -103,7 +106,7 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { } i.runComposeCommand("up", "--detach", "--quiet-pull", "--no-color") if i.runRPCInContainer() { - cmd := i.getComposeCommand("logs", "-f", "rpc") + cmd := i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr require.NoError(t, cmd.Start()) @@ -136,33 +139,30 @@ func (i *Test) MasterAccount() txnbuild.Account { return i.masterAccount } -func (i *Test) sorobanRPCURL() string { +func (i *Test) GetSorobanRPCURL() string { return fmt.Sprintf("http://localhost:%d", sorobanRPCPort) } -func (i *Test) adminURL() string { +func (i *Test) GetAdminURL() string { return fmt.Sprintf("http://localhost:%d", adminPort) } +func (i *Test) getCoreInfo() (*proto.InfoResponse, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + return i.coreClient.Info(ctx) +} + func (i *Test) waitForCheckpoint() { - i.t.Log("Waiting for core to be up...") - for t := 30 * time.Second; t >= 0; t -= time.Second { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - info, err := i.coreClient.Info(ctx) - cancel() - if err != nil { - i.t.Logf("could not obtain info response: %v", err) - time.Sleep(time.Second) - continue - } - if info.Info.Ledger.Num <= checkpointFrequency { - i.t.Logf("checkpoint not reached yet: %v", info) - time.Sleep(time.Second) - continue - } - return - } - i.t.Fatal("Core could not reach checkpoint ledger after 30s") + i.t.Log("Waiting for checkpoint...") + require.Eventually(i.t, + func() bool { + info, err := i.getCoreInfo() + return err == nil && info.Info.Ledger.Num > checkpointFrequency + }, + 30*time.Second, + time.Second, + ) } func (i *Test) getRPConfig(sqlitePath string) map[string]string { @@ -190,7 +190,7 @@ func (i *Test) getRPConfig(sqlitePath string) map[string]string { archiveURL = i.historyArchiveURL } - captiveCoreConfigPath := path.Join(GetCurrentDirectory(), "captive-core-integration-tests.cfg") + captiveCoreConfigPath := path.Join(GetCurrentDirectory(), "docker", "captive-core-integration-tests.cfg") bindHost := "localhost" stellarCoreURL := fmt.Sprintf("http://localhost:%d", stellarCorePort) if i.runRPCInContainer() { @@ -219,7 +219,7 @@ func (i *Test) getRPConfig(sqlitePath string) map[string]string { "CAPTIVE_CORE_CONFIG_PATH": captiveCoreConfigPath, "CAPTIVE_CORE_STORAGE_PATH": captiveCoreStoragePath, "STELLAR_CAPTIVE_CORE_HTTP_PORT": "0", - "FRIENDBOT_URL": friendbotURL, + "FRIENDBOT_URL": FriendbotURL, "NETWORK_PASSPHRASE": StandaloneNetworkPassphrase, "HISTORY_ARCHIVE_URLS": archiveURL, "LOG_LEVEL": "debug", @@ -235,39 +235,39 @@ func (i *Test) getRPConfig(sqlitePath string) map[string]string { func (i *Test) waitForRPC() { i.t.Log("Waiting for RPC to be healthy...") - - // This is needed because if https://github.com/creachadair/jrpc2/issues/118 + // This is needed because of https://github.com/creachadair/jrpc2/issues/118 refreshClient := func() { if i.rpcClient != nil { i.rpcClient.Close() } - ch := jhttp.NewChannel(i.sorobanRPCURL(), nil) + ch := jhttp.NewChannel(i.GetSorobanRPCURL(), nil) i.rpcClient = jrpc2.NewClient(ch, nil) } - - var result methods.HealthCheckResult - for t := 30; t >= 0; t-- { - refreshClient() - err := i.rpcClient.CallResult(context.Background(), "getHealth", nil, &result) - if err == nil { - if result.Status == "healthy" { - i.t.Log("RPC is healthy") - return - } - } - i.t.Log("RPC still unhealthy", err, result.Status) - time.Sleep(time.Second) - } - - i.t.Fatal("RPC failed to get healthy in 30 seconds") + require.Eventually(i.t, + func() bool { + refreshClient() + result, err := i.GetRPCHealth() + return err == nil && result.Status == "healthy" + }, + 30*time.Second, + time.Second, + ) } func (i *Test) createRPCContainerMountDir(rpcConfig map[string]string) string { mountDir := i.t.TempDir() + + getOldVersionCaptiveCoreConfigVersion := func(dir string) ([]byte, error) { + cmd := exec.Command("git", "show", fmt.Sprintf("v%s:./%s/captive-core-integration-tests.cfg", i.rpcContainerVersion, dir)) + cmd.Dir = GetCurrentDirectory() + return cmd.Output() + } + // Get old version of captive-core-integration-tests.cfg - cmd := exec.Command("git", "show", fmt.Sprintf("v%s:./captive-core-integration-tests.cfg", i.rpcContainerVersion)) - cmd.Dir = GetCurrentDirectory() - out, err := cmd.Output() + out, err := getOldVersionCaptiveCoreConfigVersion("docker") + if err != nil { + out, err = getOldVersionCaptiveCoreConfigVersion("../../test") + } require.NoError(i.t, err) // replace ADDRESS="localhost" by ADDRESS="core", so that the container can find core @@ -299,10 +299,10 @@ func (i *Test) createDaemon(env map[string]string) *daemon.Daemon { } func (i *Test) getComposeCommand(args ...string) *exec.Cmd { - integrationYaml := filepath.Join(GetCurrentDirectory(), "docker-compose.yml") + integrationYaml := filepath.Join(GetCurrentDirectory(), "docker", "docker-compose.yml") configFiles := []string{"-f", integrationYaml} if i.runRPCInContainer() { - rpcYaml := filepath.Join(GetCurrentDirectory(), "docker-compose.rpc.yml") + rpcYaml := filepath.Join(GetCurrentDirectory(), "docker", "docker-compose.rpc.yml") configFiles = append(configFiles, "-f", rpcYaml) } cmdline := append(configFiles, args...) @@ -336,8 +336,8 @@ func (i *Test) runComposeCommand(args ...string) { i.t.Log("Running", cmd.Args) out, innerErr := cmd.Output() if exitErr, ok := innerErr.(*exec.ExitError); ok { - fmt.Printf("stdout:\n%s\n", string(out)) - fmt.Printf("stderr:\n%s\n", string(exitErr.Stderr)) + i.t.Log("stdout\n:", string(out)) + i.t.Log("stderr:\n", string(exitErr.Stderr)) } if innerErr != nil { @@ -388,33 +388,25 @@ func (i *Test) Shutdown() { // Wait for core to be up and manually close the first ledger func (i *Test) waitForCore() { i.t.Log("Waiting for core to be up...") - for t := 30 * time.Second; t >= 0; t -= time.Second { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - _, err := i.coreClient.Info(ctx) - cancel() - if err != nil { - i.t.Logf("Core is not up: %v", err) - time.Sleep(time.Second) - continue - } - break - } + require.Eventually(i.t, + func() bool { + _, err := i.getCoreInfo() + return err == nil + }, + 30*time.Second, + time.Second, + ) i.UpgradeProtocol(i.protocolVersion) - for t := 0; t < 5; t++ { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - info, err := i.coreClient.Info(ctx) - cancel() - if err != nil || !info.IsSynced() { - i.t.Logf("Core is still not synced: %v %v", err, info) - time.Sleep(time.Second) - continue - } - i.t.Log("Core is up.") - return - } - i.t.Fatal("Core could not sync after 30s") + require.Eventually(i.t, + func() bool { + info, err := i.getCoreInfo() + return err == nil && info.IsSynced() + }, + 30*time.Second, + time.Second, + ) } // UpgradeProtocol arms Core with upgrade and blocks until protocol is upgraded. @@ -422,28 +414,16 @@ func (i *Test) UpgradeProtocol(version uint32) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) err := i.coreClient.Upgrade(ctx, int(version)) cancel() - if err != nil { - i.t.Fatalf("could not upgrade protocol: %v", err) - } - - for t := 0; t < 10; t++ { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - info, err := i.coreClient.Info(ctx) - cancel() - if err != nil { - i.t.Logf("could not obtain info response: %v", err) - time.Sleep(time.Second) - continue - } - - if info.Info.Ledger.Version == int(version) { - i.t.Logf("Protocol upgraded to: %d", info.Info.Ledger.Version) - return - } - time.Sleep(time.Second) - } + require.NoError(i.t, err) - i.t.Fatalf("could not upgrade protocol in 10s") + require.Eventually(i.t, + func() bool { + info, err := i.getCoreInfo() + return err == nil && info.Info.Ledger.Version == int(version) + }, + 10*time.Second, + time.Second, + ) } func (i *Test) StopRPC() { @@ -456,10 +436,71 @@ func (i *Test) StopRPC() { } } -//go:noinline -func GetCurrentDirectory() string { - _, currentFilename, _, _ := runtime.Caller(1) - return filepath.Dir(currentFilename) +func (i *Test) GetProtocolVersion() uint32 { + return i.protocolVersion +} + +func (i *Test) GetDaemon() *daemon.Daemon { + return i.daemon +} + +func (i *Test) SendMasterOperation(op txnbuild.Operation) methods.GetTransactionResponse { + params := CreateTransactionParams(i.MasterAccount(), op) + tx, err := txnbuild.NewTransaction(params) + assert.NoError(i.t, err) + return i.SendMasterTransaction(tx) +} + +func (i *Test) SendMasterTransaction(tx *txnbuild.Transaction) methods.GetTransactionResponse { + kp := keypair.Root(StandaloneNetworkPassphrase) + return SendSuccessfulTransaction(i.t, i.rpcClient, kp, tx) +} + +func (i *Test) GetTransaction(hash string) methods.GetTransactionResponse { + return getTransaction(i.t, i.rpcClient, hash) +} + +func (i *Test) PreflightAndSendMasterOperation(op txnbuild.Operation) methods.GetTransactionResponse { + params := CreateTransactionParams( + i.MasterAccount(), + op, + ) + params = PreflightTransactionParams(i.t, i.rpcClient, params) + tx, err := txnbuild.NewTransaction(params) + assert.NoError(i.t, err) + return i.SendMasterTransaction(tx) +} + +func (i *Test) UploadHelloWorldContract() (methods.GetTransactionResponse, xdr.Hash) { + contractBinary := GetHelloWorldContract() + return i.uploadContract(contractBinary) +} + +func (i *Test) uploadContract(contractBinary []byte) (methods.GetTransactionResponse, xdr.Hash) { + contractHash := xdr.Hash(sha256.Sum256(contractBinary)) + op := CreateUploadWasmOperation(i.MasterAccount().GetAccountID(), contractBinary) + return i.PreflightAndSendMasterOperation(op), contractHash +} + +func (i *Test) CreateHelloWorldContract() (methods.GetTransactionResponse, [32]byte, xdr.Hash) { + contractBinary := GetHelloWorldContract() + _, contractHash := i.uploadContract(contractBinary) + salt := xdr.Uint256(testSalt) + account := i.MasterAccount().GetAccountID() + op := createCreateContractOperation(account, salt, contractHash) + contractID := getContractID(i.t, account, salt, StandaloneNetworkPassphrase) + return i.PreflightAndSendMasterOperation(op), contractID, contractHash +} + +func (i *Test) InvokeHostFunc(contractID xdr.Hash, method string, args ...xdr.ScVal) methods.GetTransactionResponse { + op := CreateInvokeHostOperation(i.MasterAccount().GetAccountID(), contractID, method, args...) + return i.PreflightAndSendMasterOperation(op) +} + +func (i *Test) GetRPCHealth() (methods.HealthCheckResult, error) { + var result methods.HealthCheckResult + err := i.rpcClient.CallResult(context.Background(), "getHealth", nil, &result) + return result, err } func GetCoreMaxSupportedProtocol() uint32 { diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go new file mode 100644 index 00000000..a2aae8ea --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go @@ -0,0 +1,26 @@ +package infrastructure + +import ( + "path/filepath" + "runtime" + + "github.com/stellar/go/txnbuild" +) + +//go:noinline +func GetCurrentDirectory() string { + _, currentFilename, _, _ := runtime.Caller(1) + return filepath.Dir(currentFilename) +} + +func CreateTransactionParams(account txnbuild.Account, op txnbuild.Operation) txnbuild.TransactionParams { + return txnbuild.TransactionParams{ + SourceAccount: account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{op}, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + } +} diff --git a/cmd/soroban-rpc/internal/test/metrics_test.go b/cmd/soroban-rpc/internal/integrationtest/metrics_test.go similarity index 64% rename from cmd/soroban-rpc/internal/test/metrics_test.go rename to cmd/soroban-rpc/internal/integrationtest/metrics_test.go index 57211ad1..6d0cf1ae 100644 --- a/cmd/soroban-rpc/internal/test/metrics_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/metrics_test.go @@ -1,4 +1,4 @@ -package test +package integrationtest import ( "fmt" @@ -15,11 +15,14 @@ import ( "github.com/stretchr/testify/require" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" ) func TestMetrics(t *testing.T) { - test := NewTest(t, nil) - metrics := getMetrics(test) + test := infrastructure.NewTest(t, nil) + metricsURL, err := url.JoinPath(test.GetAdminURL(), "/metrics") + require.NoError(t, err) + metrics := getMetrics(t, metricsURL) buildMetric := fmt.Sprintf( "soroban_rpc_build_info{branch=\"%s\",build_timestamp=\"%s\",commit=\"%s\",goversion=\"%s\",version=\"%s\"} 1", config.Branch, @@ -30,12 +33,13 @@ func TestMetrics(t *testing.T) { ) require.Contains(t, metrics, buildMetric) - logger := test.daemon.Logger() - err := errors.Errorf("test-error") + daemon := test.GetDaemon() + logger := daemon.Logger() + err = errors.Errorf("test-error") logger.WithError(err).Error("test error 1") logger.WithError(err).Error("test error 2") - metricFamilies, err := test.daemon.MetricsRegistry().Gather() + metricFamilies, err := daemon.MetricsRegistry().Gather() assert.NoError(t, err) var metric *io_prometheus_client.MetricFamily for _, mf := range metricFamilies { @@ -49,13 +53,11 @@ func TestMetrics(t *testing.T) { assert.GreaterOrEqual(t, val, 2.0) } -func getMetrics(test *Test) string { - metricsURL, err := url.JoinPath(test.adminURL(), "/metrics") - require.NoError(test.t, err) - response, err := http.Get(metricsURL) - require.NoError(test.t, err) +func getMetrics(t *testing.T, url string) string { + response, err := http.Get(url) + require.NoError(t, err) responseBytes, err := io.ReadAll(response.Body) - require.NoError(test.t, err) - require.NoError(test.t, response.Body.Close()) + require.NoError(t, err) + require.NoError(t, response.Body.Close()) return string(responseBytes) } diff --git a/cmd/soroban-rpc/internal/test/migrate_test.go b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go similarity index 68% rename from cmd/soroban-rpc/internal/test/migrate_test.go rename to cmd/soroban-rpc/internal/integrationtest/migrate_test.go index c8fcc7ec..9e0a83de 100644 --- a/cmd/soroban-rpc/internal/test/migrate_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go @@ -1,4 +1,4 @@ -package test +package integrationtest import ( "context" @@ -8,11 +8,9 @@ import ( "strings" "testing" - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) @@ -20,8 +18,8 @@ import ( // We cannot test prior protocol versions since the Transaction XDR used for the test could be incompatible // TODO: find a way to test migrations between protocols func TestMigrate(t *testing.T) { - if GetCoreMaxSupportedProtocol() != MaxSupportedProtocolVersion { - t.Skip("Only test this for the latest protocol: ", MaxSupportedProtocolVersion) + if infrastructure.GetCoreMaxSupportedProtocol() != infrastructure.MaxSupportedProtocolVersion { + t.Skip("Only test this for the latest protocol: ", infrastructure.MaxSupportedProtocolVersion) } for _, originVersion := range getCurrentProtocolReleasedVersions(t) { if originVersion == "21.1.0" { @@ -41,37 +39,19 @@ func TestMigrate(t *testing.T) { func testMigrateFromVersion(t *testing.T, version string) { sqliteFile := filepath.Join(t.TempDir(), "soroban-rpc.db") - it := NewTest(t, &TestConfig{ + test := infrastructure.NewTest(t, &infrastructure.TestConfig{ UseReleasedRPCVersion: version, UseSQLitePath: sqliteFile, }) - client := it.GetRPCLient() + client := test.GetRPCLient() // Submit an event-logging transaction in the version to migrate from - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - submitTransactionResponse := sendSuccessfulTransaction(t, client, kp, tx) + submitTransactionResponse, _ := test.UploadHelloWorldContract() // Run the current RPC version, but the previous network and sql database (causing a data migration if needed) - it.StopRPC() - it = NewTest(t, &TestConfig{UseSQLitePath: sqliteFile}) + test.StopRPC() + test = infrastructure.NewTest(t, &infrastructure.TestConfig{UseSQLitePath: sqliteFile}) // make sure that the transaction submitted before and its events exist in current RPC var transactionsResult methods.GetTransactionsResponse @@ -81,7 +61,7 @@ func testMigrateFromVersion(t *testing.T, version string) { Limit: 1, }, } - err = client.CallResult(context.Background(), "getTransactions", getTransactions, &transactionsResult) + err := client.CallResult(context.Background(), "getTransactions", getTransactions, &transactionsResult) require.NoError(t, err) require.Equal(t, 1, len(transactionsResult.Transactions)) require.Equal(t, submitTransactionResponse.Ledger, transactionsResult.Transactions[0].Ledger) @@ -100,9 +80,9 @@ func testMigrateFromVersion(t *testing.T, version string) { } func getCurrentProtocolReleasedVersions(t *testing.T) []string { - protocolStr := strconv.Itoa(MaxSupportedProtocolVersion) + protocolStr := strconv.Itoa(infrastructure.MaxSupportedProtocolVersion) cmd := exec.Command("git", "tag") - cmd.Dir = GetCurrentDirectory() + cmd.Dir = infrastructure.GetCurrentDirectory() out, err := cmd.Output() require.NoError(t, err) tags := strings.Split(string(out), "\n") diff --git a/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go new file mode 100644 index 00000000..4429bfca --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go @@ -0,0 +1,631 @@ +package integrationtest + +import ( + "context" + "crypto/sha256" + "testing" + "time" + + "github.com/creachadair/jrpc2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" +) + +func TestSimulateTransactionSucceeds(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + contractBinary := infrastructure.GetHelloWorldContract() + params := infrastructure.CreateTransactionParams( + test.MasterAccount(), + infrastructure.CreateUploadWasmOperation(test.MasterAccount().GetAccountID(), contractBinary), + ) + client := test.GetRPCLient() + result := infrastructure.SimulateTransactionFromTxParams(t, client, params) + + contractHash := sha256.Sum256(contractBinary) + contractHashBytes := xdr.ScBytes(contractHash[:]) + expectedXdr := xdr.ScVal{Type: xdr.ScValTypeScvBytes, Bytes: &contractHashBytes} + assert.Greater(t, result.LatestLedger, uint32(0)) + assert.Greater(t, result.Cost.CPUInstructions, uint64(0)) + assert.Greater(t, result.Cost.MemoryBytes, uint64(0)) + + expectedTransactionData := xdr.SorobanTransactionData{ + Resources: xdr.SorobanResources{ + Footprint: xdr.LedgerFootprint{ + ReadWrite: []xdr.LedgerKey{ + { + Type: xdr.LedgerEntryTypeContractCode, + ContractCode: &xdr.LedgerKeyContractCode{ + Hash: xdr.Hash(contractHash), + }, + }, + }, + }, + Instructions: 4378462, + ReadBytes: 0, + WriteBytes: 7048, + }, + // the resulting fee is derived from the compute factors and a default padding is applied to instructions by preflight + // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture + // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo + // in the test. + ResourceFee: 149755, + } + + // First, decode and compare the transaction data so we get a decent diff if it fails. + var transactionData xdr.SorobanTransactionData + err := xdr.SafeUnmarshalBase64(result.TransactionData, &transactionData) + assert.NoError(t, err) + assert.Equal(t, expectedTransactionData.Resources.Footprint, transactionData.Resources.Footprint) + assert.InDelta(t, uint32(expectedTransactionData.Resources.Instructions), uint32(transactionData.Resources.Instructions), 3200000) + assert.InDelta(t, uint32(expectedTransactionData.Resources.ReadBytes), uint32(transactionData.Resources.ReadBytes), 10) + assert.InDelta(t, uint32(expectedTransactionData.Resources.WriteBytes), uint32(transactionData.Resources.WriteBytes), 300) + assert.InDelta(t, int64(expectedTransactionData.ResourceFee), int64(transactionData.ResourceFee), 40000) + + // Then decode and check the result xdr, separately so we get a decent diff if it fails. + assert.Len(t, result.Results, 1) + var resultXdr xdr.ScVal + err = xdr.SafeUnmarshalBase64(result.Results[0].XDR, &resultXdr) + assert.NoError(t, err) + assert.Equal(t, expectedXdr, resultXdr) + + // Check state diff + assert.Len(t, result.StateChanges, 1) + assert.Nil(t, result.StateChanges[0].Before) + assert.NotNil(t, result.StateChanges[0].After) + assert.Equal(t, methods.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type) + var after xdr.LedgerEntry + assert.NoError(t, xdr.SafeUnmarshalBase64(*result.StateChanges[0].After, &after)) + assert.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type) + entryKey, err := after.LedgerKey() + assert.NoError(t, err) + entryKeyB64, err := xdr.MarshalBase64(entryKey) + assert.NoError(t, err) + assert.Equal(t, entryKeyB64, result.StateChanges[0].Key) + + // test operation which does not have a source account + params = infrastructure.CreateTransactionParams(test.MasterAccount(), + infrastructure.CreateUploadWasmOperation("", contractBinary), + ) + require.NoError(t, err) + + resultForRequestWithoutOpSource := infrastructure.SimulateTransactionFromTxParams(t, client, params) + // Let's not compare the latest ledger since it may change + result.LatestLedger = resultForRequestWithoutOpSource.LatestLedger + assert.Equal(t, result, resultForRequestWithoutOpSource) + + // test that operation source account takes precedence over tx source account + params = infrastructure.CreateTransactionParams( + &txnbuild.SimpleAccount{ + AccountID: keypair.Root("test passphrase").Address(), + Sequence: 0, + }, + infrastructure.CreateUploadWasmOperation("", contractBinary), + ) + + resultForRequestWithDifferentTxSource := infrastructure.SimulateTransactionFromTxParams(t, client, params) + assert.GreaterOrEqual(t, resultForRequestWithDifferentTxSource.LatestLedger, result.LatestLedger) + // apart from latest ledger the response should be the same + resultForRequestWithDifferentTxSource.LatestLedger = result.LatestLedger + assert.Equal(t, result, resultForRequestWithDifferentTxSource) +} + +func TestSimulateTransactionWithAuth(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + test.UploadHelloWorldContract() + + deployContractOp := infrastructure.CreateCreateHelloWorldContractOperation(test.MasterAccount().GetAccountID()) + deployContractParams := infrastructure.CreateTransactionParams( + test.MasterAccount(), + deployContractOp, + ) + + client := test.GetRPCLient() + response := infrastructure.SimulateTransactionFromTxParams(t, client, deployContractParams) + require.NotEmpty(t, response.Results) + require.Len(t, response.Results[0].Auth, 1) + require.Empty(t, deployContractOp.Auth) + + var auth xdr.SorobanAuthorizationEntry + assert.NoError(t, xdr.SafeUnmarshalBase64(response.Results[0].Auth[0], &auth)) + require.Equal(t, auth.Credentials.Type, xdr.SorobanCredentialsTypeSorobanCredentialsSourceAccount) + deployContractOp.Auth = append(deployContractOp.Auth, auth) + deployContractParams.Operations = []txnbuild.Operation{deployContractOp} + + // preflight deployContractOp with auth + deployContractParams = infrastructure.PreflightTransactionParams(t, client, deployContractParams) + tx, err := txnbuild.NewTransaction(deployContractParams) + assert.NoError(t, err) + test.SendMasterTransaction(tx) +} + +func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + _, contractID, contractHash := test.CreateHelloWorldContract() + + contractFnParameterSym := xdr.ScSymbol("world") + authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + authAccountIDArg := xdr.MustAddress(authAddrArg) + test.SendMasterOperation(&txnbuild.CreateAccount{ + Destination: authAddrArg, + Amount: "100000", + SourceAccount: test.MasterAccount().GetAccountID(), + }) + params := infrastructure.CreateTransactionParams( + test.MasterAccount(), + infrastructure.CreateInvokeHostOperation( + test.MasterAccount().GetAccountID(), + contractID, + "auth", + xdr.ScVal{ + Type: xdr.ScValTypeScvAddress, + Address: &xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeAccount, + AccountId: &authAccountIDArg, + }, + }, + xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &contractFnParameterSym, + }, + ), + ) + tx, err := txnbuild.NewTransaction(params) + assert.NoError(t, err) + + txB64, err := tx.Base64() + assert.NoError(t, err) + + request := methods.SimulateTransactionRequest{Transaction: txB64} + var response methods.SimulateTransactionResponse + err = test.GetRPCLient().CallResult(context.Background(), "simulateTransaction", request, &response) + assert.NoError(t, err) + assert.Empty(t, response.Error) + + // check the result + assert.Len(t, response.Results, 1) + var obtainedResult xdr.ScVal + err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) + assert.NoError(t, err) + assert.Equal(t, xdr.ScValTypeScvAddress, obtainedResult.Type) + require.NotNil(t, obtainedResult.Address) + assert.Equal(t, authAccountIDArg, obtainedResult.Address.MustAccountId()) + + // check the footprint + var obtainedTransactionData xdr.SorobanTransactionData + err = xdr.SafeUnmarshalBase64(response.TransactionData, &obtainedTransactionData) + obtainedFootprint := obtainedTransactionData.Resources.Footprint + assert.NoError(t, err) + assert.Len(t, obtainedFootprint.ReadWrite, 1) + assert.Len(t, obtainedFootprint.ReadOnly, 3) + ro0 := obtainedFootprint.ReadOnly[0] + assert.Equal(t, xdr.LedgerEntryTypeAccount, ro0.Type) + assert.Equal(t, authAddrArg, ro0.Account.AccountId.Address()) + ro1 := obtainedFootprint.ReadOnly[1] + assert.Equal(t, xdr.LedgerEntryTypeContractData, ro1.Type) + assert.Equal(t, xdr.ScAddressTypeScAddressTypeContract, ro1.ContractData.Contract.Type) + assert.Equal(t, xdr.Hash(contractID), *ro1.ContractData.Contract.ContractId) + assert.Equal(t, xdr.ScValTypeScvLedgerKeyContractInstance, ro1.ContractData.Key.Type) + ro2 := obtainedFootprint.ReadOnly[2] + assert.Equal(t, xdr.LedgerEntryTypeContractCode, ro2.Type) + assert.Equal(t, contractHash, ro2.ContractCode.Hash) + assert.NoError(t, err) + + assert.NotZero(t, obtainedTransactionData.ResourceFee) + assert.NotZero(t, obtainedTransactionData.Resources.Instructions) + assert.NotZero(t, obtainedTransactionData.Resources.ReadBytes) + assert.NotZero(t, obtainedTransactionData.Resources.WriteBytes) + + // check the auth + assert.Len(t, response.Results[0].Auth, 1) + var obtainedAuth xdr.SorobanAuthorizationEntry + err = xdr.SafeUnmarshalBase64(response.Results[0].Auth[0], &obtainedAuth) + assert.NoError(t, err) + assert.Equal(t, obtainedAuth.Credentials.Type, xdr.SorobanCredentialsTypeSorobanCredentialsAddress) + assert.Equal(t, obtainedAuth.Credentials.Address.Signature.Type, xdr.ScValTypeScvVoid) + + assert.NotZero(t, obtainedAuth.Credentials.Address.Nonce) + assert.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) + assert.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) + + assert.Equal(t, xdr.SorobanCredentialsTypeSorobanCredentialsAddress, obtainedAuth.Credentials.Type) + assert.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) + assert.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) + assert.Equal(t, xdr.SorobanAuthorizedFunctionTypeSorobanAuthorizedFunctionTypeContractFn, obtainedAuth.RootInvocation.Function.Type) + assert.Equal(t, xdr.ScSymbol("auth"), obtainedAuth.RootInvocation.Function.ContractFn.FunctionName) + assert.Len(t, obtainedAuth.RootInvocation.Function.ContractFn.Args, 2) + world := obtainedAuth.RootInvocation.Function.ContractFn.Args[1] + assert.Equal(t, xdr.ScValTypeScvSymbol, world.Type) + assert.Equal(t, xdr.ScSymbol("world"), *world.Sym) + assert.Nil(t, obtainedAuth.RootInvocation.SubInvocations) + + // check the events. There will be 2 debug events and the event emitted by the "auth" function + // which is the one we are going to check. + assert.Len(t, response.Events, 3) + var event xdr.DiagnosticEvent + err = xdr.SafeUnmarshalBase64(response.Events[1], &event) + assert.NoError(t, err) + assert.True(t, event.InSuccessfulContractCall) + assert.NotNil(t, event.Event.ContractId) + assert.Equal(t, xdr.Hash(contractID), *event.Event.ContractId) + assert.Equal(t, xdr.ContractEventTypeContract, event.Event.Type) + assert.Equal(t, int32(0), event.Event.Body.V) + assert.Equal(t, xdr.ScValTypeScvSymbol, event.Event.Body.V0.Data.Type) + assert.Equal(t, xdr.ScSymbol("world"), *event.Event.Body.V0.Data.Sym) + assert.Len(t, event.Event.Body.V0.Topics, 1) + assert.Equal(t, xdr.ScValTypeScvString, event.Event.Body.V0.Topics[0].Type) + assert.Equal(t, xdr.ScString("auth"), *event.Event.Body.V0.Topics[0].Str) +} + +func TestSimulateTransactionError(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + client := test.GetRPCLient() + + invokeHostOp := infrastructure.CreateInvokeHostOperation( + test.MasterAccount().GetAccountID(), + xdr.Hash{}, + "noMethod", + ) + invokeHostOp.HostFunction = xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &xdr.Hash{0x1, 0x2}, + }, + FunctionName: "", + Args: nil, + }, + } + params := infrastructure.CreateTransactionParams( + test.MasterAccount(), + invokeHostOp, + ) + result := infrastructure.SimulateTransactionFromTxParams(t, client, params) + assert.Greater(t, result.LatestLedger, uint32(0)) + assert.Contains(t, result.Error, "MissingValue") + require.GreaterOrEqual(t, len(result.Events), 1) + var event xdr.DiagnosticEvent + require.NoError(t, xdr.SafeUnmarshalBase64(result.Events[0], &event)) +} + +func TestSimulateTransactionMultipleOperations(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + account := test.MasterAccount() + sourceAccount := account.GetAccountID() + params := txnbuild.TransactionParams{ + SourceAccount: account, + IncrementSequenceNum: false, + Operations: []txnbuild.Operation{ + infrastructure.CreateUploadHelloWorldOperation(sourceAccount), + infrastructure.CreateCreateHelloWorldContractOperation(sourceAccount), + }, + BaseFee: txnbuild.MinBaseFee, + Memo: nil, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + } + + client := test.GetRPCLient() + result := infrastructure.SimulateTransactionFromTxParams(t, client, params) + assert.Equal( + t, + methods.SimulateTransactionResponse{ + Error: "Transaction contains more than one operation", + }, + result, + ) +} + +func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + params := infrastructure.CreateTransactionParams( + test.MasterAccount(), + &txnbuild.BumpSequence{BumpTo: 1}, + ) + + client := test.GetRPCLient() + result := infrastructure.SimulateTransactionFromTxParams(t, client, params) + assert.Equal( + t, + methods.SimulateTransactionResponse{ + Error: "Transaction contains unsupported operation type: OperationTypeBumpSequence", + }, + result, + ) +} + +func TestSimulateTransactionUnmarshalError(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + client := test.GetRPCLient() + + request := methods.SimulateTransactionRequest{Transaction: "invalid"} + var result methods.SimulateTransactionResponse + err := client.CallResult(context.Background(), "simulateTransaction", request, &result) + assert.NoError(t, err) + assert.Equal( + t, + "Could not unmarshal transaction", + result.Error, + ) +} + +func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + _, contractID, _ := test.CreateHelloWorldContract() + test.InvokeHostFunc( + contractID, + "inc", + ) + + // get the counter ledger entry TTL + key := getCounterLedgerKey(contractID) + + keyB64, err := xdr.MarshalBase64(key) + require.NoError(t, err) + getLedgerEntryrequest := methods.GetLedgerEntryRequest{ + Key: keyB64, + } + var getLedgerEntryResult methods.GetLedgerEntryResponse + client := test.GetRPCLient() + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) + assert.NoError(t, err) + + var entry xdr.LedgerEntryData + assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) + assert.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) + require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) + + initialLiveUntil := *getLedgerEntryResult.LiveUntilLedgerSeq + + // Extend the initial TTL + test.PreflightAndSendMasterOperation(&txnbuild.ExtendFootprintTtl{ + ExtendTo: 20, + Ext: xdr.TransactionExt{ + V: 1, + SorobanData: &xdr.SorobanTransactionData{ + Resources: xdr.SorobanResources{ + Footprint: xdr.LedgerFootprint{ + ReadOnly: []xdr.LedgerKey{key}, + }, + }, + }, + }, + }, + ) + + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) + assert.NoError(t, err) + assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) + assert.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) + require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) + newLiveUntilSeq := *getLedgerEntryResult.LiveUntilLedgerSeq + assert.Greater(t, newLiveUntilSeq, initialLiveUntil) + + // Wait until it is not live anymore + waitUntilLedgerEntryTTL(t, client, key) + + // and restore it + test.PreflightAndSendMasterOperation( + &txnbuild.RestoreFootprint{ + Ext: xdr.TransactionExt{ + V: 1, + SorobanData: &xdr.SorobanTransactionData{ + Resources: xdr.SorobanResources{ + Footprint: xdr.LedgerFootprint{ + ReadWrite: []xdr.LedgerKey{key}, + }, + }, + }, + }, + }, + ) + + // Wait for TTL again and check the pre-restore field when trying to exec the contract again + waitUntilLedgerEntryTTL(t, client, key) + + invokeIncPresistentEntryParams := infrastructure.CreateTransactionParams( + test.MasterAccount(), + infrastructure.CreateInvokeHostOperation(test.MasterAccount().GetAccountID(), contractID, "inc"), + ) + simulationResult := infrastructure.SimulateTransactionFromTxParams(t, client, invokeIncPresistentEntryParams) + require.NotNil(t, simulationResult.RestorePreamble) + assert.NotZero(t, simulationResult.RestorePreamble) + + params := infrastructure.PreflightTransactionParamsLocally( + t, + infrastructure.CreateTransactionParams( + test.MasterAccount(), + &txnbuild.RestoreFootprint{}, + ), + methods.SimulateTransactionResponse{ + TransactionData: simulationResult.RestorePreamble.TransactionData, + MinResourceFee: simulationResult.RestorePreamble.MinResourceFee, + }, + ) + tx, err := txnbuild.NewTransaction(params) + assert.NoError(t, err) + test.SendMasterTransaction(tx) + + // Finally, we should be able to send the inc host function invocation now that we + // have pre-restored the entries + params = infrastructure.PreflightTransactionParamsLocally(t, invokeIncPresistentEntryParams, simulationResult) + tx, err = txnbuild.NewTransaction(params) + assert.NoError(t, err) + test.SendMasterTransaction(tx) +} + +func getCounterLedgerKey(contractID [32]byte) xdr.LedgerKey { + contractIDHash := xdr.Hash(contractID) + counterSym := xdr.ScSymbol("COUNTER") + key := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contractIDHash, + }, + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &counterSym, + }, + Durability: xdr.ContractDataDurabilityPersistent, + }, + } + return key +} + +func waitUntilLedgerEntryTTL(t *testing.T, client *jrpc2.Client, ledgerKey xdr.LedgerKey) { + keyB64, err := xdr.MarshalBase64(ledgerKey) + require.NoError(t, err) + request := methods.GetLedgerEntriesRequest{ + Keys: []string{keyB64}, + } + ttled := false + for i := 0; i < 50; i++ { + var result methods.GetLedgerEntriesResponse + var entry xdr.LedgerEntryData + err := client.CallResult(context.Background(), "getLedgerEntries", request, &result) + require.NoError(t, err) + require.NotEmpty(t, result.Entries) + require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &entry)) + require.NotEqual(t, xdr.LedgerEntryTypeTtl, entry.Type) + liveUntilLedgerSeq := xdr.Uint32(*result.Entries[0].LiveUntilLedgerSeq) + // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger + currentLedger := result.LatestLedger + 1 + if xdr.Uint32(currentLedger) > liveUntilLedgerSeq { + ttled = true + t.Logf("ledger entry ttl'ed") + break + } + t.Log("waiting for ledger entry to ttl at ledger", liveUntilLedgerSeq) + time.Sleep(time.Second) + } + require.True(t, ttled) +} + +func TestSimulateInvokePrng_u64_in_range(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + _, contractID, _ := test.CreateHelloWorldContract() + + authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + test.SendMasterOperation( + &txnbuild.CreateAccount{ + Destination: authAddrArg, + Amount: "100000", + SourceAccount: test.MasterAccount().GetAccountID(), + }, + ) + low := xdr.Uint64(1500) + high := xdr.Uint64(10000) + params := infrastructure.CreateTransactionParams( + test.MasterAccount(), + infrastructure.CreateInvokeHostOperation( + test.MasterAccount().GetAccountID(), + contractID, + "prng_u64_in_range", + xdr.ScVal{ + Type: xdr.ScValTypeScvU64, + U64: &low, + }, + xdr.ScVal{ + Type: xdr.ScValTypeScvU64, + U64: &high, + }, + ), + ) + + tx, err := txnbuild.NewTransaction(params) + require.NoError(t, err) + txB64, err := tx.Base64() + require.NoError(t, err) + + request := methods.SimulateTransactionRequest{Transaction: txB64} + var response methods.SimulateTransactionResponse + err = test.GetRPCLient().CallResult(context.Background(), "simulateTransaction", request, &response) + require.NoError(t, err) + require.Empty(t, response.Error) + + // check the result + require.Len(t, response.Results, 1) + var obtainedResult xdr.ScVal + err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) + require.NoError(t, err) + require.Equal(t, xdr.ScValTypeScvU64, obtainedResult.Type) + require.LessOrEqual(t, uint64(*obtainedResult.U64), uint64(high)) + require.GreaterOrEqual(t, uint64(*obtainedResult.U64), uint64(low)) +} + +func TestSimulateSystemEvent(t *testing.T) { + test := infrastructure.NewTest(t, nil) + + _, contractID, contractHash := test.CreateHelloWorldContract() + authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + test.SendMasterOperation( + &txnbuild.CreateAccount{ + Destination: authAddrArg, + Amount: "100000", + SourceAccount: test.MasterAccount().GetAccountID(), + }, + ) + + byteSlice := xdr.ScBytes(contractHash[:]) + + params := infrastructure.CreateTransactionParams( + test.MasterAccount(), + infrastructure.CreateInvokeHostOperation( + test.MasterAccount().GetAccountID(), + contractID, + "upgrade_contract", + xdr.ScVal{ + Type: xdr.ScValTypeScvBytes, + Bytes: &byteSlice, + }, + ), + ) + tx, err := txnbuild.NewTransaction(params) + require.NoError(t, err) + txB64, err := tx.Base64() + require.NoError(t, err) + + request := methods.SimulateTransactionRequest{Transaction: txB64} + var response methods.SimulateTransactionResponse + err = test.GetRPCLient().CallResult(context.Background(), "simulateTransaction", request, &response) + require.NoError(t, err) + require.Empty(t, response.Error) + + // check the result + require.Len(t, response.Results, 1) + var obtainedResult xdr.ScVal + err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) + require.NoError(t, err) + + var transactionData xdr.SorobanTransactionData + err = xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) + require.NoError(t, err) + assert.InDelta(t, 6856, uint32(transactionData.Resources.ReadBytes), 200) + + // the resulting fee is derived from compute factors and a default padding is applied to instructions by preflight + // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture + // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo + // in the test. + assert.InDelta(t, 70668, int64(transactionData.ResourceFee), 20000) + assert.InDelta(t, 104, uint32(transactionData.Resources.WriteBytes), 15) + require.GreaterOrEqual(t, len(response.Events), 3) +} diff --git a/cmd/soroban-rpc/internal/test/transaction_test.go b/cmd/soroban-rpc/internal/integrationtest/transaction_test.go similarity index 50% rename from cmd/soroban-rpc/internal/test/transaction_test.go rename to cmd/soroban-rpc/internal/integrationtest/transaction_test.go index 4a8461f6..f0d6426c 100644 --- a/cmd/soroban-rpc/internal/test/transaction_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/transaction_test.go @@ -1,72 +1,32 @@ -package test +package integrationtest import ( "context" - "crypto/sha256" "fmt" "testing" - "time" "github.com/creachadair/jrpc2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) func TestSendTransactionSucceedsWithoutResults(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.SetOptions{HomeDomain: txnbuild.NewHomeDomain("soroban.com")}, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, kp, tx) + test := infrastructure.NewTest(t, nil) + test.SendMasterOperation( + &txnbuild.SetOptions{HomeDomain: txnbuild.NewHomeDomain("soroban.com")}, + ) } func TestSendTransactionSucceedsWithResults(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() + test := infrastructure.NewTest(t, nil) - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - response := sendSuccessfulTransaction(t, client, kp, tx) + response, contractHash := test.UploadHelloWorldContract() // Check the result is what we expect var transactionResult xdr.TransactionResult @@ -76,7 +36,6 @@ func TestSendTransactionSucceedsWithResults(t *testing.T) { invokeHostFunctionResult, ok := opResults[0].MustTr().GetInvokeHostFunctionResult() assert.True(t, ok) assert.Equal(t, invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) - contractHash := sha256.Sum256(contractBinary) contractHashBytes := xdr.ScBytes(contractHash[:]) expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvBytes, Bytes: &contractHashBytes} var transactionMeta xdr.TransactionMeta @@ -107,38 +66,29 @@ func TestSendTransactionSucceedsWithResults(t *testing.T) { } func TestSendTransactionBadSequence(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) + test := infrastructure.NewTest(t, nil) - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - Operations: []txnbuild.Operation{ - &txnbuild.SetOptions{HomeDomain: txnbuild.NewHomeDomain("soroban.com")}, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) + params := infrastructure.CreateTransactionParams( + test.MasterAccount(), + &txnbuild.SetOptions{HomeDomain: txnbuild.NewHomeDomain("soroban.com")}, + ) + params.IncrementSequenceNum = false + tx, err := txnbuild.NewTransaction(params) assert.NoError(t, err) - tx, err = tx.Sign(StandaloneNetworkPassphrase, kp) + tx, err = tx.Sign(infrastructure.StandaloneNetworkPassphrase, test.MasterKey()) assert.NoError(t, err) b64, err := tx.Base64() assert.NoError(t, err) request := methods.SendTransactionRequest{Transaction: b64} var result methods.SendTransactionResponse + client := test.GetRPCLient() err = client.CallResult(context.Background(), "sendTransaction", request, &result) assert.NoError(t, err) assert.NotZero(t, result.LatestLedger) assert.NotZero(t, result.LatestLedgerCloseTime) - expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) + expectedHashHex, err := tx.HashHex(infrastructure.StandaloneNetworkPassphrase) assert.NoError(t, err) assert.Equal(t, expectedHashHex, result.Hash) assert.Equal(t, proto.TXStatusError, result.Status) @@ -148,26 +98,16 @@ func TestSendTransactionBadSequence(t *testing.T) { } func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) client := test.GetRPCLient() - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) + params := infrastructure.PreflightTransactionParams(t, client, + infrastructure.CreateTransactionParams( + test.MasterAccount(), + infrastructure.CreateUploadHelloWorldOperation(test.MasterAccount().GetAccountID()), + ), + ) // make the transaction fail due to insufficient resource fees params.Operations[0].(*txnbuild.InvokeHostFunction).Ext.SorobanData.ResourceFee /= 2 @@ -176,7 +116,7 @@ func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { assert.NoError(t, err) assert.NoError(t, err) - tx, err = tx.Sign(StandaloneNetworkPassphrase, kp) + tx, err = tx.Sign(infrastructure.StandaloneNetworkPassphrase, test.MasterKey()) assert.NoError(t, err) b64, err := tx.Base64() assert.NoError(t, err) @@ -199,18 +139,14 @@ func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { } func TestSendTransactionFailedInLedger(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) client := test.GetRPCLient() - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ + kp := keypair.Root(infrastructure.StandaloneNetworkPassphrase) + tx, err := txnbuild.NewTransaction( + infrastructure.CreateTransactionParams( + test.MasterAccount(), &txnbuild.Payment{ // Destination doesn't exist, making the transaction fail Destination: "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ", @@ -218,14 +154,10 @@ func TestSendTransactionFailedInLedger(t *testing.T) { Asset: txnbuild.NativeAsset{}, SourceAccount: "", }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) + ), + ) assert.NoError(t, err) - tx, err = tx.Sign(StandaloneNetworkPassphrase, kp) + tx, err = tx.Sign(infrastructure.StandaloneNetworkPassphrase, kp) assert.NoError(t, err) b64, err := tx.Base64() assert.NoError(t, err) @@ -235,7 +167,7 @@ func TestSendTransactionFailedInLedger(t *testing.T) { err = client.CallResult(context.Background(), "sendTransaction", request, &result) assert.NoError(t, err) - expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) + expectedHashHex, err := tx.HashHex(infrastructure.StandaloneNetworkPassphrase) assert.NoError(t, err) assert.Equal(t, expectedHashHex, result.Hash) @@ -248,7 +180,7 @@ func TestSendTransactionFailedInLedger(t *testing.T) { assert.NotZero(t, result.LatestLedger) assert.NotZero(t, result.LatestLedgerCloseTime) - response := getTransaction(t, client, expectedHashHex) + response := test.GetTransaction(expectedHashHex) assert.Equal(t, methods.TransactionStatusFailed, response.Status) var transactionResult xdr.TransactionResult assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &transactionResult)) @@ -260,7 +192,7 @@ func TestSendTransactionFailedInLedger(t *testing.T) { } func TestSendTransactionFailedInvalidXDR(t *testing.T) { - test := NewTest(t, nil) + test := infrastructure.NewTest(t, nil) client := test.GetRPCLient() @@ -270,78 +202,3 @@ func TestSendTransactionFailedInvalidXDR(t *testing.T) { assert.Equal(t, "invalid_xdr", jsonRPCErr.Message) assert.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) } - -func sendSuccessfulTransaction(t *testing.T, client *jrpc2.Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { - tx, err := transaction.Sign(StandaloneNetworkPassphrase, kp) - assert.NoError(t, err) - b64, err := tx.Base64() - assert.NoError(t, err) - - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse - assert.NoError(t, client.CallResult(context.Background(), "sendTransaction", request, &result)) - - expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) - assert.NoError(t, err) - - assert.Equal(t, expectedHashHex, result.Hash) - if !assert.Equal(t, proto.TXStatusPending, result.Status) { - var txResult xdr.TransactionResult - err := xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &txResult) - assert.NoError(t, err) - t.Logf("error: %#v\n", txResult) - } - assert.NotZero(t, result.LatestLedger) - assert.NotZero(t, result.LatestLedgerCloseTime) - - response := getTransaction(t, client, expectedHashHex) - if !assert.Equal(t, methods.TransactionStatusSuccess, response.Status) { - var txResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &txResult)) - t.Logf("error: %#v\n", txResult) - - var txMeta xdr.TransactionMeta - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXdr, &txMeta)) - - if txMeta.V == 3 && txMeta.V3.SorobanMeta != nil { - if len(txMeta.V3.SorobanMeta.Events) > 0 { - t.Log("Contract events:") - for i, e := range txMeta.V3.SorobanMeta.Events { - t.Logf(" %d: %s\n", i, e) - } - } - - if len(txMeta.V3.SorobanMeta.DiagnosticEvents) > 0 { - t.Log("Diagnostic events:") - for i, d := range txMeta.V3.SorobanMeta.DiagnosticEvents { - t.Logf(" %d: %s\n", i, d) - } - } - } - } - - require.NotNil(t, response.ResultXdr) - assert.Greater(t, response.Ledger, result.LatestLedger) - assert.Greater(t, response.LedgerCloseTime, result.LatestLedgerCloseTime) - assert.GreaterOrEqual(t, response.LatestLedger, response.Ledger) - assert.GreaterOrEqual(t, response.LatestLedgerCloseTime, response.LedgerCloseTime) - return response -} - -func getTransaction(t *testing.T, client *jrpc2.Client, hash string) methods.GetTransactionResponse { - var result methods.GetTransactionResponse - for i := 0; i < 60; i++ { - request := methods.GetTransactionRequest{Hash: hash} - err := client.CallResult(context.Background(), "getTransaction", request, &result) - assert.NoError(t, err) - - if result.Status == methods.TransactionStatusNotFound { - time.Sleep(time.Second) - continue - } - - return result - } - t.Fatal("getTransaction timed out") - return result -} diff --git a/cmd/soroban-rpc/internal/integrationtest/upgrade_test.go b/cmd/soroban-rpc/internal/integrationtest/upgrade_test.go new file mode 100644 index 00000000..386e95ff --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/upgrade_test.go @@ -0,0 +1,53 @@ +package integrationtest + +import ( + "context" + "testing" + "time" + + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/require" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" +) + +func TestUpgradeFrom20To21(t *testing.T) { + if infrastructure.GetCoreMaxSupportedProtocol() != 21 { + t.Skip("Only test this for protocol 21") + } + test := infrastructure.NewTest(t, &infrastructure.TestConfig{ + ProtocolVersion: 20, + }) + + test.UploadHelloWorldContract() + + // Upgrade to protocol 21 and re-upload the contract, which should cause a caching of the contract + // estimations + test.UpgradeProtocol(21) + // Wait for the ledger to advance, so that the simulation library passes the right protocol number + rpcDB := test.GetDaemon().GetDB() + initialLedgerSequence, err := db.NewLedgerEntryReader(rpcDB).GetLatestLedgerSequence(context.Background()) + require.NoError(t, err) + require.Eventually(t, + func() bool { + newLedgerSequence, err := db.NewLedgerEntryReader(rpcDB).GetLatestLedgerSequence(context.Background()) + require.NoError(t, err) + return newLedgerSequence > initialLedgerSequence + }, + time.Minute, + time.Second, + ) + + _, contractID, _ := test.CreateHelloWorldContract() + + contractFnParameterSym := xdr.ScSymbol("world") + test.InvokeHostFunc( + contractID, + "hello", + xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &contractFnParameterSym, + }, + ) +} diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go deleted file mode 100644 index 642c7937..00000000 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ /dev/null @@ -1,1137 +0,0 @@ -package test - -import ( - "context" - "crypto/sha256" - "fmt" - "os" - "path" - "runtime" - "testing" - "time" - - "github.com/creachadair/jrpc2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" -) - -var ( - testSalt = sha256.Sum256([]byte("a1")) -) - -func getHelloWorldContract(t *testing.T) []byte { - _, filename, _, _ := runtime.Caller(0) - testDirName := path.Dir(filename) - contractFile := path.Join(testDirName, helloWorldContractPath) - ret, err := os.ReadFile(contractFile) - if err != nil { - t.Fatalf("unable to read test_hello_world.wasm (%v) please run `make build-test-wasms` at the project root directory", err) - } - return ret -} - -func createInvokeHostOperation(sourceAccount string, contractID xdr.Hash, method string, args ...xdr.ScVal) *txnbuild.InvokeHostFunction { - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractID, - }, - FunctionName: xdr.ScSymbol(method), - Args: args, - }, - }, - Auth: nil, - SourceAccount: sourceAccount, - } -} - -func createInstallContractCodeOperation(sourceAccount string, contractCode []byte) *txnbuild.InvokeHostFunction { - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm, - Wasm: &contractCode, - }, - SourceAccount: sourceAccount, - } -} - -func createCreateContractOperation(sourceAccount string, contractCode []byte) *txnbuild.InvokeHostFunction { - saltParam := xdr.Uint256(testSalt) - contractHash := xdr.Hash(sha256.Sum256(contractCode)) - - sourceAccountID := xdr.MustAddress(sourceAccount) - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, - CreateContract: &xdr.CreateContractArgs{ - ContractIdPreimage: xdr.ContractIdPreimage{ - Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAddress, - FromAddress: &xdr.ContractIdPreimageFromAddress{ - Address: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeAccount, - AccountId: &sourceAccountID, - }, - Salt: saltParam, - }, - }, - Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableWasm, - WasmHash: &contractHash, - }, - }, - }, - Auth: []xdr.SorobanAuthorizationEntry{}, - SourceAccount: sourceAccount, - } -} - -func getContractID(t *testing.T, sourceAccount string, salt [32]byte, networkPassphrase string) [32]byte { - sourceAccountID := xdr.MustAddress(sourceAccount) - preImage := xdr.HashIdPreimage{ - Type: xdr.EnvelopeTypeEnvelopeTypeContractId, - ContractId: &xdr.HashIdPreimageContractId{ - NetworkId: sha256.Sum256([]byte(networkPassphrase)), - ContractIdPreimage: xdr.ContractIdPreimage{ - Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAddress, - FromAddress: &xdr.ContractIdPreimageFromAddress{ - Address: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeAccount, - AccountId: &sourceAccountID, - }, - Salt: salt, - }, - }, - }, - } - - xdrPreImageBytes, err := preImage.MarshalBinary() - require.NoError(t, err) - hashedContractID := sha256.Sum256(xdrPreImageBytes) - return hashedContractID -} - -func simulateTransactionFromTxParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) methods.SimulateTransactionResponse { - savedAutoIncrement := params.IncrementSequenceNum - params.IncrementSequenceNum = false - tx, err := txnbuild.NewTransaction(params) - require.NoError(t, err) - params.IncrementSequenceNum = savedAutoIncrement - txB64, err := tx.Base64() - require.NoError(t, err) - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - require.NoError(t, err) - return response -} - -func preflightTransactionParamsLocally(t *testing.T, params txnbuild.TransactionParams, response methods.SimulateTransactionResponse) txnbuild.TransactionParams { - if !assert.Empty(t, response.Error) { - fmt.Println(response.Error) - } - var transactionData xdr.SorobanTransactionData - err := xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) - require.NoError(t, err) - - op := params.Operations[0] - switch v := op.(type) { - case *txnbuild.InvokeHostFunction: - require.Len(t, response.Results, 1) - v.Ext = xdr.TransactionExt{ - V: 1, - SorobanData: &transactionData, - } - var auth []xdr.SorobanAuthorizationEntry - for _, b64 := range response.Results[0].Auth { - var a xdr.SorobanAuthorizationEntry - err := xdr.SafeUnmarshalBase64(b64, &a) - assert.NoError(t, err) - auth = append(auth, a) - } - v.Auth = auth - case *txnbuild.ExtendFootprintTtl: - require.Len(t, response.Results, 0) - v.Ext = xdr.TransactionExt{ - V: 1, - SorobanData: &transactionData, - } - case *txnbuild.RestoreFootprint: - require.Len(t, response.Results, 0) - v.Ext = xdr.TransactionExt{ - V: 1, - SorobanData: &transactionData, - } - default: - t.Fatalf("Wrong operation type %v", op) - } - - params.Operations = []txnbuild.Operation{op} - - params.BaseFee += response.MinResourceFee - return params -} - -func preflightTransactionParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { - response := simulateTransactionFromTxParams(t, client, params) - // The preamble should be zero except for the special restore case - assert.Nil(t, response.RestorePreamble) - return preflightTransactionParamsLocally(t, params, response) -} - -func TestSimulateTransactionSucceeds(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractBinary := getHelloWorldContract(t) - params := txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: sourceAccount, - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(sourceAccount, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - result := simulateTransactionFromTxParams(t, client, params) - - contractHash := sha256.Sum256(contractBinary) - contractHashBytes := xdr.ScBytes(contractHash[:]) - expectedXdr := xdr.ScVal{Type: xdr.ScValTypeScvBytes, Bytes: &contractHashBytes} - assert.Greater(t, result.LatestLedger, uint32(0)) - assert.Greater(t, result.Cost.CPUInstructions, uint64(0)) - assert.Greater(t, result.Cost.MemoryBytes, uint64(0)) - - expectedTransactionData := xdr.SorobanTransactionData{ - Resources: xdr.SorobanResources{ - Footprint: xdr.LedgerFootprint{ - ReadWrite: []xdr.LedgerKey{ - { - Type: xdr.LedgerEntryTypeContractCode, - ContractCode: &xdr.LedgerKeyContractCode{ - Hash: xdr.Hash(contractHash), - }, - }, - }, - }, - Instructions: 4378462, - ReadBytes: 0, - WriteBytes: 7048, - }, - // the resulting fee is derived from the compute factors and a default padding is applied to instructions by preflight - // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture - // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo - // in the test. - ResourceFee: 149755, - } - - // First, decode and compare the transaction data so we get a decent diff if it fails. - var transactionData xdr.SorobanTransactionData - err := xdr.SafeUnmarshalBase64(result.TransactionData, &transactionData) - assert.NoError(t, err) - assert.Equal(t, expectedTransactionData.Resources.Footprint, transactionData.Resources.Footprint) - assert.InDelta(t, uint32(expectedTransactionData.Resources.Instructions), uint32(transactionData.Resources.Instructions), 3200000) - assert.InDelta(t, uint32(expectedTransactionData.Resources.ReadBytes), uint32(transactionData.Resources.ReadBytes), 10) - assert.InDelta(t, uint32(expectedTransactionData.Resources.WriteBytes), uint32(transactionData.Resources.WriteBytes), 300) - assert.InDelta(t, int64(expectedTransactionData.ResourceFee), int64(transactionData.ResourceFee), 40000) - - // Then decode and check the result xdr, separately so we get a decent diff if it fails. - assert.Len(t, result.Results, 1) - var resultXdr xdr.ScVal - err = xdr.SafeUnmarshalBase64(result.Results[0].XDR, &resultXdr) - assert.NoError(t, err) - assert.Equal(t, expectedXdr, resultXdr) - - // Check state diff - assert.Len(t, result.StateChanges, 1) - assert.Nil(t, result.StateChanges[0].Before) - assert.NotNil(t, result.StateChanges[0].After) - assert.Equal(t, methods.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type) - var after xdr.LedgerEntry - assert.NoError(t, xdr.SafeUnmarshalBase64(*result.StateChanges[0].After, &after)) - assert.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type) - entryKey, err := after.LedgerKey() - assert.NoError(t, err) - entryKeyB64, err := xdr.MarshalBase64(entryKey) - assert.NoError(t, err) - assert.Equal(t, entryKeyB64, result.StateChanges[0].Key) - - // test operation which does not have a source account - withoutSourceAccountOp := createInstallContractCodeOperation("", contractBinary) - params = txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: sourceAccount, - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{withoutSourceAccountOp}, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - require.NoError(t, err) - - resultForRequestWithoutOpSource := simulateTransactionFromTxParams(t, client, params) - // Let's not compare the latest ledger since it may change - result.LatestLedger = resultForRequestWithoutOpSource.LatestLedger - assert.Equal(t, result, resultForRequestWithoutOpSource) - - // test that operation source account takes precedence over tx source account - params = txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.Root("test passphrase").Address(), - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(sourceAccount, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - - resultForRequestWithDifferentTxSource := simulateTransactionFromTxParams(t, client, params) - assert.GreaterOrEqual(t, resultForRequestWithDifferentTxSource.LatestLedger, result.LatestLedger) - // apart from latest ledger the response should be the same - resultForRequestWithDifferentTxSource.LatestLedger = result.LatestLedger - assert.Equal(t, result, resultForRequestWithDifferentTxSource) -} - -func TestSimulateTransactionWithAuth(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - deployContractOp := createCreateContractOperation(address, helloWorldContract) - deployContractParams := txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - deployContractOp, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - response := simulateTransactionFromTxParams(t, client, deployContractParams) - require.NotEmpty(t, response.Results) - require.Len(t, response.Results[0].Auth, 1) - require.Empty(t, deployContractOp.Auth) - - var auth xdr.SorobanAuthorizationEntry - assert.NoError(t, xdr.SafeUnmarshalBase64(response.Results[0].Auth[0], &auth)) - require.Equal(t, auth.Credentials.Type, xdr.SorobanCredentialsTypeSorobanCredentialsSourceAccount) - deployContractOp.Auth = append(deployContractOp.Auth, auth) - deployContractParams.Operations = []txnbuild.Operation{deployContractOp} - - // preflight deployContractOp with auth - deployContractParams = preflightTransactionParams(t, client, deployContractParams) - tx, err = txnbuild.NewTransaction(deployContractParams) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) -} - -func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - contractFnParameterSym := xdr.ScSymbol("world") - authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" - authAccountIDArg := xdr.MustAddress(authAddrArg) - tx, err = txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.CreateAccount{ - Destination: authAddrArg, - Amount: "100000", - SourceAccount: address, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - params = txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "auth", - xdr.ScVal{ - Type: xdr.ScValTypeScvAddress, - Address: &xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeAccount, - AccountId: &authAccountIDArg, - }, - }, - xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &contractFnParameterSym, - }, - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - tx, err = txnbuild.NewTransaction(params) - - assert.NoError(t, err) - - txB64, err := tx.Base64() - assert.NoError(t, err) - - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - assert.NoError(t, err) - assert.Empty(t, response.Error) - - // check the result - assert.Len(t, response.Results, 1) - var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) - assert.NoError(t, err) - assert.Equal(t, xdr.ScValTypeScvAddress, obtainedResult.Type) - require.NotNil(t, obtainedResult.Address) - assert.Equal(t, authAccountIDArg, obtainedResult.Address.MustAccountId()) - - // check the footprint - var obtainedTransactionData xdr.SorobanTransactionData - err = xdr.SafeUnmarshalBase64(response.TransactionData, &obtainedTransactionData) - obtainedFootprint := obtainedTransactionData.Resources.Footprint - assert.NoError(t, err) - assert.Len(t, obtainedFootprint.ReadWrite, 1) - assert.Len(t, obtainedFootprint.ReadOnly, 3) - ro0 := obtainedFootprint.ReadOnly[0] - assert.Equal(t, xdr.LedgerEntryTypeAccount, ro0.Type) - assert.Equal(t, authAddrArg, ro0.Account.AccountId.Address()) - ro1 := obtainedFootprint.ReadOnly[1] - assert.Equal(t, xdr.LedgerEntryTypeContractData, ro1.Type) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeContract, ro1.ContractData.Contract.Type) - assert.Equal(t, xdr.Hash(contractID), *ro1.ContractData.Contract.ContractId) - assert.Equal(t, xdr.ScValTypeScvLedgerKeyContractInstance, ro1.ContractData.Key.Type) - ro2 := obtainedFootprint.ReadOnly[2] - assert.Equal(t, xdr.LedgerEntryTypeContractCode, ro2.Type) - contractHash := sha256.Sum256(helloWorldContract) - assert.Equal(t, xdr.Hash(contractHash), ro2.ContractCode.Hash) - assert.NoError(t, err) - - assert.NotZero(t, obtainedTransactionData.ResourceFee) - assert.NotZero(t, obtainedTransactionData.Resources.Instructions) - assert.NotZero(t, obtainedTransactionData.Resources.ReadBytes) - assert.NotZero(t, obtainedTransactionData.Resources.WriteBytes) - - // check the auth - assert.Len(t, response.Results[0].Auth, 1) - var obtainedAuth xdr.SorobanAuthorizationEntry - err = xdr.SafeUnmarshalBase64(response.Results[0].Auth[0], &obtainedAuth) - assert.NoError(t, err) - assert.Equal(t, obtainedAuth.Credentials.Type, xdr.SorobanCredentialsTypeSorobanCredentialsAddress) - assert.Equal(t, obtainedAuth.Credentials.Address.Signature.Type, xdr.ScValTypeScvVoid) - - assert.NotZero(t, obtainedAuth.Credentials.Address.Nonce) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) - assert.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) - - assert.Equal(t, xdr.SorobanCredentialsTypeSorobanCredentialsAddress, obtainedAuth.Credentials.Type) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) - assert.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) - assert.Equal(t, xdr.SorobanAuthorizedFunctionTypeSorobanAuthorizedFunctionTypeContractFn, obtainedAuth.RootInvocation.Function.Type) - assert.Equal(t, xdr.ScSymbol("auth"), obtainedAuth.RootInvocation.Function.ContractFn.FunctionName) - assert.Len(t, obtainedAuth.RootInvocation.Function.ContractFn.Args, 2) - world := obtainedAuth.RootInvocation.Function.ContractFn.Args[1] - assert.Equal(t, xdr.ScValTypeScvSymbol, world.Type) - assert.Equal(t, xdr.ScSymbol("world"), *world.Sym) - assert.Nil(t, obtainedAuth.RootInvocation.SubInvocations) - - // check the events. There will be 2 debug events and the event emitted by the "auth" function - // which is the one we are going to check. - assert.Len(t, response.Events, 3) - var event xdr.DiagnosticEvent - err = xdr.SafeUnmarshalBase64(response.Events[1], &event) - assert.NoError(t, err) - assert.True(t, event.InSuccessfulContractCall) - assert.NotNil(t, event.Event.ContractId) - assert.Equal(t, xdr.Hash(contractID), *event.Event.ContractId) - assert.Equal(t, xdr.ContractEventTypeContract, event.Event.Type) - assert.Equal(t, int32(0), event.Event.Body.V) - assert.Equal(t, xdr.ScValTypeScvSymbol, event.Event.Body.V0.Data.Type) - assert.Equal(t, xdr.ScSymbol("world"), *event.Event.Body.V0.Data.Sym) - assert.Len(t, event.Event.Body.V0.Topics, 1) - assert.Equal(t, xdr.ScValTypeScvString, event.Event.Body.V0.Topics[0].Type) - assert.Equal(t, xdr.ScString("auth"), *event.Event.Body.V0.Topics[0].Str) - metrics := getMetrics(test) - require.Contains(t, metrics, "soroban_rpc_json_rpc_request_duration_seconds_count{endpoint=\"simulateTransaction\",status=\"ok\"} 3") - require.Contains(t, metrics, "soroban_rpc_preflight_pool_request_ledger_get_duration_seconds_count{status=\"ok\",type=\"db\"} 3") - require.Contains(t, metrics, "soroban_rpc_preflight_pool_request_ledger_get_duration_seconds_count{status=\"ok\",type=\"all\"} 3") -} - -func TestSimulateTransactionError(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - invokeHostOp := createInvokeHostOperation(sourceAccount, xdr.Hash{}, "noMethod") - invokeHostOp.HostFunction = xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0x1, 0x2}, - }, - FunctionName: "", - Args: nil, - }, - } - params := txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.Root(StandaloneNetworkPassphrase).Address(), - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{invokeHostOp}, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - result := simulateTransactionFromTxParams(t, client, params) - assert.Greater(t, result.LatestLedger, uint32(0)) - assert.Contains(t, result.Error, "MissingValue") - require.GreaterOrEqual(t, len(result.Events), 1) - var event xdr.DiagnosticEvent - require.NoError(t, xdr.SafeUnmarshalBase64(result.Events[0], &event)) -} - -func TestSimulateTransactionMultipleOperations(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractBinary := getHelloWorldContract(t) - params := txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.Root(StandaloneNetworkPassphrase).Address(), - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(sourceAccount, contractBinary), - createCreateContractOperation(sourceAccount, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - - result := simulateTransactionFromTxParams(t, client, params) - assert.Equal( - t, - methods.SimulateTransactionResponse{ - Error: "Transaction contains more than one operation", - }, - result, - ) -} - -func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - params := txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.Root(StandaloneNetworkPassphrase).Address(), - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - &txnbuild.BumpSequence{BumpTo: 1}, - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - result := simulateTransactionFromTxParams(t, client, params) - assert.Equal( - t, - methods.SimulateTransactionResponse{ - Error: "Transaction contains unsupported operation type: OperationTypeBumpSequence", - }, - result, - ) -} - -func TestSimulateTransactionUnmarshalError(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - request := methods.SimulateTransactionRequest{Transaction: "invalid"} - var result methods.SimulateTransactionResponse - err := client.CallResult(context.Background(), "simulateTransaction", request, &result) - assert.NoError(t, err) - assert.Equal( - t, - "Could not unmarshal transaction", - result.Error, - ) -} - -func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - invokeIncPresistentEntryParams := txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "inc", - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - params = preflightTransactionParams(t, client, invokeIncPresistentEntryParams) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - // get the counter ledger entry TTL - key := getCounterLedgerKey(contractID) - - keyB64, err := xdr.MarshalBase64(key) - require.NoError(t, err) - getLedgerEntryrequest := methods.GetLedgerEntryRequest{ - Key: keyB64, - } - var getLedgerEntryResult methods.GetLedgerEntryResponse - err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) - assert.NoError(t, err) - - var entry xdr.LedgerEntryData - assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) - assert.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) - require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) - - initialLiveUntil := *getLedgerEntryResult.LiveUntilLedgerSeq - - // Extend the initial TTL - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.ExtendFootprintTtl{ - ExtendTo: 20, - Ext: xdr.TransactionExt{ - V: 1, - SorobanData: &xdr.SorobanTransactionData{ - Resources: xdr.SorobanResources{ - Footprint: xdr.LedgerFootprint{ - ReadOnly: []xdr.LedgerKey{key}, - }, - }, - }, - }, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) - assert.NoError(t, err) - assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) - assert.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) - require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) - newLiveUntilSeq := *getLedgerEntryResult.LiveUntilLedgerSeq - assert.Greater(t, newLiveUntilSeq, initialLiveUntil) - - // Wait until it is not live anymore - waitUntilLedgerEntryTTL(t, client, key) - - // and restore it - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.RestoreFootprint{ - Ext: xdr.TransactionExt{ - V: 1, - SorobanData: &xdr.SorobanTransactionData{ - Resources: xdr.SorobanResources{ - Footprint: xdr.LedgerFootprint{ - ReadWrite: []xdr.LedgerKey{key}, - }, - }, - }, - }, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - // Wait for TTL again and check the pre-restore field when trying to exec the contract again - waitUntilLedgerEntryTTL(t, client, key) - - simulationResult := simulateTransactionFromTxParams(t, client, invokeIncPresistentEntryParams) - require.NotNil(t, simulationResult.RestorePreamble) - assert.NotZero(t, simulationResult.RestorePreamble) - - params = preflightTransactionParamsLocally(t, - txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.RestoreFootprint{}, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }, - methods.SimulateTransactionResponse{ - TransactionData: simulationResult.RestorePreamble.TransactionData, - MinResourceFee: simulationResult.RestorePreamble.MinResourceFee, - }, - ) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - // Finally, we should be able to send the inc host function invocation now that we - // have pre-restored the entries - params = preflightTransactionParamsLocally(t, invokeIncPresistentEntryParams, simulationResult) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) -} - -func getCounterLedgerKey(contractID [32]byte) xdr.LedgerKey { - contractIDHash := xdr.Hash(contractID) - counterSym := xdr.ScSymbol("COUNTER") - key := xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractIDHash, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counterSym, - }, - Durability: xdr.ContractDataDurabilityPersistent, - }, - } - return key -} - -func waitUntilLedgerEntryTTL(t *testing.T, client *jrpc2.Client, ledgerKey xdr.LedgerKey) { - keyB64, err := xdr.MarshalBase64(ledgerKey) - require.NoError(t, err) - request := methods.GetLedgerEntriesRequest{ - Keys: []string{keyB64}, - } - ttled := false - for i := 0; i < 50; i++ { - var result methods.GetLedgerEntriesResponse - var entry xdr.LedgerEntryData - err := client.CallResult(context.Background(), "getLedgerEntries", request, &result) - require.NoError(t, err) - require.NotEmpty(t, result.Entries) - require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &entry)) - require.NotEqual(t, xdr.LedgerEntryTypeTtl, entry.Type) - liveUntilLedgerSeq := xdr.Uint32(*result.Entries[0].LiveUntilLedgerSeq) - // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger - currentLedger := result.LatestLedger + 1 - if xdr.Uint32(currentLedger) > liveUntilLedgerSeq { - ttled = true - t.Logf("ledger entry ttl'ed") - break - } - t.Log("waiting for ledger entry to ttl at ledger", liveUntilLedgerSeq) - time.Sleep(time.Second) - } - require.True(t, ttled) -} - -func TestSimulateInvokePrng_u64_in_range(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err = txnbuild.NewTransaction(params) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" - tx, err = txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.CreateAccount{ - Destination: authAddrArg, - Amount: "100000", - SourceAccount: address, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - low := xdr.Uint64(1500) - high := xdr.Uint64(10000) - params = txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "prng_u64_in_range", - xdr.ScVal{ - Type: xdr.ScValTypeScvU64, - U64: &low, - }, - xdr.ScVal{ - Type: xdr.ScValTypeScvU64, - U64: &high, - }, - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - tx, err = txnbuild.NewTransaction(params) - - require.NoError(t, err) - - txB64, err := tx.Base64() - require.NoError(t, err) - - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - require.NoError(t, err) - require.Empty(t, response.Error) - - // check the result - require.Len(t, response.Results, 1) - var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) - require.NoError(t, err) - require.Equal(t, xdr.ScValTypeScvU64, obtainedResult.Type) - require.LessOrEqual(t, uint64(*obtainedResult.U64), uint64(high)) - require.GreaterOrEqual(t, uint64(*obtainedResult.U64), uint64(low)) -} - -func TestSimulateSystemEvent(t *testing.T) { - test := NewTest(t, nil) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err = txnbuild.NewTransaction(params) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" - tx, err = txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.CreateAccount{ - Destination: authAddrArg, - Amount: "100000", - SourceAccount: address, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractHash := sha256.Sum256(helloWorldContract) - byteSlice := xdr.ScBytes(contractHash[:]) - - params = txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "upgrade_contract", - xdr.ScVal{ - Type: xdr.ScValTypeScvBytes, - Bytes: &byteSlice, - }, - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - tx, err = txnbuild.NewTransaction(params) - - require.NoError(t, err) - - txB64, err := tx.Base64() - require.NoError(t, err) - - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - require.NoError(t, err) - require.Empty(t, response.Error) - - // check the result - require.Len(t, response.Results, 1) - var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) - require.NoError(t, err) - - var transactionData xdr.SorobanTransactionData - err = xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) - require.NoError(t, err) - assert.InDelta(t, 6856, uint32(transactionData.Resources.ReadBytes), 200) - - // the resulting fee is derived from compute factors and a default padding is applied to instructions by preflight - // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture - // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo - // in the test. - assert.InDelta(t, 70668, int64(transactionData.ResourceFee), 20000) - assert.InDelta(t, 104, uint32(transactionData.Resources.WriteBytes), 15) - require.GreaterOrEqual(t, len(response.Events), 3) -} diff --git a/cmd/soroban-rpc/internal/test/upgrade_test.go b/cmd/soroban-rpc/internal/test/upgrade_test.go deleted file mode 100644 index 6a59cefd..00000000 --- a/cmd/soroban-rpc/internal/test/upgrade_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package test - -import ( - "context" - "testing" - "time" - - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/db" -) - -func TestUpgradeFrom20To21(t *testing.T) { - if GetCoreMaxSupportedProtocol() != 21 { - t.Skip("Only test this for protocol 21") - } - test := NewTest(t, &TestConfig{ - ProtocolVersion: 20, - }) - - client := test.GetRPCLient() - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - // Upgrade to protocol 21 and re-upload the contract, which should cause a caching of the contract - // estimations - test.UpgradeProtocol(21) - // Wait for the ledger to advance, so that the simulation library passes the right protocol number - rpcDB := test.daemon.GetDB() - initialLedgerSequence, err := db.NewLedgerEntryReader(rpcDB).GetLatestLedgerSequence(context.Background()) - require.Eventually(t, - func() bool { - newLedgerSequence, err := db.NewLedgerEntryReader(rpcDB).GetLatestLedgerSequence(context.Background()) - require.NoError(t, err) - return newLedgerSequence > initialLedgerSequence - }, - time.Minute, - time.Second, - ) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - contractFnParameterSym := xdr.ScSymbol("world") - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "hello", - xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &contractFnParameterSym, - }, - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - - sendSuccessfulTransaction(t, client, sourceAccount, tx) -} From 71b82723d5c115616a2229796e05f32aff186c82 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 14 Jun 2024 11:43:24 +0200 Subject: [PATCH 02/13] Make test ports dynamic --- .../internal/integrationtest/archive_test.go | 4 +- ...> captive-core-integration-tests.cfg.tmpl} | 4 +- .../{core-start.sh => core-start.sh.tmpl} | 4 +- .../docker/docker-compose.rpc.yml | 4 +- .../infrastructure/docker/docker-compose.yml | 10 +- ...> stellar-core-integration-tests.cfg.tmpl} | 4 +- .../integrationtest/infrastructure/test.go | 188 +++++++++++++++--- .../integrationtest/infrastructure/util.go | 13 ++ .../internal/integrationtest/migrate_test.go | 20 +- 9 files changed, 202 insertions(+), 49 deletions(-) rename cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/{captive-core-integration-tests.cfg => captive-core-integration-tests.cfg.tmpl} (88%) rename cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/{core-start.sh => core-start.sh.tmpl} (81%) rename cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/{stellar-core-integration-tests.cfg => stellar-core-integration-tests.cfg.tmpl} (94%) diff --git a/cmd/soroban-rpc/internal/integrationtest/archive_test.go b/cmd/soroban-rpc/internal/integrationtest/archive_test.go index bbc10ad9..9da7805f 100644 --- a/cmd/soroban-rpc/internal/integrationtest/archive_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/archive_test.go @@ -16,7 +16,8 @@ import ( ) func TestArchiveUserAgent(t *testing.T) { - archiveHost := net.JoinHostPort("localhost", strconv.Itoa(infrastructure.StellarCoreArchivePort)) + ports := infrastructure.NewTestPorts(t) + archiveHost := net.JoinHostPort("localhost", strconv.Itoa(int(ports.CoreArchivePort))) proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: archiveHost}) userAgents := sync.Map{} historyArchiveProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -26,6 +27,7 @@ func TestArchiveUserAgent(t *testing.T) { defer historyArchiveProxy.Close() cfg := &infrastructure.TestConfig{ + TestPorts: &ports, HistoryArchiveURL: historyArchiveProxy.URL, } diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl similarity index 88% rename from cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl index a772012a..8b87c65c 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl @@ -1,4 +1,4 @@ -PEER_PORT=11725 +PEER_PORT=${CORE_CAPTIVE_PORT} ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true UNSAFE_QUORUM=true @@ -14,5 +14,5 @@ NAME="local_core" HOME_DOMAIN="core.local" # From "SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" PUBLIC_KEY="GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS" -ADDRESS="localhost" +ADDRESS="localhost:${CORE_PORT}" QUALITY="MEDIUM" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl similarity index 81% rename from cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl index 9dd89ba6..75fd9d56 100755 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl @@ -20,9 +20,9 @@ if [ "$1" = "standalone" ]; then rm -rf ./history stellar-core new-hist vs - # serve history archives to horizon on port 1570 + # serve history archives to horizon on port CORE_ARCHIVE_PORT pushd ./history/vs/ - python3 -m http.server 1570 & + python3 -m http.server ${CORE_ARCHIVE_PORT} & popd fi diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml index 3443aff4..036eefaf 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml @@ -5,8 +5,8 @@ services: depends_on: - core ports: - - "8000:8000" - - "8080:8080" + - ${RPC_PORT}:${RPC_PORT} + - ${RPC_ADMIN_PORT}:${RPC_ADMIN_PORT} command: --config-path /soroban-rpc.config volumes: - ${RPC_CONFIG_MOUNT_DIR}/stellar-core-integration-tests.cfg:/stellar-core.cfg diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml index cf3e7b0d..212ded76 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml @@ -21,14 +21,14 @@ services: environment: - TRACY_NO_INVARIANT_CHECK=1 ports: - - "11625:11625" - - "11626:11626" + - ${CORE_PORT}:${CORE_PORT} + - ${CORE_HTTP_PORT}:${CORE_HTTP_PORT} # add extra port for history archive server - - "1570:1570" + - ${CORE_ARCHIVE_PORT}:${CORE_ARCHIVE_PORT} entrypoint: /usr/bin/env command: /start standalone volumes: - - ./stellar-core-integration-tests.cfg:/stellar-core.cfg - - ./core-start.sh:/start + - ${CORE_MOUNT_DIR}/stellar-core-integration-tests.cfg:/stellar-core.cfg + - ${CORE_MOUNT_DIR}/core-start.sh:/start extra_hosts: - "host.docker.internal:host-gateway" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl similarity index 94% rename from cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl index 5462dbfb..c40599e3 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl @@ -4,8 +4,8 @@ DEPRECATED_SQL_LEDGER_STATE=false NETWORK_PASSPHRASE="Standalone Network ; February 2017" -PEER_PORT=11625 -HTTP_PORT=11626 +PEER_PORT=${CORE_PORT} +HTTP_PORT=${CORE_HTTP_PORT} PUBLIC_HTTP_PORT=true NODE_SEED="SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go index 40b4da3e..f43a320e 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -36,13 +36,9 @@ import ( const ( StandaloneNetworkPassphrase = "Standalone Network ; February 2017" MaxSupportedProtocolVersion = 21 - StellarCoreArchivePort = 1570 - stellarCorePort = 11626 FriendbotURL = "http://localhost:8000/friendbot" // Needed when Core is run with ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true checkpointFrequency = 8 - sorobanRPCPort = 8000 - adminPort = 8080 helloWorldContractPath = "../../../../../wasms/test_hello_world.wasm" ) @@ -52,15 +48,74 @@ type TestConfig struct { UseReleasedRPCVersion string UseSQLitePath string HistoryArchiveURL string + TestPorts *TestPorts + OnlyRPC bool +} + +type TestPorts struct { + RPCPort uint16 + RPCAdminPort uint16 + CorePort uint16 + CoreHTTPPort uint16 + CoreArchivePort uint16 + CoreCaptivePeerPort uint16 +} + +func NewTestPorts(t *testing.T) TestPorts { + return TestPorts{ + RPCPort: getFreeTCPPort(t), + RPCAdminPort: getFreeTCPPort(t), + CorePort: getFreeTCPPort(t), + CoreHTTPPort: getFreeTCPPort(t), + CoreArchivePort: getFreeTCPPort(t), + CoreCaptivePeerPort: getFreeTCPPort(t), + } + +} + +func (tp TestPorts) getMapping() map[string]uint16 { + return map[string]uint16{ + "RPC_PORT": tp.RPCPort, + "RPC_ADMIN_PORT": tp.RPCAdminPort, + "CORE_PORT": tp.CorePort, + "CORE_HTTP_PORT": tp.CoreHTTPPort, + "CORE_ARCHIVE_PORT": tp.CoreArchivePort, + "CORE_CAPTIVE_PORT": tp.CoreCaptivePeerPort, + } +} + +func (tp TestPorts) getEnvs() []string { + mapping := tp.getMapping() + result := make([]string, 0, len(mapping)) + for k, v := range mapping { + result = append(result, fmt.Sprintf("%s=%d", k, v)) + } + return result +} + +func (tp TestPorts) getFuncMapping() func(string) string { + mapping := tp.getMapping() + return func(name string) string { + port, ok := mapping[name] + if !ok { + // try to leave it as it was + return "$" + name + } + return strconv.Itoa(int(port)) + } } type Test struct { t *testing.T + testPorts TestPorts + protocolVersion uint32 historyArchiveURL string + expandedTemplatesDir string + rpcContainerVersion string rpcContainerConfigMountDir string rpcContainerSQLiteMountDir string @@ -74,6 +129,7 @@ type Test struct { masterAccount txnbuild.Account shutdownOnce sync.Once shutdown func() + onlyRPC bool } func NewTest(t *testing.T, cfg *TestConfig) *Test { @@ -93,6 +149,15 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { i.rpcContainerVersion = cfg.UseReleasedRPCVersion i.protocolVersion = cfg.ProtocolVersion sqlLitePath = cfg.UseSQLitePath + i.onlyRPC = cfg.OnlyRPC + if cfg.TestPorts != nil { + i.testPorts = *cfg.TestPorts + } else { + // TODO: this is ugly + i.testPorts = NewTestPorts(t) + } + } else { + i.testPorts = NewTestPorts(t) } if i.protocolVersion == 0 { @@ -100,11 +165,22 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { i.protocolVersion = GetCoreMaxSupportedProtocol() } + i.expandedTemplatesDir = i.createExpandedTemplatesDir() rpcCfg := i.getRPConfig(sqlLitePath) if i.runRPCInContainer() { i.rpcContainerConfigMountDir = i.createRPCContainerMountDir(rpcCfg) } - i.runComposeCommand("up", "--detach", "--quiet-pull", "--no-color") + // TODO: this is really really ugly + if i.runRPCInContainer() || !i.onlyRPC { + upCmd := []string{"up"} + if i.runRPCInContainer() && i.onlyRPC { + if cfg.OnlyRPC { + upCmd = append(upCmd, "rpc") + } + } + upCmd = append(upCmd, "--detach", "--quiet-pull", "--no-color") + i.runComposeCommand(upCmd...) + } if i.runRPCInContainer() { cmd := i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") cmd.Stdout = os.Stdout @@ -112,9 +188,11 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { require.NoError(t, cmd.Start()) } i.prepareShutdownHandlers() - i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(stellarCorePort)} - i.waitForCore() - i.waitForCheckpoint() + if !i.onlyRPC { + i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(int(i.testPorts.CoreHTTPPort))} + i.waitForCore() + i.waitForCheckpoint() + } if !i.runRPCInContainer() { i.daemon = i.createDaemon(rpcCfg) go i.daemon.Run() @@ -140,11 +218,11 @@ func (i *Test) MasterAccount() txnbuild.Account { } func (i *Test) GetSorobanRPCURL() string { - return fmt.Sprintf("http://localhost:%d", sorobanRPCPort) + return fmt.Sprintf("http://localhost:%d", i.testPorts.RPCPort) } func (i *Test) GetAdminURL() string { - return fmt.Sprintf("http://localhost:%d", adminPort) + return fmt.Sprintf("http://localhost:%d", i.testPorts.RPCAdminPort) } func (i *Test) getCoreInfo() (*proto.InfoResponse, error) { @@ -179,20 +257,20 @@ func (i *Test) getRPConfig(sqlitePath string) map[string]string { } } - archiveURL := fmt.Sprintf("http://localhost:%d", StellarCoreArchivePort) + archiveURL := fmt.Sprintf("http://localhost:%d", i.testPorts.CoreArchivePort) if i.runRPCInContainer() { // the archive needs to be accessed from the container // where core is Core's hostname - archiveURL = fmt.Sprintf("http://core:%d", StellarCoreArchivePort) + archiveURL = fmt.Sprintf("http://core:%d", i.testPorts.CoreArchivePort) } if i.historyArchiveURL != "" { // an archive URL was supplied explicitly archiveURL = i.historyArchiveURL } - captiveCoreConfigPath := path.Join(GetCurrentDirectory(), "docker", "captive-core-integration-tests.cfg") + captiveCoreConfigPath := path.Join(i.expandedTemplatesDir, "captive-core-integration-tests.cfg") bindHost := "localhost" - stellarCoreURL := fmt.Sprintf("http://localhost:%d", stellarCorePort) + stellarCoreURL := fmt.Sprintf("http://localhost:%d", i.testPorts.CoreHTTPPort) if i.runRPCInContainer() { // The file will be inside the container captiveCoreConfigPath = "/stellar-core.cfg" @@ -201,7 +279,7 @@ func (i *Test) getRPConfig(sqlitePath string) map[string]string { // The container needs to use the sqlite mount point i.rpcContainerSQLiteMountDir = filepath.Dir(sqlitePath) sqlitePath = "/db/" + filepath.Base(sqlitePath) - stellarCoreURL = fmt.Sprintf("http://core:%d", stellarCorePort) + stellarCoreURL = fmt.Sprintf("http://core:%d", i.testPorts.CoreHTTPPort) } // in the container @@ -211,8 +289,8 @@ func (i *Test) getRPConfig(sqlitePath string) map[string]string { } return map[string]string{ - "ENDPOINT": fmt.Sprintf("%s:%d", bindHost, sorobanRPCPort), - "ADMIN_ENDPOINT": fmt.Sprintf("%s:%d", bindHost, adminPort), + "ENDPOINT": fmt.Sprintf("%s:%d", bindHost, i.testPorts.RPCPort), + "ADMIN_ENDPOINT": fmt.Sprintf("%s:%d", bindHost, i.testPorts.RPCAdminPort), "STELLAR_CORE_URL": stellarCoreURL, "CORE_REQUEST_TIMEOUT": "2s", "STELLAR_CORE_BINARY_PATH": coreBinaryPath, @@ -254,24 +332,58 @@ func (i *Test) waitForRPC() { ) } +func (i *Test) createExpandedTemplatesDir() string { + mountDir := i.t.TempDir() + configDir := filepath.Join(GetCurrentDirectory(), "docker") + entries, err := os.ReadDir(configDir) + require.NoError(i.t, err) + fmapping := i.testPorts.getFuncMapping() + for _, entry := range entries { + if !entry.Type().IsRegular() { + continue + } + if !strings.HasSuffix(entry.Name(), ".tmpl") { + continue + } + originalPath := filepath.Join(configDir, entry.Name()) + in, err := os.ReadFile(originalPath) + require.NoError(i.t, err) + out := os.Expand(string(in), fmapping) + targetPath := filepath.Join(mountDir, strings.TrimSuffix(entry.Name(), ".tmpl")) + info, err := entry.Info() + require.NoError(i.t, err) + err = os.WriteFile(targetPath, []byte(out), info.Mode()) + require.NoError(i.t, err) + } + return mountDir +} + func (i *Test) createRPCContainerMountDir(rpcConfig map[string]string) string { mountDir := i.t.TempDir() - getOldVersionCaptiveCoreConfigVersion := func(dir string) ([]byte, error) { - cmd := exec.Command("git", "show", fmt.Sprintf("v%s:./%s/captive-core-integration-tests.cfg", i.rpcContainerVersion, dir)) + getOldVersionCaptiveCoreConfigVersion := func(dir string, filename string) ([]byte, error) { + cmd := exec.Command("git", "show", fmt.Sprintf("v%s:./%s/%s", i.rpcContainerVersion, dir, filename)) cmd.Dir = GetCurrentDirectory() return cmd.Output() } - // Get old version of captive-core-integration-tests.cfg - out, err := getOldVersionCaptiveCoreConfigVersion("docker") + // Get old version of captive-core-integration-tests.cfg.tmpl + out, err := getOldVersionCaptiveCoreConfigVersion("docker", "captive-core-integration-tests.cfg.tmpl") if err != nil { - out, err = getOldVersionCaptiveCoreConfigVersion("../../test") + // Try the directory before the integration test refactoring + // TODO: remove this hack after protocol 22 is released + out, err = getOldVersionCaptiveCoreConfigVersion("../../test", "captive-core-integration-tests.cfg") + outStr := strings.Replace(string(out), `ADDRESS="localhost"`, fmt.Sprintf(`ADDRESS="localhost:%d"`, i.testPorts.CorePort), -1) + out = []byte(outStr) } require.NoError(i.t, err) - // replace ADDRESS="localhost" by ADDRESS="core", so that the container can find core - captiveCoreCfgContents := strings.Replace(string(out), `ADDRESS="localhost"`, `ADDRESS="core"`, -1) + // Apply expansion + captiveCoreCfgContents := os.Expand(string(out), i.testPorts.getFuncMapping()) + + // TODO: maybe it would be better to not place localhost in the file (and use a host replacement) + // replace ADDRESS="localhost by ADDRESS="core, so that the container can find core + captiveCoreCfgContents = strings.Replace(captiveCoreCfgContents, `ADDRESS="localhost`, `ADDRESS="core`, -1) err = os.WriteFile(filepath.Join(mountDir, "stellar-core-integration-tests.cfg"), []byte(captiveCoreCfgContents), 0666) require.NoError(i.t, err) @@ -308,12 +420,19 @@ func (i *Test) getComposeCommand(args ...string) *exec.Cmd { cmdline := append(configFiles, args...) cmd := exec.Command("docker-compose", cmdline...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, + "CORE_MOUNT_DIR="+i.expandedTemplatesDir, + ) + cmd.Env = append(cmd.Env, i.testPorts.getEnvs()...) + if img := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_DOCKER_IMG"); img != "" { cmd.Env = append( cmd.Env, "CORE_IMAGE="+img, ) } + if i.runRPCInContainer() { cmd.Env = append( cmd.Env, @@ -324,9 +443,7 @@ func (i *Test) getComposeCommand(args ...string) *exec.Cmd { "RPC_GID="+strconv.Itoa(os.Getgid()), ) } - if len(cmd.Env) > 0 { - cmd.Env = append(cmd.Env, os.Environ()...) - } + return cmd } @@ -349,11 +466,24 @@ func (i *Test) prepareShutdownHandlers() { done := make(chan struct{}) i.shutdown = func() { close(done) - i.StopRPC() + if i.daemon != nil { + i.daemon.Close() + i.daemon = nil + } if i.rpcClient != nil { i.rpcClient.Close() } - i.runComposeCommand("down", "-v") + // TODO: this is ugly + if i.runRPCInContainer() || !i.onlyRPC { + downCmd := []string{"down"} + if i.runRPCInContainer() && i.onlyRPC { + if i.onlyRPC { + downCmd = append(downCmd, "rpc") + } + } + downCmd = append(downCmd, "-v") + i.runComposeCommand(downCmd...) + } if i.rpcContainerLogsCommand != nil { i.rpcContainerLogsCommand.Wait() } diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go index a2aae8ea..f98b3c77 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go @@ -1,10 +1,12 @@ package infrastructure import ( + "net" "path/filepath" "runtime" "github.com/stellar/go/txnbuild" + "github.com/stretchr/testify/require" ) //go:noinline @@ -13,6 +15,17 @@ func GetCurrentDirectory() string { return filepath.Dir(currentFilename) } +func getFreeTCPPort(t require.TestingT) uint16 { + var a *net.TCPAddr + a, err := net.ResolveTCPAddr("tcp", "localhost:0") + require.NoError(t, err) + var l *net.TCPListener + l, err = net.ListenTCP("tcp", a) + require.NoError(t, err) + defer l.Close() + return uint16(l.Addr().(*net.TCPAddr).Port) +} + func CreateTransactionParams(account txnbuild.Account, op txnbuild.Operation) txnbuild.TransactionParams { return txnbuild.TransactionParams{ SourceAccount: account, diff --git a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go index 9e0a83de..55cb4618 100644 --- a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go @@ -38,20 +38,28 @@ func TestMigrate(t *testing.T) { } func testMigrateFromVersion(t *testing.T, version string) { + originalPorts := infrastructure.NewTestPorts(t) sqliteFile := filepath.Join(t.TempDir(), "soroban-rpc.db") test := infrastructure.NewTest(t, &infrastructure.TestConfig{ UseReleasedRPCVersion: version, UseSQLitePath: sqliteFile, + TestPorts: &originalPorts, }) - client := test.GetRPCLient() - // Submit an event-logging transaction in the version to migrate from submitTransactionResponse, _ := test.UploadHelloWorldContract() - // Run the current RPC version, but the previous network and sql database (causing a data migration if needed) + // Replace RPC with the current version, but keeping the previous network and sql database (causing a data migration if needed) + // We need to do some wiring to plug RPC into the prior network test.StopRPC() - test = infrastructure.NewTest(t, &infrastructure.TestConfig{UseSQLitePath: sqliteFile}) + freshPorts := infrastructure.NewTestPorts(t) + ports := originalPorts + ports.RPCPort = freshPorts.RPCPort + test = infrastructure.NewTest(t, &infrastructure.TestConfig{ + TestPorts: &ports, + OnlyRPC: true, + UseSQLitePath: sqliteFile, + }) // make sure that the transaction submitted before and its events exist in current RPC var transactionsResult methods.GetTransactionsResponse @@ -61,7 +69,7 @@ func testMigrateFromVersion(t *testing.T, version string) { Limit: 1, }, } - err := client.CallResult(context.Background(), "getTransactions", getTransactions, &transactionsResult) + err := test.GetRPCLient().CallResult(context.Background(), "getTransactions", getTransactions, &transactionsResult) require.NoError(t, err) require.Equal(t, 1, len(transactionsResult.Transactions)) require.Equal(t, submitTransactionResponse.Ledger, transactionsResult.Transactions[0].Ledger) @@ -73,7 +81,7 @@ func testMigrateFromVersion(t *testing.T, version string) { Limit: 1, }, } - err = client.CallResult(context.Background(), "getEvents", getEventsRequest, &eventsResult) + err = test.GetRPCLient().CallResult(context.Background(), "getEvents", getEventsRequest, &eventsResult) require.NoError(t, err) require.Equal(t, len(eventsResult.Events), 1) require.Equal(t, submitTransactionResponse.Ledger, uint32(eventsResult.Events[0].Ledger)) From 5e1e7fcf63f97f07fe10af616e142624ae2fad30 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 14 Jun 2024 12:42:54 +0200 Subject: [PATCH 03/13] Run integration tests in parallel --- .../integrationtest/infrastructure/test.go | 19 ++++++++++++++++++- .../internal/integrationtest/migrate_test.go | 2 ++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go index f43a320e..9d8f6aad 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -9,6 +9,7 @@ import ( "os/signal" "path" "path/filepath" + "regexp" "strconv" "strings" "sync" @@ -50,6 +51,7 @@ type TestConfig struct { HistoryArchiveURL string TestPorts *TestPorts OnlyRPC bool + NoParallel bool } type TestPorts struct { @@ -143,6 +145,7 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { Sequence: 0, } + parallel := true sqlLitePath := "" if cfg != nil { i.historyArchiveURL = cfg.HistoryArchiveURL @@ -150,6 +153,7 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { i.protocolVersion = cfg.ProtocolVersion sqlLitePath = cfg.UseSQLitePath i.onlyRPC = cfg.OnlyRPC + parallel = !cfg.NoParallel if cfg.TestPorts != nil { i.testPorts = *cfg.TestPorts } else { @@ -159,6 +163,9 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { } else { i.testPorts = NewTestPorts(t) } + if parallel { + t.Parallel() + } if i.protocolVersion == 0 { // Default to the maximum supported protocol version @@ -410,6 +417,13 @@ func (i *Test) createDaemon(env map[string]string) *daemon.Daemon { return daemon.MustNew(&cfg) } +var nonAlphanumericRegex = regexp.MustCompile("[^a-zA-Z0-9]+") + +func (i *Test) getComposeProjectName() string { + alphanumeric := nonAlphanumericRegex.ReplaceAllString(i.t.Name(), "") + return strings.ToLower(alphanumeric) +} + func (i *Test) getComposeCommand(args ...string) *exec.Cmd { integrationYaml := filepath.Join(GetCurrentDirectory(), "docker", "docker-compose.yml") configFiles := []string{"-f", integrationYaml} @@ -417,7 +431,10 @@ func (i *Test) getComposeCommand(args ...string) *exec.Cmd { rpcYaml := filepath.Join(GetCurrentDirectory(), "docker", "docker-compose.rpc.yml") configFiles = append(configFiles, "-f", rpcYaml) } - cmdline := append(configFiles, args...) + // Use separate projects to run them in parallel + projectName := i.getComposeProjectName() + cmdline := append([]string{"-p", projectName}, configFiles...) + cmdline = append(cmdline, args...) cmd := exec.Command("docker-compose", cmdline...) cmd.Env = os.Environ() diff --git a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go index 55cb4618..b401b545 100644 --- a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go @@ -59,6 +59,8 @@ func testMigrateFromVersion(t *testing.T, version string) { TestPorts: &ports, OnlyRPC: true, UseSQLitePath: sqliteFile, + // We don't want to mark the test as parallel twice since it causes a panic + NoParallel: true, }) // make sure that the transaction submitted before and its events exist in current RPC From 6d88a73762e2929981c5e3a457f9f0e475d49fe7 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 14 Jun 2024 15:28:05 +0200 Subject: [PATCH 04/13] Simplify getVersionInfo test and remove rage condition --- .../integrationtest/get_version_info_test.go | 52 ++++--------------- 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go b/cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go index c7ebf413..b1bdb858 100644 --- a/cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go @@ -2,12 +2,8 @@ package integrationtest import ( "context" - "fmt" - "os/exec" "testing" - "github.com/stretchr/testify/require" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" @@ -16,50 +12,22 @@ import ( "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) +func init() { + // Initialize variables to non-empty values + config.CommitHash = "commitHash" + config.BuildTimestamp = "buildTimestamp" +} + func TestGetVersionInfoSucceeds(t *testing.T) { test := infrastructure.NewTest(t, nil) - version, commitHash, buildTimeStamp := config.Version, config.CommitHash, config.BuildTimestamp - - populateVersionInfo(t) - - // reset to previous config values - t.Cleanup(func() { - config.Version = version - config.CommitHash = commitHash - config.BuildTimestamp = buildTimeStamp - }) - - client := test.GetRPCLient() - var result methods.GetVersionInfoResponse - err := client.CallResult(context.Background(), "getVersionInfo", nil, &result) + err := test.GetRPCLient().CallResult(context.Background(), "getVersionInfo", nil, &result) assert.NoError(t, err) - assert.Equal(t, config.Version, result.Version) - assert.Equal(t, config.BuildTimestamp, result.BuildTimestamp) - assert.Equal(t, config.CommitHash, result.CommitHash) + assert.Equal(t, "0.0.0", result.Version) + assert.Equal(t, "buildTimestamp", result.BuildTimestamp) + assert.Equal(t, "commitHash", result.CommitHash) assert.Equal(t, test.GetProtocolVersion(), result.ProtocolVersion) assert.NotEmpty(t, result.CaptiveCoreVersion) - -} - -// Runs git commands to fetch version information -func populateVersionInfo(t *testing.T) { - - execFunction := func(command string, args ...string) string { - cmd := exec.Command(command, args...) - t.Log("Running", cmd.Env, cmd.Args) - out, innerErr := cmd.Output() - if exitErr, ok := innerErr.(*exec.ExitError); ok { - fmt.Printf("stdout:\n%s\n", string(out)) - fmt.Printf("stderr:\n%s\n", string(exitErr.Stderr)) - } - require.NoError(t, innerErr) - return string(out) - } - - config.Version = execFunction("git", "describe", "--tags", "--always", "--abbrev=0", "--match='v[0-9]*.[0-9]*.[0-9]*'") - config.CommitHash = execFunction("git", "rev-parse", "HEAD") - config.BuildTimestamp = execFunction("date", "+%Y-%m-%dT%H:%M:%S") } From 6b7a0c76e51e6be720559a438a9893a22551cb9f Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 14 Jun 2024 15:50:30 +0200 Subject: [PATCH 05/13] Clean up test infra a bit --- .../integrationtest/infrastructure/test.go | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go index 9d8f6aad..cf9b8d33 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -147,6 +147,7 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { parallel := true sqlLitePath := "" + testPortsInitialized := false if cfg != nil { i.historyArchiveURL = cfg.HistoryArchiveURL i.rpcContainerVersion = cfg.UseReleasedRPCVersion @@ -156,11 +157,11 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { parallel = !cfg.NoParallel if cfg.TestPorts != nil { i.testPorts = *cfg.TestPorts - } else { - // TODO: this is ugly - i.testPorts = NewTestPorts(t) + testPortsInitialized = true } - } else { + } + + if !testPortsInitialized { i.testPorts = NewTestPorts(t) } if parallel { @@ -177,22 +178,21 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { if i.runRPCInContainer() { i.rpcContainerConfigMountDir = i.createRPCContainerMountDir(rpcCfg) } - // TODO: this is really really ugly + if i.runRPCInContainer() || !i.onlyRPC { + // There are containerized workloads upCmd := []string{"up"} if i.runRPCInContainer() && i.onlyRPC { - if cfg.OnlyRPC { - upCmd = append(upCmd, "rpc") - } + upCmd = append(upCmd, "rpc") } upCmd = append(upCmd, "--detach", "--quiet-pull", "--no-color") i.runComposeCommand(upCmd...) - } - if i.runRPCInContainer() { - cmd := i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - require.NoError(t, cmd.Start()) + if i.runRPCInContainer() { + i.rpcContainerLogsCommand = i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") + i.rpcContainerLogsCommand.Stdout = os.Stdout + i.rpcContainerLogsCommand.Stderr = os.Stderr + require.NoError(t, i.rpcContainerLogsCommand.Start()) + } } i.prepareShutdownHandlers() if !i.onlyRPC { @@ -464,7 +464,6 @@ func (i *Test) getComposeCommand(args ...string) *exec.Cmd { return cmd } -// Runs a docker-compose command applied to the above configs func (i *Test) runComposeCommand(args ...string) { cmd := i.getComposeCommand(args...) i.t.Log("Running", cmd.Args) @@ -490,13 +489,11 @@ func (i *Test) prepareShutdownHandlers() { if i.rpcClient != nil { i.rpcClient.Close() } - // TODO: this is ugly if i.runRPCInContainer() || !i.onlyRPC { + // There were containerized workloads we should bring down downCmd := []string{"down"} if i.runRPCInContainer() && i.onlyRPC { - if i.onlyRPC { - downCmd = append(downCmd, "rpc") - } + downCmd = append(downCmd, "rpc") } downCmd = append(downCmd, "-v") i.runComposeCommand(downCmd...) From b3ba5697207414564f6308d5d2efa96a2d2ea77b Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 14 Jun 2024 16:01:52 +0200 Subject: [PATCH 06/13] Tweak integration test invocation in CI --- .github/workflows/soroban-rpc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/soroban-rpc.yml b/.github/workflows/soroban-rpc.yml index c3104ca5..27ad3b30 100644 --- a/.github/workflows/soroban-rpc.yml +++ b/.github/workflows/soroban-rpc.yml @@ -194,4 +194,4 @@ jobs: - name: Run Soroban RPC Integration Tests run: | make install_rust - go test -race -timeout 60m -v ./cmd/soroban-rpc/internal/integrationtest/... + go test -race -timeout 20m ./cmd/soroban-rpc/internal/integrationtest/... From 44677beaa09d924a648ab133be57f7123ec685a1 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 14 Jun 2024 23:00:06 +0200 Subject: [PATCH 07/13] Stop preallocating tcp ports to avoid clashes --- cmd/soroban-rpc/internal/daemon/daemon.go | 35 +- .../internal/integrationtest/archive_test.go | 53 ++- .../integrationtest/get_transactions_test.go | 3 +- .../integrationtest/infrastructure/client.go | 43 +- .../infrastructure/contract.go | 3 +- .../captive-core-integration-tests.cfg.tmpl | 10 +- .../{core-start.sh.tmpl => core-start.sh} | 11 +- .../docker/docker-compose.rpc.yml | 12 +- .../infrastructure/docker/docker-compose.yml | 18 +- ...mpl => stellar-core-integration-tests.cfg} | 4 +- .../integrationtest/infrastructure/test.go | 430 +++++++++--------- .../integrationtest/infrastructure/util.go | 13 + .../internal/integrationtest/migrate_test.go | 15 +- .../simulate_transaction_test.go | 3 +- 14 files changed, 385 insertions(+), 268 deletions(-) rename cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/{core-start.sh.tmpl => core-start.sh} (64%) rename cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/{stellar-core-integration-tests.cfg.tmpl => stellar-core-integration-tests.cfg} (94%) diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index 39685927..732cd500 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -3,6 +3,7 @@ package daemon import ( "context" "errors" + "net" "net/http" "net/http/pprof" //nolint:gosec "os" @@ -50,7 +51,9 @@ type Daemon struct { jsonRPCHandler *internal.Handler logger *supportlog.Entry preflightWorkerPool *preflight.PreflightWorkerPool + listener net.Listener server *http.Server + adminListener net.Listener adminServer *http.Server closeOnce sync.Once closeError error @@ -62,6 +65,15 @@ func (d *Daemon) GetDB() *db.DB { return d.db } +func (d *Daemon) GetEndpointAddrs() (net.TCPAddr, *net.TCPAddr) { + var addr = d.listener.Addr().(*net.TCPAddr) + var adminAddr *net.TCPAddr + if d.adminListener != nil { + adminAddr = d.adminListener.Addr().(*net.TCPAddr) + } + return *addr, adminAddr +} + func (d *Daemon) close() { shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), defaultShutdownGracePeriod) defer shutdownRelease() @@ -251,8 +263,13 @@ func MustNew(cfg *config.Config) *Daemon { daemon.ingestService = ingestService daemon.jsonRPCHandler = &jsonRPCHandler + // Use a separate listener in order to obtain the actual TCP port + // when using dynamic ports during testing (e.g. endpoint="localhost:0") + daemon.listener, err = net.Listen("tcp", cfg.Endpoint) + if err != nil { + daemon.logger.WithError(err).WithField("endpoint", cfg.Endpoint).Fatal("cannot listen on endpoint") + } daemon.server = &http.Server{ - Addr: cfg.Endpoint, Handler: httpHandler, ReadTimeout: defaultReadTimeout, } @@ -269,7 +286,11 @@ func MustNew(cfg *config.Config) *Daemon { adminMux.Handle("/debug/pprof/"+profile.Name(), pprof.Handler(profile.Name())) } adminMux.Handle("/metrics", promhttp.HandlerFor(metricsRegistry, promhttp.HandlerOpts{})) - daemon.adminServer = &http.Server{Addr: cfg.AdminEndpoint, Handler: adminMux} + daemon.adminListener, err = net.Listen("tcp", cfg.AdminEndpoint) + if err != nil { + daemon.logger.WithError(err).WithField("endpoint", cfg.Endpoint).Fatal("cannot listen on admin endpoint") + } + daemon.adminServer = &http.Server{Handler: adminMux} } daemon.registerMetrics() return daemon @@ -340,20 +361,22 @@ func (d *Daemon) mustInitializeStorage(cfg *config.Config) (*feewindow.FeeWindow func (d *Daemon) Run() { d.logger.WithFields(supportlog.F{ - "addr": d.server.Addr, + "addr": d.listener.Addr().String(), }).Info("starting HTTP server") panicGroup := util.UnrecoverablePanicGroup.Log(d.logger) panicGroup.Go(func() { - if err := d.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - // Error starting or closing listener: + if err := d.server.Serve(d.listener); !errors.Is(err, http.ErrServerClosed) { d.logger.WithError(err).Fatal("soroban JSON RPC server encountered fatal error") } }) if d.adminServer != nil { + d.logger.WithFields(supportlog.F{ + "addr": d.adminListener.Addr().String(), + }).Info("starting Admin HTTP server") panicGroup.Go(func() { - if err := d.adminServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + if err := d.adminServer.Serve(d.adminListener); !errors.Is(err, http.ErrServerClosed) { d.logger.WithError(err).Error("soroban admin server encountered fatal error") } }) diff --git a/cmd/soroban-rpc/internal/integrationtest/archive_test.go b/cmd/soroban-rpc/internal/integrationtest/archive_test.go index 9da7805f..57179284 100644 --- a/cmd/soroban-rpc/internal/integrationtest/archive_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/archive_test.go @@ -4,38 +4,55 @@ import ( "net" "net/http" "net/http/httptest" - "net/http/httputil" - "net/url" - "strconv" "sync" "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" ) func TestArchiveUserAgent(t *testing.T) { - ports := infrastructure.NewTestPorts(t) - archiveHost := net.JoinHostPort("localhost", strconv.Itoa(int(ports.CoreArchivePort))) - proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: archiveHost}) userAgents := sync.Map{} - historyArchiveProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userAgents.Store(r.Header["User-Agent"][0], "") - proxy.ServeHTTP(w, r) + historyArchive := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + agent := r.Header["User-Agent"][0] + t.Log("agent", agent) + userAgents.Store(agent, "") + if r.URL.Path == "/.well-known/stellar-history.json" || r.URL.Path == "/history/00/00/00/history-0000001f.json" { + w.Write([]byte(`{ + "version": 1, + "server": "stellar-core 21.0.1 (dfd3dbff1d9cad4dc31e022de6ac2db731b4b326)", + "currentLedger": 31, + "networkPassphrase": "Standalone Network ; February 2017", + "currentBuckets": [] +}`)) + return + } + // emulate a problem with the archive + w.WriteHeader(http.StatusInternalServerError) })) - defer historyArchiveProxy.Close() + defer historyArchive.Close() + historyPort := historyArchive.Listener.Addr().(*net.TCPAddr).Port cfg := &infrastructure.TestConfig{ - TestPorts: &ports, - HistoryArchiveURL: historyArchiveProxy.URL, + OnlyRPC: &infrastructure.TestOnlyRPCConfig{ + CorePorts: infrastructure.TestCorePorts{ + CoreArchivePort: uint16(historyPort), + }, + DontWait: true, + }, } infrastructure.NewTest(t, cfg) - _, ok := userAgents.Load("soroban-rpc/0.0.0") - assert.True(t, ok, "rpc service should set user agent for history archives") - - _, ok = userAgents.Load("soroban-rpc/0.0.0/captivecore") - assert.True(t, ok, "rpc captive core should set user agent for history archives") + require.Eventually(t, + func() bool { + _, ok1 := userAgents.Load("soroban-rpc/0.0.0") + _, ok2 := userAgents.Load("soroban-rpc/0.0.0/captivecore") + return ok1 && ok2 + }, + 5*time.Second, + time.Second, + ) } diff --git a/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go b/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go index 90869bfa..6af5954f 100644 --- a/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/creachadair/jrpc2" "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/assert" @@ -33,7 +32,7 @@ func buildSetOptionsTxParams(account txnbuild.SimpleAccount) txnbuild.Transactio // client - the JSON-RPC client used to send the transactions. // // Returns a slice of ledger numbers corresponding to where each transaction was recorded. -func sendTransactions(t *testing.T, client *jrpc2.Client) []uint32 { +func sendTransactions(t *testing.T, client *infrastructure.Client) []uint32 { kp := keypair.Root(infrastructure.StandaloneNetworkPassphrase) address := kp.Address() diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go index 1f536b43..d3e5b48b 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go @@ -7,6 +7,7 @@ import ( "time" "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/keypair" "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/txnbuild" @@ -17,7 +18,41 @@ import ( "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" ) -func getTransaction(t *testing.T, client *jrpc2.Client, hash string) methods.GetTransactionResponse { +// Client is a jrpc2 client which tolerates errors +type Client struct { + url string + cli *jrpc2.Client + opts *jrpc2.ClientOptions +} + +func NewClient(url string, opts *jrpc2.ClientOptions) *Client { + c := &Client{url: url, opts: opts} + c.refreshClient() + return c +} + +func (c *Client) refreshClient() { + if c.cli != nil { + c.cli.Close() + } + ch := jhttp.NewChannel(c.url, nil) + c.cli = jrpc2.NewClient(ch, c.opts) +} + +func (c *Client) CallResult(ctx context.Context, method string, params, result any) error { + err := c.cli.CallResult(ctx, method, params, result) + if err != nil { + // This is needed because of https://github.com/creachadair/jrpc2/issues/118 + c.refreshClient() + } + return err +} + +func (c *Client) Close() error { + return c.cli.Close() +} + +func getTransaction(t *testing.T, client *Client, hash string) methods.GetTransactionResponse { var result methods.GetTransactionResponse for i := 0; i < 60; i++ { request := methods.GetTransactionRequest{Hash: hash} @@ -35,7 +70,7 @@ func getTransaction(t *testing.T, client *jrpc2.Client, hash string) methods.Get return result } -func SendSuccessfulTransaction(t *testing.T, client *jrpc2.Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { +func SendSuccessfulTransaction(t *testing.T, client *Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { tx, err := transaction.Sign(StandaloneNetworkPassphrase, kp) assert.NoError(t, err) b64, err := tx.Base64() @@ -92,7 +127,7 @@ func SendSuccessfulTransaction(t *testing.T, client *jrpc2.Client, kp *keypair.F return response } -func SimulateTransactionFromTxParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) methods.SimulateTransactionResponse { +func SimulateTransactionFromTxParams(t *testing.T, client *Client, params txnbuild.TransactionParams) methods.SimulateTransactionResponse { savedAutoIncrement := params.IncrementSequenceNum params.IncrementSequenceNum = false tx, err := txnbuild.NewTransaction(params) @@ -153,7 +188,7 @@ func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.Transaction return params } -func PreflightTransactionParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { +func PreflightTransactionParams(t *testing.T, client *Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { response := SimulateTransactionFromTxParams(t, client, params) // The preamble should be zero except for the special restore case assert.Nil(t, response.RestorePreamble) diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go index 7fa49d90..4daa1427 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go @@ -15,8 +15,7 @@ import ( var testSalt = sha256.Sum256([]byte("a1")) func GetHelloWorldContract() []byte { - testDirName := GetCurrentDirectory() - contractFile := path.Join(testDirName, helloWorldContractPath) + contractFile := path.Join(GetCurrentDirectory(), "../../../../../wasms/test_hello_world.wasm") ret, err := os.ReadFile(contractFile) if err != nil { str := fmt.Sprintf( diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl index 8b87c65c..670010c7 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl @@ -1,4 +1,8 @@ -PEER_PORT=${CORE_CAPTIVE_PORT} +# To fill in and use by RPC + +# This simply needs to be an unconflicting, unused port +# since captive core doesn't expect external connections +PEER_PORT=${CAPTIVE_CORE_PORT} ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true UNSAFE_QUORUM=true @@ -14,5 +18,7 @@ NAME="local_core" HOME_DOMAIN="core.local" # From "SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" PUBLIC_KEY="GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS" -ADDRESS="localhost:${CORE_PORT}" + +# should be "core" when running RPC in a container or "localhost:port" when running RPC in the host +ADDRESS="${CORE_HOST_PORT}" QUALITY="MEDIUM" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh similarity index 64% rename from cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh index 75fd9d56..550ad1eb 100755 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh.tmpl +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh @@ -12,17 +12,20 @@ fi echo "using config:" cat stellar-core.cfg -# initialize new db -stellar-core new-db +# initialize new db (retry a few times to wait for the database to be available) +until stellar-core new-db; do + sleep 0.2 + echo "couldn't create new db, retrying" +done if [ "$1" = "standalone" ]; then # initialize for new history archive path, remove any pre-existing on same path from base image rm -rf ./history stellar-core new-hist vs - # serve history archives to horizon on port CORE_ARCHIVE_PORT + # serve history archives to horizon on port 1570 pushd ./history/vs/ - python3 -m http.server ${CORE_ARCHIVE_PORT} & + python3 -m http.server 1570 & popd fi diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml index 036eefaf..6a0992d4 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml @@ -1,15 +1,19 @@ +include: + - docker-compose.yml services: rpc: platform: linux/amd64 image: stellar/soroban-rpc:${RPC_IMAGE_TAG} depends_on: - core - ports: - - ${RPC_PORT}:${RPC_PORT} - - ${RPC_ADMIN_PORT}:${RPC_ADMIN_PORT} + ports: # we omit the host-side ports to allocate them dynamically + # HTTP + - "127.0.0.1::8000" + # Admin HTTP + - "127.0.0.1::8080" command: --config-path /soroban-rpc.config volumes: - - ${RPC_CONFIG_MOUNT_DIR}/stellar-core-integration-tests.cfg:/stellar-core.cfg + - ${RPC_CONFIG_MOUNT_DIR}/captive-core-integration-tests.cfg:/stellar-core.cfg - ${RPC_CONFIG_MOUNT_DIR}/soroban-rpc.config:/soroban-rpc.config - ${RPC_SQLITE_MOUNT_DIR}:/db/ # Needed so that the sql database files created in the container diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml index 212ded76..579cf67b 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml @@ -1,7 +1,6 @@ services: core-postgres: image: postgres:9.6.17-alpine - restart: on-failure environment: - POSTGRES_PASSWORD=mysecretpassword - POSTGRES_DB=stellar @@ -17,18 +16,19 @@ services: image: ${CORE_IMAGE:-stellar/unsafe-stellar-core:21.0.1-1897.dfd3dbff1.focal} depends_on: - core-postgres - restart: on-failure environment: - TRACY_NO_INVARIANT_CHECK=1 - ports: - - ${CORE_PORT}:${CORE_PORT} - - ${CORE_HTTP_PORT}:${CORE_HTTP_PORT} - # add extra port for history archive server - - ${CORE_ARCHIVE_PORT}:${CORE_ARCHIVE_PORT} + ports: # we omit the host-side ports to allocate them dynamically + # peer + - "127.0.0.1:0:11625" + # http + - "127.0.0.1:0:11626" + # history archive + - "127.0.0.1:0:1570" entrypoint: /usr/bin/env command: /start standalone volumes: - - ${CORE_MOUNT_DIR}/stellar-core-integration-tests.cfg:/stellar-core.cfg - - ${CORE_MOUNT_DIR}/core-start.sh:/start + - ./stellar-core-integration-tests.cfg:/stellar-core.cfg + - ./core-start.sh:/start extra_hosts: - "host.docker.internal:host-gateway" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg similarity index 94% rename from cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg index c40599e3..5462dbfb 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg.tmpl +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/stellar-core-integration-tests.cfg @@ -4,8 +4,8 @@ DEPRECATED_SQL_LEDGER_STATE=false NETWORK_PASSPHRASE="Standalone Network ; February 2017" -PEER_PORT=${CORE_PORT} -HTTP_PORT=${CORE_HTTP_PORT} +PEER_PORT=11625 +HTTP_PORT=11626 PUBLIC_HTTP_PORT=true NODE_SEED="SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go index cf9b8d33..0c7191c6 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "fmt" + "net" "os" "os/exec" "os/signal" @@ -17,8 +18,6 @@ import ( "testing" "time" - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" "github.com/stellar/go/clients/stellarcore" "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/stellarcore" @@ -39,72 +38,51 @@ const ( MaxSupportedProtocolVersion = 21 FriendbotURL = "http://localhost:8000/friendbot" // Needed when Core is run with ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true - checkpointFrequency = 8 - helloWorldContractPath = "../../../../../wasms/test_hello_world.wasm" + checkpointFrequency = 8 + captiveCoreConfigFilename = "captive-core-integration-tests.cfg" + captiveCoreConfigTemplateFilename = captiveCoreConfigFilename + ".tmpl" + + inContainerCoreHostname = "core" + inContainerCorePort = 11625 + inContainerCoreHTTPPort = 11626 + inContainerCoreArchivePort = 1570 + // any unused port would do + inContainerCaptiveCorePort = 11725 + + inContainerRPCPort = 8000 + inContainerRPCAdminPort = 8080 ) +// Only run RPC, telling how to connect to Core +// and whether we should wait for it +type TestOnlyRPCConfig struct { + CorePorts TestCorePorts + DontWait bool +} + type TestConfig struct { ProtocolVersion uint32 // Run a previously released version of RPC (in a container) instead of the current version UseReleasedRPCVersion string - UseSQLitePath string - HistoryArchiveURL string - TestPorts *TestPorts - OnlyRPC bool - NoParallel bool -} - -type TestPorts struct { - RPCPort uint16 - RPCAdminPort uint16 - CorePort uint16 - CoreHTTPPort uint16 - CoreArchivePort uint16 - CoreCaptivePeerPort uint16 -} - -func NewTestPorts(t *testing.T) TestPorts { - return TestPorts{ - RPCPort: getFreeTCPPort(t), - RPCAdminPort: getFreeTCPPort(t), - CorePort: getFreeTCPPort(t), - CoreHTTPPort: getFreeTCPPort(t), - CoreArchivePort: getFreeTCPPort(t), - CoreCaptivePeerPort: getFreeTCPPort(t), - } - -} - -func (tp TestPorts) getMapping() map[string]uint16 { - return map[string]uint16{ - "RPC_PORT": tp.RPCPort, - "RPC_ADMIN_PORT": tp.RPCAdminPort, - "CORE_PORT": tp.CorePort, - "CORE_HTTP_PORT": tp.CoreHTTPPort, - "CORE_ARCHIVE_PORT": tp.CoreArchivePort, - "CORE_CAPTIVE_PORT": tp.CoreCaptivePeerPort, - } + // Use/Reuse a SQLite file instead of creating it from scratch + UseSQLitePath string + OnlyRPC *TestOnlyRPCConfig + // Do not mark the test as running in parallel + NoParallel bool } -func (tp TestPorts) getEnvs() []string { - mapping := tp.getMapping() - result := make([]string, 0, len(mapping)) - for k, v := range mapping { - result = append(result, fmt.Sprintf("%s=%d", k, v)) - } - return result +type TestCorePorts struct { + CorePort uint16 + CoreHTTPPort uint16 + CoreArchivePort uint16 + // This only needs to be an unconflicting port + captiveCorePort uint16 } -func (tp TestPorts) getFuncMapping() func(string) string { - mapping := tp.getMapping() - return func(name string) string { - port, ok := mapping[name] - if !ok { - // try to leave it as it was - return "$" + name - } - return strconv.Itoa(int(port)) - } +type TestPorts struct { + RPCPort uint16 + RPCAdminPort uint16 + TestCorePorts } type Test struct { @@ -114,16 +92,13 @@ type Test struct { protocolVersion uint32 - historyArchiveURL string - - expandedTemplatesDir string + rpcConfigFilesDir string rpcContainerVersion string - rpcContainerConfigMountDir string rpcContainerSQLiteMountDir string rpcContainerLogsCommand *exec.Cmd - rpcClient *jrpc2.Client + rpcClient *Client coreClient *stellarcore.Client daemon *daemon.Daemon @@ -146,39 +121,45 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { } parallel := true - sqlLitePath := "" - testPortsInitialized := false + sqlitePath := "" + shouldWaitForRPC := true if cfg != nil { - i.historyArchiveURL = cfg.HistoryArchiveURL i.rpcContainerVersion = cfg.UseReleasedRPCVersion i.protocolVersion = cfg.ProtocolVersion - sqlLitePath = cfg.UseSQLitePath - i.onlyRPC = cfg.OnlyRPC - parallel = !cfg.NoParallel - if cfg.TestPorts != nil { - i.testPorts = *cfg.TestPorts - testPortsInitialized = true + sqlitePath = cfg.UseSQLitePath + if cfg.OnlyRPC != nil { + i.onlyRPC = true + i.testPorts.TestCorePorts = cfg.OnlyRPC.CorePorts + shouldWaitForRPC = !cfg.OnlyRPC.DontWait } + parallel = !cfg.NoParallel } - - if !testPortsInitialized { - i.testPorts = NewTestPorts(t) + if sqlitePath == "" { + sqlitePath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") } + if parallel { t.Parallel() } + // TODO: this function is pretty unreadable + if i.protocolVersion == 0 { // Default to the maximum supported protocol version i.protocolVersion = GetCoreMaxSupportedProtocol() } - i.expandedTemplatesDir = i.createExpandedTemplatesDir() - rpcCfg := i.getRPConfig(sqlLitePath) + i.rpcConfigFilesDir = i.t.TempDir() + if i.runRPCInContainer() { - i.rpcContainerConfigMountDir = i.createRPCContainerMountDir(rpcCfg) + // The container needs to use the sqlite mount point + i.rpcContainerSQLiteMountDir = filepath.Dir(sqlitePath) + i.generateCaptiveCoreCfgForContainer() + rpcCfg := i.getRPConfigForContainer(sqlitePath) + i.generateRPCConfigFile(rpcCfg) } + i.prepareShutdownHandlers() if i.runRPCInContainer() || !i.onlyRPC { // There are containerized workloads upCmd := []string{"up"} @@ -186,34 +167,48 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { upCmd = append(upCmd, "rpc") } upCmd = append(upCmd, "--detach", "--quiet-pull", "--no-color") - i.runComposeCommand(upCmd...) + i.runSuccessfulComposeCommand(upCmd...) if i.runRPCInContainer() { i.rpcContainerLogsCommand = i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") i.rpcContainerLogsCommand.Stdout = os.Stdout i.rpcContainerLogsCommand.Stderr = os.Stderr require.NoError(t, i.rpcContainerLogsCommand.Start()) } + i.fillContainerPorts() } - i.prepareShutdownHandlers() if !i.onlyRPC { i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(int(i.testPorts.CoreHTTPPort))} i.waitForCore() i.waitForCheckpoint() } if !i.runRPCInContainer() { + // We need to get a free port. Unfortunately this isn't completely clash-Free + // but there is no way to tell core to allocate the port dynamically + i.testPorts.captiveCorePort = getFreeTCPPort(i.t) + i.generateCaptiveCoreCfgForDaemon() + rpcCfg := i.getRPConfigForDaemon(sqlitePath) i.daemon = i.createDaemon(rpcCfg) + i.fillDaemonPorts() go i.daemon.Run() } - i.waitForRPC() + + i.rpcClient = NewClient(i.GetSorobanRPCURL(), nil) + if shouldWaitForRPC { + i.waitForRPC() + } return i } +func (i *Test) GetPorts() TestPorts { + return i.testPorts +} + func (i *Test) runRPCInContainer() bool { return i.rpcContainerVersion != "" } -func (i *Test) GetRPCLient() *jrpc2.Client { +func (i *Test) GetRPCLient() *Client { return i.rpcClient } func (i *Test) MasterKey() *keypair.Full { @@ -250,65 +245,67 @@ func (i *Test) waitForCheckpoint() { ) } -func (i *Test) getRPConfig(sqlitePath string) map[string]string { - if sqlitePath == "" { - sqlitePath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") - } - - // Container's default path to captive core - coreBinaryPath := "/usr/bin/stellar-core" - if !i.runRPCInContainer() { - coreBinaryPath = os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - if coreBinaryPath == "" { - i.t.Fatal("missing SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - } - } - - archiveURL := fmt.Sprintf("http://localhost:%d", i.testPorts.CoreArchivePort) - if i.runRPCInContainer() { - // the archive needs to be accessed from the container - // where core is Core's hostname - archiveURL = fmt.Sprintf("http://core:%d", i.testPorts.CoreArchivePort) - } - if i.historyArchiveURL != "" { - // an archive URL was supplied explicitly - archiveURL = i.historyArchiveURL - } - - captiveCoreConfigPath := path.Join(i.expandedTemplatesDir, "captive-core-integration-tests.cfg") - bindHost := "localhost" - stellarCoreURL := fmt.Sprintf("http://localhost:%d", i.testPorts.CoreHTTPPort) - if i.runRPCInContainer() { +func (i *Test) getRPConfigForContainer(sqlitePath string) rpcConfig { + return rpcConfig{ + // Container's default path to captive core + coreBinaryPath: "/usr/bin/stellar-core", + archiveURL: fmt.Sprintf("http://%s:%d", inContainerCoreHostname, inContainerCoreArchivePort), // The file will be inside the container - captiveCoreConfigPath = "/stellar-core.cfg" + captiveCoreConfigPath: "/stellar-core.cfg", // The container needs to listen on all interfaces, not just localhost - bindHost = "0.0.0.0" - // The container needs to use the sqlite mount point - i.rpcContainerSQLiteMountDir = filepath.Dir(sqlitePath) - sqlitePath = "/db/" + filepath.Base(sqlitePath) - stellarCoreURL = fmt.Sprintf("http://core:%d", i.testPorts.CoreHTTPPort) + // (otherwise it can't be accessible from the outside) + endPoint: fmt.Sprintf("0.0.0.0:%d", inContainerRPCPort), + adminEndpoint: fmt.Sprintf("0.0.0.0:%d", inContainerRPCAdminPort), + sqlitePath: "/db/" + filepath.Base(sqlitePath), + stellarCoreURL: fmt.Sprintf("http://%s:%d", inContainerCoreHostname, inContainerCoreHTTPPort), + captiveCoreStoragePath: "/tmp/captive-core", } +} - // in the container - captiveCoreStoragePath := "/tmp/captive-core" - if !i.runRPCInContainer() { - captiveCoreStoragePath = i.t.TempDir() +func (i *Test) getRPConfigForDaemon(sqlitePath string) rpcConfig { + coreBinaryPath := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") + if coreBinaryPath == "" { + i.t.Fatal("missing SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") } + return rpcConfig{ + coreBinaryPath: coreBinaryPath, + archiveURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreArchivePort), + captiveCoreConfigPath: path.Join(i.rpcConfigFilesDir, captiveCoreConfigFilename), + stellarCoreURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreHTTPPort), + // Allocate port dynamically and then figure out what the port is + endPoint: "localhost:0", + adminEndpoint: "localhost:0", + captiveCoreStoragePath: i.t.TempDir(), + sqlitePath: sqlitePath, + } +} + +type rpcConfig struct { + endPoint string + adminEndpoint string + stellarCoreURL string + coreBinaryPath string + captiveCoreConfigPath string + captiveCoreStoragePath string + archiveURL string + sqlitePath string +} +func (vars rpcConfig) toMap() map[string]string { return map[string]string{ - "ENDPOINT": fmt.Sprintf("%s:%d", bindHost, i.testPorts.RPCPort), - "ADMIN_ENDPOINT": fmt.Sprintf("%s:%d", bindHost, i.testPorts.RPCAdminPort), - "STELLAR_CORE_URL": stellarCoreURL, + "ENDPOINT": vars.endPoint, + "ADMIN_ENDPOINT": vars.adminEndpoint, + "STELLAR_CORE_URL": vars.stellarCoreURL, "CORE_REQUEST_TIMEOUT": "2s", - "STELLAR_CORE_BINARY_PATH": coreBinaryPath, - "CAPTIVE_CORE_CONFIG_PATH": captiveCoreConfigPath, - "CAPTIVE_CORE_STORAGE_PATH": captiveCoreStoragePath, + "STELLAR_CORE_BINARY_PATH": vars.coreBinaryPath, + "CAPTIVE_CORE_CONFIG_PATH": vars.captiveCoreConfigPath, + "CAPTIVE_CORE_STORAGE_PATH": vars.captiveCoreStoragePath, "STELLAR_CAPTIVE_CORE_HTTP_PORT": "0", "FRIENDBOT_URL": FriendbotURL, "NETWORK_PASSPHRASE": StandaloneNetworkPassphrase, - "HISTORY_ARCHIVE_URLS": archiveURL, + "HISTORY_ARCHIVE_URLS": vars.archiveURL, "LOG_LEVEL": "debug", - "DB_PATH": sqlitePath, + "DB_PATH": vars.sqlitePath, "INGESTION_TIMEOUT": "10m", "EVENT_LEDGER_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), "TRANSACTION_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), @@ -320,17 +317,9 @@ func (i *Test) getRPConfig(sqlitePath string) map[string]string { func (i *Test) waitForRPC() { i.t.Log("Waiting for RPC to be healthy...") - // This is needed because of https://github.com/creachadair/jrpc2/issues/118 - refreshClient := func() { - if i.rpcClient != nil { - i.rpcClient.Close() - } - ch := jhttp.NewChannel(i.GetSorobanRPCURL(), nil) - i.rpcClient = jrpc2.NewClient(ch, nil) - } + require.Eventually(i.t, func() bool { - refreshClient() result, err := i.GetRPCHealth() return err == nil && result.Status == "healthy" }, @@ -339,35 +328,7 @@ func (i *Test) waitForRPC() { ) } -func (i *Test) createExpandedTemplatesDir() string { - mountDir := i.t.TempDir() - configDir := filepath.Join(GetCurrentDirectory(), "docker") - entries, err := os.ReadDir(configDir) - require.NoError(i.t, err) - fmapping := i.testPorts.getFuncMapping() - for _, entry := range entries { - if !entry.Type().IsRegular() { - continue - } - if !strings.HasSuffix(entry.Name(), ".tmpl") { - continue - } - originalPath := filepath.Join(configDir, entry.Name()) - in, err := os.ReadFile(originalPath) - require.NoError(i.t, err) - out := os.Expand(string(in), fmapping) - targetPath := filepath.Join(mountDir, strings.TrimSuffix(entry.Name(), ".tmpl")) - info, err := entry.Info() - require.NoError(i.t, err) - err = os.WriteFile(targetPath, []byte(out), info.Mode()) - require.NoError(i.t, err) - } - return mountDir -} - -func (i *Test) createRPCContainerMountDir(rpcConfig map[string]string) string { - mountDir := i.t.TempDir() - +func (i *Test) generateCaptiveCoreCfgForContainer() { getOldVersionCaptiveCoreConfigVersion := func(dir string, filename string) ([]byte, error) { cmd := exec.Command("git", "show", fmt.Sprintf("v%s:./%s/%s", i.rpcContainerVersion, dir, filename)) cmd.Dir = GetCurrentDirectory() @@ -375,40 +336,58 @@ func (i *Test) createRPCContainerMountDir(rpcConfig map[string]string) string { } // Get old version of captive-core-integration-tests.cfg.tmpl - out, err := getOldVersionCaptiveCoreConfigVersion("docker", "captive-core-integration-tests.cfg.tmpl") + out, err := getOldVersionCaptiveCoreConfigVersion("docker", captiveCoreConfigTemplateFilename) if err != nil { // Try the directory before the integration test refactoring // TODO: remove this hack after protocol 22 is released - out, err = getOldVersionCaptiveCoreConfigVersion("../../test", "captive-core-integration-tests.cfg") - outStr := strings.Replace(string(out), `ADDRESS="localhost"`, fmt.Sprintf(`ADDRESS="localhost:%d"`, i.testPorts.CorePort), -1) + out, err = getOldVersionCaptiveCoreConfigVersion("../../test", captiveCoreConfigFilename) + outStr := strings.Replace(string(out), `ADDRESS="localhost"`, `ADDRESS="${CORE_HOST_PORT}"`, -1) out = []byte(outStr) } require.NoError(i.t, err) + i.generateCaptiveCoreCfg(out, inContainerCaptiveCorePort, inContainerCoreHostname) +} +func (i *Test) generateCaptiveCoreCfg(tmplContents []byte, captiveCorePort uint16, coreHostPort string) { // Apply expansion - captiveCoreCfgContents := os.Expand(string(out), i.testPorts.getFuncMapping()) + mapping := func(in string) string { + switch in { + case "CAPTIVE_CORE_PORT": + // any non-conflicting port would do + return strconv.Itoa(int(captiveCorePort)) + case "CORE_HOST_PORT": + return coreHostPort + default: + // Try to leave it as it was + return "$" + in + } + } + + captiveCoreCfgContents := os.Expand(string(tmplContents), mapping) + err := os.WriteFile(filepath.Join(i.rpcConfigFilesDir, captiveCoreConfigFilename), []byte(captiveCoreCfgContents), 0666) + require.NoError(i.t, err) +} - // TODO: maybe it would be better to not place localhost in the file (and use a host replacement) - // replace ADDRESS="localhost by ADDRESS="core, so that the container can find core - captiveCoreCfgContents = strings.Replace(captiveCoreCfgContents, `ADDRESS="localhost`, `ADDRESS="core`, -1) - err = os.WriteFile(filepath.Join(mountDir, "stellar-core-integration-tests.cfg"), []byte(captiveCoreCfgContents), 0666) +func (i *Test) generateCaptiveCoreCfgForDaemon() { + out, err := os.ReadFile(filepath.Join(GetCurrentDirectory(), "docker", captiveCoreConfigTemplateFilename)) require.NoError(i.t, err) + i.generateCaptiveCoreCfg(out, i.testPorts.captiveCorePort, "localhost:"+strconv.Itoa(int(i.testPorts.CorePort))) +} - // Generate config file +func (i *Test) generateRPCConfigFile(rpcConfig rpcConfig) { cfgFileContents := "" - for k, v := range rpcConfig { + for k, v := range rpcConfig.toMap() { cfgFileContents += fmt.Sprintf("%s=%q\n", k, v) } - err = os.WriteFile(filepath.Join(mountDir, "soroban-rpc.config"), []byte(cfgFileContents), 0666) + err := os.WriteFile(filepath.Join(i.rpcConfigFilesDir, "soroban-rpc.config"), []byte(cfgFileContents), 0666) require.NoError(i.t, err) - - return mountDir } -func (i *Test) createDaemon(env map[string]string) *daemon.Daemon { +func (i *Test) createDaemon(c rpcConfig) *daemon.Daemon { var cfg config.Config + m := c.toMap() lookup := func(s string) (string, bool) { - ret, ok := env[s] + ret, ok := m[s] return ret, ok } require.NoError(i.t, cfg.SetValues(lookup)) @@ -425,24 +404,18 @@ func (i *Test) getComposeProjectName() string { } func (i *Test) getComposeCommand(args ...string) *exec.Cmd { - integrationYaml := filepath.Join(GetCurrentDirectory(), "docker", "docker-compose.yml") - configFiles := []string{"-f", integrationYaml} + composeFile := "docker-compose.yml" if i.runRPCInContainer() { - rpcYaml := filepath.Join(GetCurrentDirectory(), "docker", "docker-compose.rpc.yml") - configFiles = append(configFiles, "-f", rpcYaml) + composeFile = "docker-compose.rpc.yml" } + fullComposeFilePath := filepath.Join(GetCurrentDirectory(), "docker", composeFile) + cmdline := []string{"-f", fullComposeFilePath} // Use separate projects to run them in parallel projectName := i.getComposeProjectName() - cmdline := append([]string{"-p", projectName}, configFiles...) + cmdline = append([]string{"-p", projectName}, cmdline...) cmdline = append(cmdline, args...) cmd := exec.Command("docker-compose", cmdline...) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, - "CORE_MOUNT_DIR="+i.expandedTemplatesDir, - ) - cmd.Env = append(cmd.Env, i.testPorts.getEnvs()...) - if img := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_DOCKER_IMG"); img != "" { cmd.Env = append( cmd.Env, @@ -454,28 +427,35 @@ func (i *Test) getComposeCommand(args ...string) *exec.Cmd { cmd.Env = append( cmd.Env, "RPC_IMAGE_TAG="+i.rpcContainerVersion, - "RPC_CONFIG_MOUNT_DIR="+i.rpcContainerConfigMountDir, + "RPC_CONFIG_MOUNT_DIR="+i.rpcConfigFilesDir, "RPC_SQLITE_MOUNT_DIR="+i.rpcContainerSQLiteMountDir, "RPC_UID="+strconv.Itoa(os.Getuid()), "RPC_GID="+strconv.Itoa(os.Getgid()), ) } + if cmd.Env != nil { + cmd.Env = append(os.Environ(), cmd.Env...) + } return cmd } -func (i *Test) runComposeCommand(args ...string) { +func (i *Test) runComposeCommand(args ...string) ([]byte, error) { cmd := i.getComposeCommand(args...) - i.t.Log("Running", cmd.Args) - out, innerErr := cmd.Output() - if exitErr, ok := innerErr.(*exec.ExitError); ok { - i.t.Log("stdout\n:", string(out)) - i.t.Log("stderr:\n", string(exitErr.Stderr)) - } + return cmd.Output() +} - if innerErr != nil { - i.t.Fatalf("Compose command failed: %v", innerErr) +func (i *Test) runSuccessfulComposeCommand(args ...string) []byte { + out, err := i.runComposeCommand(args...) + if err != nil { + i.t.Log("Compose command failed, args:", args) } + if exitErr, ok := err.(*exec.ExitError); ok { + i.t.Log("stdout:\n", string(out)) + i.t.Log("stderr:\n", string(exitErr.Stderr)) + } + require.NoError(i.t, err) + return out } func (i *Test) prepareShutdownHandlers() { @@ -496,7 +476,7 @@ func (i *Test) prepareShutdownHandlers() { downCmd = append(downCmd, "rpc") } downCmd = append(downCmd, "-v") - i.runComposeCommand(downCmd...) + i.runSuccessfulComposeCommand(downCmd...) } if i.rpcContainerLogsCommand != nil { i.rpcContainerLogsCommand.Wait() @@ -576,7 +556,7 @@ func (i *Test) StopRPC() { i.daemon = nil } if i.runRPCInContainer() { - i.runComposeCommand("down", "rpc", "-v") + i.runSuccessfulComposeCommand("down", "rpc", "-v") } } @@ -647,6 +627,46 @@ func (i *Test) GetRPCHealth() (methods.HealthCheckResult, error) { return result, err } +func (i *Test) fillContainerPorts() { + getPublicPort := func(service string, privatePort int) uint16 { + var port uint16 + // We need to try several times because we detached from `docker-compose up` + // and the container may not be ready + require.Eventually(i.t, + func() bool { + out, err := i.runComposeCommand("port", service, strconv.Itoa(privatePort)) + if err != nil { + return false + } + _, strPort, err := net.SplitHostPort(strings.TrimSpace(string(out))) + require.NoError(i.t, err) + intPort, err := strconv.Atoi(strPort) + require.NoError(i.t, err) + port = uint16(intPort) + return true + }, + 2*time.Second, + 100*time.Millisecond, + ) + return port + } + i.testPorts.CorePort = getPublicPort("core", inContainerCorePort) + i.testPorts.CoreHTTPPort = getPublicPort("core", inContainerCoreHTTPPort) + i.testPorts.CoreArchivePort = getPublicPort("core", inContainerCoreArchivePort) + if i.runRPCInContainer() { + i.testPorts.RPCPort = getPublicPort("rpc", inContainerRPCPort) + i.testPorts.RPCAdminPort = getPublicPort("rpc", inContainerRPCAdminPort) + } +} + +func (i *Test) fillDaemonPorts() { + endpointAddr, adminEndpointAddr := i.daemon.GetEndpointAddrs() + i.testPorts.RPCPort = uint16(endpointAddr.Port) + if adminEndpointAddr != nil { + i.testPorts.RPCAdminPort = uint16(adminEndpointAddr.Port) + } +} + func GetCoreMaxSupportedProtocol() uint32 { str := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL") if str == "" { diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go index f98b3c77..3cd791b3 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go @@ -1,9 +1,11 @@ package infrastructure import ( + "fmt" "net" "path/filepath" "runtime" + "time" "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/require" @@ -37,3 +39,14 @@ func CreateTransactionParams(account txnbuild.Account, op txnbuild.Operation) tx }, } } + +func isLocalTCPPortOpen(port uint16) bool { + host := fmt.Sprintf("localhost:%d", port) + timeout := time.Second + conn, err := net.DialTimeout("tcp", host, timeout) + if err != nil { + return false + } + conn.Close() + return true +} diff --git a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go index b401b545..b6c360a3 100644 --- a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go @@ -38,26 +38,25 @@ func TestMigrate(t *testing.T) { } func testMigrateFromVersion(t *testing.T, version string) { - originalPorts := infrastructure.NewTestPorts(t) sqliteFile := filepath.Join(t.TempDir(), "soroban-rpc.db") test := infrastructure.NewTest(t, &infrastructure.TestConfig{ UseReleasedRPCVersion: version, UseSQLitePath: sqliteFile, - TestPorts: &originalPorts, }) // Submit an event-logging transaction in the version to migrate from submitTransactionResponse, _ := test.UploadHelloWorldContract() - // Replace RPC with the current version, but keeping the previous network and sql database (causing a data migration if needed) + // Replace RPC with the current version, but keeping the previous network and sql database (causing any data migrations) // We need to do some wiring to plug RPC into the prior network test.StopRPC() - freshPorts := infrastructure.NewTestPorts(t) - ports := originalPorts - ports.RPCPort = freshPorts.RPCPort + corePorts := test.GetPorts().TestCorePorts test = infrastructure.NewTest(t, &infrastructure.TestConfig{ - TestPorts: &ports, - OnlyRPC: true, + // We don't want to run Core again + OnlyRPC: &infrastructure.TestOnlyRPCConfig{ + CorePorts: corePorts, + DontWait: false, + }, UseSQLitePath: sqliteFile, // We don't want to mark the test as parallel twice since it causes a panic NoParallel: true, diff --git a/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go index 4429bfca..380c9307 100644 --- a/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/creachadair/jrpc2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -490,7 +489,7 @@ func getCounterLedgerKey(contractID [32]byte) xdr.LedgerKey { return key } -func waitUntilLedgerEntryTTL(t *testing.T, client *jrpc2.Client, ledgerKey xdr.LedgerKey) { +func waitUntilLedgerEntryTTL(t *testing.T, client *infrastructure.Client, ledgerKey xdr.LedgerKey) { keyB64, err := xdr.MarshalBase64(ledgerKey) require.NoError(t, err) request := methods.GetLedgerEntriesRequest{ From 92ec9c67035cb1e7a99b08fffdb54fe1f5071c4a Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Sun, 16 Jun 2024 21:37:54 +0200 Subject: [PATCH 08/13] Use proper logging in tests --- cmd/soroban-rpc/internal/daemon/daemon.go | 3 +-- .../integrationtest/infrastructure/test.go | 25 ++++++++++++++++--- .../integrationtest/transaction_test.go | 3 +-- cmd/soroban-rpc/main.go | 3 ++- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index 732cd500..184fa353 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -145,8 +145,7 @@ func newCaptiveCore(cfg *config.Config, logger *supportlog.Entry) (*ledgerbacken } -func MustNew(cfg *config.Config) *Daemon { - logger := supportlog.New() +func MustNew(cfg *config.Config, logger *supportlog.Entry) *Daemon { logger.SetLevel(cfg.LogLevel) if cfg.LogFormat == config.LogFormatJSON { logger.UseJSONFormatter() diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go index 0c7191c6..a4ab624e 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -21,6 +21,7 @@ import ( "github.com/stellar/go/clients/stellarcore" "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/stellarcore" + supportlog "github.com/stellar/go/support/log" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" @@ -170,8 +171,9 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { i.runSuccessfulComposeCommand(upCmd...) if i.runRPCInContainer() { i.rpcContainerLogsCommand = i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") - i.rpcContainerLogsCommand.Stdout = os.Stdout - i.rpcContainerLogsCommand.Stderr = os.Stderr + writer := testLogWriter{t: t, prefix: fmt.Sprintf(`rpc="container" version="%s" `, i.rpcContainerVersion)} + i.rpcContainerLogsCommand.Stdout = writer + i.rpcContainerLogsCommand.Stderr = writer require.NoError(t, i.rpcContainerLogsCommand.Start()) } i.fillContainerPorts() @@ -383,6 +385,20 @@ func (i *Test) generateRPCConfigFile(rpcConfig rpcConfig) { require.NoError(i.t, err) } +type testLogWriter struct { + t *testing.T + prefix string +} + +func (tw testLogWriter) Write(p []byte) (n int, err error) { + all := strings.TrimSpace(string(p)) + lines := strings.Split(all, "\n") + for _, l := range lines { + tw.t.Log(tw.prefix + l) + } + return len(p), nil +} + func (i *Test) createDaemon(c rpcConfig) *daemon.Daemon { var cfg config.Config m := c.toMap() @@ -393,7 +409,10 @@ func (i *Test) createDaemon(c rpcConfig) *daemon.Daemon { require.NoError(i.t, cfg.SetValues(lookup)) require.NoError(i.t, cfg.Validate()) cfg.HistoryArchiveUserAgent = fmt.Sprintf("soroban-rpc/%s", config.Version) - return daemon.MustNew(&cfg) + + logger := supportlog.New() + logger.SetOutput(testLogWriter{t: i.t, prefix: `rpc="daemon" `}) + return daemon.MustNew(&cfg, logger) } var nonAlphanumericRegex = regexp.MustCompile("[^a-zA-Z0-9]+") diff --git a/cmd/soroban-rpc/internal/integrationtest/transaction_test.go b/cmd/soroban-rpc/internal/integrationtest/transaction_test.go index f0d6426c..ddda9f68 100644 --- a/cmd/soroban-rpc/internal/integrationtest/transaction_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/transaction_test.go @@ -2,7 +2,6 @@ package integrationtest import ( "context" - "fmt" "testing" "github.com/creachadair/jrpc2" @@ -175,7 +174,7 @@ func TestSendTransactionFailedInLedger(t *testing.T) { var txResult xdr.TransactionResult err := xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &txResult) assert.NoError(t, err) - fmt.Printf("error: %#v\n", txResult) + t.Logf("error: %#v\n", txResult) } assert.NotZero(t, result.LatestLedger) assert.NotZero(t, result.LatestLedgerCloseTime) diff --git a/cmd/soroban-rpc/main.go b/cmd/soroban-rpc/main.go index 48698223..ffa61d71 100644 --- a/cmd/soroban-rpc/main.go +++ b/cmd/soroban-rpc/main.go @@ -5,6 +5,7 @@ import ( "os" "github.com/spf13/cobra" + supportlog "github.com/stellar/go/support/log" goxdr "github.com/stellar/go/xdr" "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" @@ -27,7 +28,7 @@ func main() { os.Exit(1) } cfg.HistoryArchiveUserAgent = fmt.Sprintf("soroban-rpc/%s", config.Version) - daemon.MustNew(&cfg).Run() + daemon.MustNew(&cfg, supportlog.New()).Run() }, } From 07809ffa0c9ea4d738626a6eb00e2059aba38994 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Mon, 17 Jun 2024 02:10:38 +0200 Subject: [PATCH 09/13] Make code a bit more readable --- .../integrationtest/infrastructure/test.go | 161 ++++++++++-------- .../internal/integrationtest/migrate_test.go | 4 +- 2 files changed, 91 insertions(+), 74 deletions(-) diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go index a4ab624e..820fae41 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -65,9 +65,9 @@ type TestConfig struct { ProtocolVersion uint32 // Run a previously released version of RPC (in a container) instead of the current version UseReleasedRPCVersion string - // Use/Reuse a SQLite file instead of creating it from scratch - UseSQLitePath string - OnlyRPC *TestOnlyRPCConfig + // Use/Reuse a SQLite file path + SQLitePath string + OnlyRPC *TestOnlyRPCConfig // Do not mark the test as running in parallel NoParallel bool } @@ -95,6 +95,8 @@ type Test struct { rpcConfigFilesDir string + sqlitePath string + rpcContainerVersion string rpcContainerSQLiteMountDir string rpcContainerLogsCommand *exec.Cmd @@ -122,12 +124,11 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { } parallel := true - sqlitePath := "" shouldWaitForRPC := true if cfg != nil { i.rpcContainerVersion = cfg.UseReleasedRPCVersion i.protocolVersion = cfg.ProtocolVersion - sqlitePath = cfg.UseSQLitePath + i.sqlitePath = cfg.SQLitePath if cfg.OnlyRPC != nil { i.onlyRPC = true i.testPorts.TestCorePorts = cfg.OnlyRPC.CorePorts @@ -135,16 +136,15 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { } parallel = !cfg.NoParallel } - if sqlitePath == "" { - sqlitePath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") + + if i.sqlitePath == "" { + i.sqlitePath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") } if parallel { t.Parallel() } - // TODO: this function is pretty unreadable - if i.protocolVersion == 0 { // Default to the maximum supported protocol version i.protocolVersion = GetCoreMaxSupportedProtocol() @@ -152,31 +152,9 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { i.rpcConfigFilesDir = i.t.TempDir() - if i.runRPCInContainer() { - // The container needs to use the sqlite mount point - i.rpcContainerSQLiteMountDir = filepath.Dir(sqlitePath) - i.generateCaptiveCoreCfgForContainer() - rpcCfg := i.getRPConfigForContainer(sqlitePath) - i.generateRPCConfigFile(rpcCfg) - } - i.prepareShutdownHandlers() - if i.runRPCInContainer() || !i.onlyRPC { - // There are containerized workloads - upCmd := []string{"up"} - if i.runRPCInContainer() && i.onlyRPC { - upCmd = append(upCmd, "rpc") - } - upCmd = append(upCmd, "--detach", "--quiet-pull", "--no-color") - i.runSuccessfulComposeCommand(upCmd...) - if i.runRPCInContainer() { - i.rpcContainerLogsCommand = i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") - writer := testLogWriter{t: t, prefix: fmt.Sprintf(`rpc="container" version="%s" `, i.rpcContainerVersion)} - i.rpcContainerLogsCommand.Stdout = writer - i.rpcContainerLogsCommand.Stderr = writer - require.NoError(t, i.rpcContainerLogsCommand.Start()) - } - i.fillContainerPorts() + if i.areThereContainers() { + i.spawnContainers() } if !i.onlyRPC { i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(int(i.testPorts.CoreHTTPPort))} @@ -184,14 +162,7 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { i.waitForCheckpoint() } if !i.runRPCInContainer() { - // We need to get a free port. Unfortunately this isn't completely clash-Free - // but there is no way to tell core to allocate the port dynamically - i.testPorts.captiveCorePort = getFreeTCPPort(i.t) - i.generateCaptiveCoreCfgForDaemon() - rpcCfg := i.getRPConfigForDaemon(sqlitePath) - i.daemon = i.createDaemon(rpcCfg) - i.fillDaemonPorts() - go i.daemon.Run() + i.spawnRPCDaemon() } i.rpcClient = NewClient(i.GetSorobanRPCURL(), nil) @@ -202,6 +173,46 @@ func NewTest(t *testing.T, cfg *TestConfig) *Test { return i } +func (i *Test) areThereContainers() bool { + return i.runRPCInContainer() || !i.onlyRPC +} + +func (i *Test) spawnContainers() { + if i.runRPCInContainer() { + // The container needs to use the sqlite mount point + i.rpcContainerSQLiteMountDir = filepath.Dir(i.sqlitePath) + i.generateCaptiveCoreCfgForContainer() + rpcCfg := i.getRPConfigForContainer() + i.generateRPCConfigFile(rpcCfg) + } + // There are containerized workloads + upCmd := []string{"up"} + if i.runRPCInContainer() && i.onlyRPC { + upCmd = append(upCmd, "rpc") + } + upCmd = append(upCmd, "--detach", "--quiet-pull", "--no-color") + i.runSuccessfulComposeCommand(upCmd...) + if i.runRPCInContainer() { + i.rpcContainerLogsCommand = i.getComposeCommand("logs", "--no-log-prefix", "-f", "rpc") + writer := testLogWriter{t: i.t, prefix: fmt.Sprintf(`rpc="container" version="%s" `, i.rpcContainerVersion)} + i.rpcContainerLogsCommand.Stdout = writer + i.rpcContainerLogsCommand.Stderr = writer + require.NoError(i.t, i.rpcContainerLogsCommand.Start()) + } + i.fillContainerPorts() +} + +func (i *Test) stopContainers() { + // There were containerized workloads we should bring down + downCmd := []string{"down"} + if i.runRPCInContainer() && i.onlyRPC { + downCmd = append(downCmd, "rpc") + } + downCmd = append(downCmd, "-v") + i.runSuccessfulComposeCommand(downCmd...) + +} + func (i *Test) GetPorts() TestPorts { return i.testPorts } @@ -247,38 +258,39 @@ func (i *Test) waitForCheckpoint() { ) } -func (i *Test) getRPConfigForContainer(sqlitePath string) rpcConfig { +func (i *Test) getRPConfigForContainer() rpcConfig { return rpcConfig{ + // The container needs to listen on all interfaces, not just localhost + // (otherwise it can't be accessible from the outside) + endPoint: fmt.Sprintf("0.0.0.0:%d", inContainerRPCPort), + adminEndpoint: fmt.Sprintf("0.0.0.0:%d", inContainerRPCAdminPort), + stellarCoreURL: fmt.Sprintf("http://%s:%d", inContainerCoreHostname, inContainerCoreHTTPPort), // Container's default path to captive core coreBinaryPath: "/usr/bin/stellar-core", - archiveURL: fmt.Sprintf("http://%s:%d", inContainerCoreHostname, inContainerCoreArchivePort), // The file will be inside the container captiveCoreConfigPath: "/stellar-core.cfg", - // The container needs to listen on all interfaces, not just localhost - // (otherwise it can't be accessible from the outside) - endPoint: fmt.Sprintf("0.0.0.0:%d", inContainerRPCPort), - adminEndpoint: fmt.Sprintf("0.0.0.0:%d", inContainerRPCAdminPort), - sqlitePath: "/db/" + filepath.Base(sqlitePath), - stellarCoreURL: fmt.Sprintf("http://%s:%d", inContainerCoreHostname, inContainerCoreHTTPPort), + // Any writable directory would do captiveCoreStoragePath: "/tmp/captive-core", + archiveURL: fmt.Sprintf("http://%s:%d", inContainerCoreHostname, inContainerCoreArchivePort), + sqlitePath: "/db/" + filepath.Base(i.sqlitePath), } } -func (i *Test) getRPConfigForDaemon(sqlitePath string) rpcConfig { +func (i *Test) getRPConfigForDaemon() rpcConfig { coreBinaryPath := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") if coreBinaryPath == "" { i.t.Fatal("missing SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") } return rpcConfig{ - coreBinaryPath: coreBinaryPath, - archiveURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreArchivePort), - captiveCoreConfigPath: path.Join(i.rpcConfigFilesDir, captiveCoreConfigFilename), - stellarCoreURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreHTTPPort), // Allocate port dynamically and then figure out what the port is endPoint: "localhost:0", adminEndpoint: "localhost:0", + stellarCoreURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreHTTPPort), + coreBinaryPath: coreBinaryPath, + captiveCoreConfigPath: path.Join(i.rpcConfigFilesDir, captiveCoreConfigFilename), captiveCoreStoragePath: i.t.TempDir(), - sqlitePath: sqlitePath, + archiveURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreArchivePort), + sqlitePath: i.sqlitePath, } } @@ -399,7 +411,7 @@ func (tw testLogWriter) Write(p []byte) (n int, err error) { return len(p), nil } -func (i *Test) createDaemon(c rpcConfig) *daemon.Daemon { +func (i *Test) createRPCDaemon(c rpcConfig) *daemon.Daemon { var cfg config.Config m := c.toMap() lookup := func(s string) (string, bool) { @@ -415,6 +427,25 @@ func (i *Test) createDaemon(c rpcConfig) *daemon.Daemon { return daemon.MustNew(&cfg, logger) } +func (i *Test) fillRPCDaemonPorts() { + endpointAddr, adminEndpointAddr := i.daemon.GetEndpointAddrs() + i.testPorts.RPCPort = uint16(endpointAddr.Port) + if adminEndpointAddr != nil { + i.testPorts.RPCAdminPort = uint16(adminEndpointAddr.Port) + } +} + +func (i *Test) spawnRPCDaemon() { + // We need to get a free port. Unfortunately this isn't completely clash-Free + // but there is no way to tell core to allocate the port dynamically + i.testPorts.captiveCorePort = getFreeTCPPort(i.t) + i.generateCaptiveCoreCfgForDaemon() + rpcCfg := i.getRPConfigForDaemon() + i.daemon = i.createRPCDaemon(rpcCfg) + i.fillRPCDaemonPorts() + go i.daemon.Run() +} + var nonAlphanumericRegex = regexp.MustCompile("[^a-zA-Z0-9]+") func (i *Test) getComposeProjectName() string { @@ -488,14 +519,8 @@ func (i *Test) prepareShutdownHandlers() { if i.rpcClient != nil { i.rpcClient.Close() } - if i.runRPCInContainer() || !i.onlyRPC { - // There were containerized workloads we should bring down - downCmd := []string{"down"} - if i.runRPCInContainer() && i.onlyRPC { - downCmd = append(downCmd, "rpc") - } - downCmd = append(downCmd, "-v") - i.runSuccessfulComposeCommand(downCmd...) + if i.areThereContainers() { + i.stopContainers() } if i.rpcContainerLogsCommand != nil { i.rpcContainerLogsCommand.Wait() @@ -678,14 +703,6 @@ func (i *Test) fillContainerPorts() { } } -func (i *Test) fillDaemonPorts() { - endpointAddr, adminEndpointAddr := i.daemon.GetEndpointAddrs() - i.testPorts.RPCPort = uint16(endpointAddr.Port) - if adminEndpointAddr != nil { - i.testPorts.RPCAdminPort = uint16(adminEndpointAddr.Port) - } -} - func GetCoreMaxSupportedProtocol() uint32 { str := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL") if str == "" { diff --git a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go index b6c360a3..dd3c9b9c 100644 --- a/cmd/soroban-rpc/internal/integrationtest/migrate_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/migrate_test.go @@ -41,7 +41,7 @@ func testMigrateFromVersion(t *testing.T, version string) { sqliteFile := filepath.Join(t.TempDir(), "soroban-rpc.db") test := infrastructure.NewTest(t, &infrastructure.TestConfig{ UseReleasedRPCVersion: version, - UseSQLitePath: sqliteFile, + SQLitePath: sqliteFile, }) // Submit an event-logging transaction in the version to migrate from @@ -57,7 +57,7 @@ func testMigrateFromVersion(t *testing.T, version string) { CorePorts: corePorts, DontWait: false, }, - UseSQLitePath: sqliteFile, + SQLitePath: sqliteFile, // We don't want to mark the test as parallel twice since it causes a panic NoParallel: true, }) From 8aed4c68404a1c9383bbeb4ba8a2785f96064fa9 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 18 Jun 2024 01:28:39 +0200 Subject: [PATCH 10/13] Bump stellar/go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d3cbb18a..5317d150 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stellar/go v0.0.0-20240603200326-f03edd0aab37 + github.com/stellar/go v0.0.0-20240617183518-100dc4fa6043 github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index 72aff604..2a6e1022 100644 --- a/go.sum +++ b/go.sum @@ -340,8 +340,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= -github.com/stellar/go v0.0.0-20240603200326-f03edd0aab37 h1:JczaIDb2jjnG+ZxLpByCN+gTfVVYuJkTve+eLW4qdI0= -github.com/stellar/go v0.0.0-20240603200326-f03edd0aab37/go.mod h1:TuXKLL7WViqwrvpWno2I4UYGn2Ny9KZld1jUIN6fnK8= +github.com/stellar/go v0.0.0-20240617183518-100dc4fa6043 h1:5UQzsvt9VtD3ijpzPtdW0/lXWCNgDs6GzmLUE8ZuWfk= +github.com/stellar/go v0.0.0-20240617183518-100dc4fa6043/go.mod h1:TuXKLL7WViqwrvpWno2I4UYGn2Ny9KZld1jUIN6fnK8= github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE= github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 62d9b72b235874230aa7c8335d325d6a8d952d20 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 18 Jun 2024 01:43:16 +0200 Subject: [PATCH 11/13] Use logger in archive pool --- cmd/soroban-rpc/internal/daemon/daemon.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index 184fa353..da56f70e 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -168,6 +168,7 @@ func MustNew(cfg *config.Config, logger *supportlog.Entry) *Daemon { historyArchive, err := historyarchive.NewArchivePool( cfg.HistoryArchiveURLs, historyarchive.ArchiveOptions{ + Logger: logger, NetworkPassphrase: cfg.NetworkPassphrase, CheckpointFrequency: cfg.CheckpointFrequency, ConnectOptions: storage.ConnectOptions{ From 562172cb0dd50c9809282ef3cde3e300891304bf Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 18 Jun 2024 03:21:51 +0200 Subject: [PATCH 12/13] Update cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go Co-authored-by: George --- .../internal/integrationtest/infrastructure/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go index d3e5b48b..6304b236 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go @@ -144,7 +144,7 @@ func SimulateTransactionFromTxParams(t *testing.T, client *Client, params txnbui func PreflightTransactionParamsLocally(t *testing.T, params txnbuild.TransactionParams, response methods.SimulateTransactionResponse) txnbuild.TransactionParams { if !assert.Empty(t, response.Error) { - fmt.Println(response.Error) + t.Log(response.Error) } var transactionData xdr.SorobanTransactionData err := xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) From b3b2adc88f969fcbc9af6d3a2f76e46fd35afea5 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 18 Jun 2024 03:26:19 +0200 Subject: [PATCH 13/13] Remove unneded import --- .../internal/integrationtest/infrastructure/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go index 6304b236..4dfdedc0 100644 --- a/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go @@ -2,7 +2,6 @@ package infrastructure import ( "context" - "fmt" "testing" "time"