diff --git a/.github/workflows/soroban-rpc.yml b/.github/workflows/soroban-rpc.yml index b2a962cc..03d6c336 100644 --- a/.github/workflows/soroban-rpc.yml +++ b/.github/workflows/soroban-rpc.yml @@ -183,4 +183,4 @@ jobs: - name: Run Soroban RPC Integration Tests run: | - go test -race -timeout 60m -v ./cmd/soroban-rpc/internal/test/... + go test -race -timeout 20m ./cmd/soroban-rpc/internal/integrationtest/... diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index 39685927..da56f70e 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() @@ -133,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() @@ -157,6 +168,7 @@ func MustNew(cfg *config.Config) *Daemon { historyArchive, err := historyarchive.NewArchivePool( cfg.HistoryArchiveURLs, historyarchive.ArchiveOptions{ + Logger: logger, NetworkPassphrase: cfg.NetworkPassphrase, CheckpointFrequency: cfg.CheckpointFrequency, ConnectOptions: storage.ConnectOptions{ @@ -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 new file mode 100644 index 00000000..57179284 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/archive_test.go @@ -0,0 +1,58 @@ +package integrationtest + +import ( + "net" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/integrationtest/infrastructure" +) + +func TestArchiveUserAgent(t *testing.T) { + userAgents := sync.Map{} + 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 historyArchive.Close() + historyPort := historyArchive.Listener.Addr().(*net.TCPAddr).Port + + cfg := &infrastructure.TestConfig{ + OnlyRPC: &infrastructure.TestOnlyRPCConfig{ + CorePorts: infrastructure.TestCorePorts{ + CoreArchivePort: uint16(historyPort), + }, + DontWait: true, + }, + } + + infrastructure.NewTest(t, cfg) + + 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/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 82% rename from cmd/soroban-rpc/internal/test/get_transactions_test.go rename to cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go index 48371cea..6af5954f 100644 --- a/cmd/soroban-rpc/internal/test/get_transactions_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/get_transactions_test.go @@ -1,14 +1,14 @@ -package test +package integrationtest import ( "context" "testing" - "github.com/creachadair/jrpc2" "github.com/stellar/go/keypair" "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 +18,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. @@ -38,8 +32,8 @@ 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 { - kp := keypair.Root(StandaloneNetworkPassphrase) +func sendTransactions(t *testing.T, client *infrastructure.Client) []uint32 { + kp := keypair.Root(infrastructure.StandaloneNetworkPassphrase) address := kp.Address() var ledgers []uint32 @@ -48,18 +42,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/integrationtest/get_version_info_test.go b/cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go new file mode 100644 index 00000000..b1bdb858 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/get_version_info_test.go @@ -0,0 +1,33 @@ +package integrationtest + +import ( + "context" + "testing" + + "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" + + "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) + + var result methods.GetVersionInfoResponse + err := test.GetRPCLient().CallResult(context.Background(), "getVersionInfo", nil, &result) + assert.NoError(t, err) + + 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) +} 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..4dfdedc0 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/client.go @@ -0,0 +1,195 @@ +package infrastructure + +import ( + "context" + "testing" + "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" + "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" +) + +// 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} + 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 *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 *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) { + t.Log(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 *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..4daa1427 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/contract.go @@ -0,0 +1,117 @@ +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 { + contractFile := path.Join(GetCurrentDirectory(), "../../../../../wasms/test_hello_world.wasm") + 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.tmpl similarity index 61% 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.tmpl index a772012a..670010c7 100644 --- a/cmd/soroban-rpc/internal/test/captive-core-integration-tests.cfg +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/captive-core-integration-tests.cfg.tmpl @@ -1,4 +1,8 @@ -PEER_PORT=11725 +# 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" + +# 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/test/core-start.sh b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh similarity index 76% rename from cmd/soroban-rpc/internal/test/core-start.sh rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh index 9dd89ba6..550ad1eb 100755 --- a/cmd/soroban-rpc/internal/test/core-start.sh +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/core-start.sh @@ -12,8 +12,11 @@ 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 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 64% rename from cmd/soroban-rpc/internal/test/docker-compose.rpc.yml rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.rpc.yml index 3443aff4..6a0992d4 100644 --- a/cmd/soroban-rpc/internal/test/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: - - "8000:8000" - - "8080:8080" + 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/test/docker-compose.yml b/cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml similarity index 80% rename from cmd/soroban-rpc/internal/test/docker-compose.yml rename to cmd/soroban-rpc/internal/integrationtest/infrastructure/docker/docker-compose.yml index cf3e7b0d..579cf67b 100644 --- a/cmd/soroban-rpc/internal/test/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,14 +16,15 @@ 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: - - "11625:11625" - - "11626:11626" - # add extra port for history archive server - - "1570:1570" + 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: 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/integrationtest/infrastructure/test.go b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go new file mode 100644 index 00000000..820fae41 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/test.go @@ -0,0 +1,717 @@ +package infrastructure + +import ( + "context" + "crypto/sha256" + "fmt" + "net" + "os" + "os/exec" + "os/signal" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "syscall" + "testing" + "time" + + "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" + "github.com/stretchr/testify/require" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" + + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" + "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon" +) + +const ( + StandaloneNetworkPassphrase = "Standalone Network ; February 2017" + MaxSupportedProtocolVersion = 21 + FriendbotURL = "http://localhost:8000/friendbot" + // Needed when Core is run with ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true + 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 + // Use/Reuse a SQLite file path + SQLitePath string + OnlyRPC *TestOnlyRPCConfig + // Do not mark the test as running in parallel + NoParallel bool +} + +type TestCorePorts struct { + CorePort uint16 + CoreHTTPPort uint16 + CoreArchivePort uint16 + // This only needs to be an unconflicting port + captiveCorePort uint16 +} + +type TestPorts struct { + RPCPort uint16 + RPCAdminPort uint16 + TestCorePorts +} + +type Test struct { + t *testing.T + + testPorts TestPorts + + protocolVersion uint32 + + rpcConfigFilesDir string + + sqlitePath string + + rpcContainerVersion string + rpcContainerSQLiteMountDir string + rpcContainerLogsCommand *exec.Cmd + + rpcClient *Client + coreClient *stellarcore.Client + + daemon *daemon.Daemon + + masterAccount txnbuild.Account + shutdownOnce sync.Once + shutdown func() + onlyRPC bool +} + +func NewTest(t *testing.T, cfg *TestConfig) *Test { + if os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_ENABLED") == "" { + t.Skip("skipping integration test: SOROBAN_RPC_INTEGRATION_TESTS_ENABLED not set") + } + i := &Test{t: t} + + i.masterAccount = &txnbuild.SimpleAccount{ + AccountID: i.MasterKey().Address(), + Sequence: 0, + } + + parallel := true + shouldWaitForRPC := true + if cfg != nil { + i.rpcContainerVersion = cfg.UseReleasedRPCVersion + i.protocolVersion = cfg.ProtocolVersion + i.sqlitePath = cfg.SQLitePath + if cfg.OnlyRPC != nil { + i.onlyRPC = true + i.testPorts.TestCorePorts = cfg.OnlyRPC.CorePorts + shouldWaitForRPC = !cfg.OnlyRPC.DontWait + } + parallel = !cfg.NoParallel + } + + if i.sqlitePath == "" { + i.sqlitePath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") + } + + if parallel { + t.Parallel() + } + + if i.protocolVersion == 0 { + // Default to the maximum supported protocol version + i.protocolVersion = GetCoreMaxSupportedProtocol() + } + + i.rpcConfigFilesDir = i.t.TempDir() + + i.prepareShutdownHandlers() + if i.areThereContainers() { + i.spawnContainers() + } + if !i.onlyRPC { + i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(int(i.testPorts.CoreHTTPPort))} + i.waitForCore() + i.waitForCheckpoint() + } + if !i.runRPCInContainer() { + i.spawnRPCDaemon() + } + + i.rpcClient = NewClient(i.GetSorobanRPCURL(), nil) + if shouldWaitForRPC { + i.waitForRPC() + } + + 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 +} + +func (i *Test) runRPCInContainer() bool { + return i.rpcContainerVersion != "" +} + +func (i *Test) GetRPCLient() *Client { + return i.rpcClient +} +func (i *Test) MasterKey() *keypair.Full { + return keypair.Root(StandaloneNetworkPassphrase) +} + +func (i *Test) MasterAccount() txnbuild.Account { + return i.masterAccount +} + +func (i *Test) GetSorobanRPCURL() string { + return fmt.Sprintf("http://localhost:%d", i.testPorts.RPCPort) +} + +func (i *Test) GetAdminURL() string { + return fmt.Sprintf("http://localhost:%d", i.testPorts.RPCAdminPort) +} + +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 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) 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", + // The file will be inside the container + captiveCoreConfigPath: "/stellar-core.cfg", + // 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() 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{ + // 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(), + archiveURL: fmt.Sprintf("http://localhost:%d", i.testPorts.CoreArchivePort), + sqlitePath: i.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": vars.endPoint, + "ADMIN_ENDPOINT": vars.adminEndpoint, + "STELLAR_CORE_URL": vars.stellarCoreURL, + "CORE_REQUEST_TIMEOUT": "2s", + "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": vars.archiveURL, + "LOG_LEVEL": "debug", + "DB_PATH": vars.sqlitePath, + "INGESTION_TIMEOUT": "10m", + "EVENT_LEDGER_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), + "TRANSACTION_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), + "CHECKPOINT_FREQUENCY": strconv.Itoa(checkpointFrequency), + "MAX_HEALTHY_LEDGER_LATENCY": "10s", + "PREFLIGHT_ENABLE_DEBUG": "true", + } +} + +func (i *Test) waitForRPC() { + i.t.Log("Waiting for RPC to be healthy...") + + require.Eventually(i.t, + func() bool { + result, err := i.GetRPCHealth() + return err == nil && result.Status == "healthy" + }, + 30*time.Second, + time.Second, + ) +} + +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() + return cmd.Output() + } + + // Get old version of 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", 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 + 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) +} + +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))) +} + +func (i *Test) generateRPCConfigFile(rpcConfig rpcConfig) { + cfgFileContents := "" + for k, v := range rpcConfig.toMap() { + cfgFileContents += fmt.Sprintf("%s=%q\n", k, v) + } + err := os.WriteFile(filepath.Join(i.rpcConfigFilesDir, "soroban-rpc.config"), []byte(cfgFileContents), 0666) + 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) createRPCDaemon(c rpcConfig) *daemon.Daemon { + var cfg config.Config + m := c.toMap() + lookup := func(s string) (string, bool) { + ret, ok := m[s] + return ret, ok + } + require.NoError(i.t, cfg.SetValues(lookup)) + require.NoError(i.t, cfg.Validate()) + cfg.HistoryArchiveUserAgent = fmt.Sprintf("soroban-rpc/%s", config.Version) + + logger := supportlog.New() + logger.SetOutput(testLogWriter{t: i.t, prefix: `rpc="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 { + alphanumeric := nonAlphanumericRegex.ReplaceAllString(i.t.Name(), "") + return strings.ToLower(alphanumeric) +} + +func (i *Test) getComposeCommand(args ...string) *exec.Cmd { + composeFile := "docker-compose.yml" + if i.runRPCInContainer() { + 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}, cmdline...) + cmdline = append(cmdline, args...) + cmd := exec.Command("docker-compose", cmdline...) + + 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, + "RPC_IMAGE_TAG="+i.rpcContainerVersion, + "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) ([]byte, error) { + cmd := i.getComposeCommand(args...) + return cmd.Output() +} + +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() { + done := make(chan struct{}) + i.shutdown = func() { + close(done) + if i.daemon != nil { + i.daemon.Close() + i.daemon = nil + } + if i.rpcClient != nil { + i.rpcClient.Close() + } + if i.areThereContainers() { + i.stopContainers() + } + if i.rpcContainerLogsCommand != nil { + i.rpcContainerLogsCommand.Wait() + } + } + + // Register shutdown handlers (on panic and ctrl+c) so the containers are + // stopped even if ingestion or testing fails. + i.t.Cleanup(i.Shutdown) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + select { + case <-c: + i.Shutdown() + os.Exit(int(syscall.SIGTERM)) + case <-done: + } + }() +} + +// Shutdown stops the integration tests and destroys all its associated +// resources. It will be implicitly called when the calling test (i.e. the +// `testing.Test` passed to `New()`) is finished if it hasn't been explicitly +// called before. +func (i *Test) Shutdown() { + i.shutdownOnce.Do(func() { + i.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...") + require.Eventually(i.t, + func() bool { + _, err := i.getCoreInfo() + return err == nil + }, + 30*time.Second, + time.Second, + ) + + i.UpgradeProtocol(i.protocolVersion) + + 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. +func (i *Test) UpgradeProtocol(version uint32) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + err := i.coreClient.Upgrade(ctx, int(version)) + cancel() + require.NoError(i.t, err) + + 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() { + if i.daemon != nil { + i.daemon.Close() + i.daemon = nil + } + if i.runRPCInContainer() { + i.runSuccessfulComposeCommand("down", "rpc", "-v") + } +} + +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 (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 GetCoreMaxSupportedProtocol() uint32 { + str := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL") + if str == "" { + return MaxSupportedProtocolVersion + } + version, err := strconv.ParseUint(str, 10, 32) + if err != nil { + return MaxSupportedProtocolVersion + } + + return uint32(version) +} 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..3cd791b3 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/infrastructure/util.go @@ -0,0 +1,52 @@ +package infrastructure + +import ( + "fmt" + "net" + "path/filepath" + "runtime" + "time" + + "github.com/stellar/go/txnbuild" + "github.com/stretchr/testify/require" +) + +//go:noinline +func GetCurrentDirectory() string { + _, currentFilename, _, _ := runtime.Caller(1) + 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, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{op}, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + } +} + +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/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 62% rename from cmd/soroban-rpc/internal/test/migrate_test.go rename to cmd/soroban-rpc/internal/integrationtest/migrate_test.go index c8fcc7ec..dd3c9b9c 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,28 @@ 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, + SQLitePath: sqliteFile, }) - client := it.GetRPCLient() - // Submit an event-logging transaction in the version to migrate from - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) + submitTransactionResponse, _ := test.UploadHelloWorldContract() - 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(), + // 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() + corePorts := test.GetPorts().TestCorePorts + test = infrastructure.NewTest(t, &infrastructure.TestConfig{ + // We don't want to run Core again + OnlyRPC: &infrastructure.TestOnlyRPCConfig{ + CorePorts: corePorts, + DontWait: false, }, + SQLitePath: sqliteFile, + // We don't want to mark the test as parallel twice since it causes a panic + NoParallel: true, }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - submitTransactionResponse := sendSuccessfulTransaction(t, client, kp, tx) - - // 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}) // make sure that the transaction submitted before and its events exist in current RPC var transactionsResult methods.GetTransactionsResponse @@ -81,7 +70,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) @@ -93,16 +82,16 @@ 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)) } 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..380c9307 --- /dev/null +++ b/cmd/soroban-rpc/internal/integrationtest/simulate_transaction_test.go @@ -0,0 +1,630 @@ +package integrationtest + +import ( + "context" + "crypto/sha256" + "testing" + "time" + + "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 *infrastructure.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..ddda9f68 100644 --- a/cmd/soroban-rpc/internal/test/transaction_test.go +++ b/cmd/soroban-rpc/internal/integrationtest/transaction_test.go @@ -1,72 +1,31 @@ -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() - - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) + test := infrastructure.NewTest(t, nil) - 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 +35,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 +65,29 @@ func TestSendTransactionSucceedsWithResults(t *testing.T) { } func TestSendTransactionBadSequence(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, - 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 +97,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 +115,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 +138,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 +153,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 +166,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) @@ -243,12 +174,12 @@ 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) - 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 +191,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 +201,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/archive_test.go b/cmd/soroban-rpc/internal/test/archive_test.go deleted file mode 100644 index eaa4578e..00000000 --- a/cmd/soroban-rpc/internal/test/archive_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package test - -import ( - "net" - "net/http" - "net/http/httptest" - "net/http/httputil" - "net/url" - "strconv" - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestArchiveUserAgent(t *testing.T) { - archiveHost := net.JoinHostPort("localhost", strconv.Itoa(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) { - userAgents.Store(r.Header["User-Agent"][0], "") - proxy.ServeHTTP(w, r) - })) - defer historyArchiveProxy.Close() - - cfg := &TestConfig{ - HistoryArchiveURL: historyArchiveProxy.URL, - } - - 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") -} diff --git a/cmd/soroban-rpc/internal/test/get_version_info_test.go b/cmd/soroban-rpc/internal/test/get_version_info_test.go deleted file mode 100644 index 25a43f62..00000000 --- a/cmd/soroban-rpc/internal/test/get_version_info_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package test - -import ( - "context" - "fmt" - "os/exec" - "testing" - - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" - - "github.com/stretchr/testify/assert" - - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" -) - -func TestGetVersionInfoSucceeds(t *testing.T) { - test := NewTest(t, nil) - - version, commitHash, buildTimeStamp := config.Version, config.CommitHash, config.BuildTimestamp - - populateVersionInfo(test) - - // 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) - 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, test.protocolVersion, result.ProtocolVersion) - assert.NotEmpty(t, result.CaptiveCoreVersion) - -} - -// Runs git commands to fetch version information -func populateVersionInfo(test *Test) { - - execFunction := func(command string, args ...string) string { - cmd := exec.Command(command, args...) - test.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) - } - 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") -} diff --git a/cmd/soroban-rpc/internal/test/integration.go b/cmd/soroban-rpc/internal/test/integration.go deleted file mode 100644 index 531a162f..00000000 --- a/cmd/soroban-rpc/internal/test/integration.go +++ /dev/null @@ -1,476 +0,0 @@ -package test - -import ( - "context" - "fmt" - "os" - "os/exec" - "os/signal" - "path" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - "syscall" - "testing" - "time" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" - "github.com/stellar/go/clients/stellarcore" - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - "github.com/stretchr/testify/require" - - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/ledgerbucketwindow" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/methods" - - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/config" - "github.com/stellar/soroban-rpc/cmd/soroban-rpc/internal/daemon" -) - -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" -) - -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 -} - -type Test struct { - t *testing.T - - protocolVersion uint32 - - historyArchiveURL string - - rpcContainerVersion string - rpcContainerConfigMountDir string - rpcContainerSQLiteMountDir string - rpcContainerLogsCommand *exec.Cmd - - rpcClient *jrpc2.Client - coreClient *stellarcore.Client - - daemon *daemon.Daemon - - masterAccount txnbuild.Account - shutdownOnce sync.Once - shutdown func() -} - -func NewTest(t *testing.T, cfg *TestConfig) *Test { - if os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_ENABLED") == "" { - t.Skip("skipping integration test: SOROBAN_RPC_INTEGRATION_TESTS_ENABLED not set") - } - i := &Test{t: t} - - i.masterAccount = &txnbuild.SimpleAccount{ - AccountID: i.MasterKey().Address(), - Sequence: 0, - } - - sqlLitePath := "" - if cfg != nil { - i.historyArchiveURL = cfg.HistoryArchiveURL - i.rpcContainerVersion = cfg.UseReleasedRPCVersion - i.protocolVersion = cfg.ProtocolVersion - sqlLitePath = cfg.UseSQLitePath - } - - if i.protocolVersion == 0 { - // Default to the maximum supported protocol version - i.protocolVersion = GetCoreMaxSupportedProtocol() - } - - rpcCfg := i.getRPConfig(sqlLitePath) - if i.runRPCInContainer() { - i.rpcContainerConfigMountDir = i.createRPCContainerMountDir(rpcCfg) - } - i.runComposeCommand("up", "--detach", "--quiet-pull", "--no-color") - if i.runRPCInContainer() { - cmd := i.getComposeCommand("logs", "-f", "rpc") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - require.NoError(t, cmd.Start()) - } - i.prepareShutdownHandlers() - i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(stellarCorePort)} - i.waitForCore() - i.waitForCheckpoint() - if !i.runRPCInContainer() { - i.daemon = i.createDaemon(rpcCfg) - go i.daemon.Run() - } - i.waitForRPC() - - return i -} - -func (i *Test) runRPCInContainer() bool { - return i.rpcContainerVersion != "" -} - -func (i *Test) GetRPCLient() *jrpc2.Client { - return i.rpcClient -} -func (i *Test) MasterKey() *keypair.Full { - return keypair.Root(StandaloneNetworkPassphrase) -} - -func (i *Test) MasterAccount() txnbuild.Account { - return i.masterAccount -} - -func (i *Test) sorobanRPCURL() string { - return fmt.Sprintf("http://localhost:%d", sorobanRPCPort) -} - -func (i *Test) adminURL() string { - return fmt.Sprintf("http://localhost:%d", adminPort) -} - -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") -} - -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", StellarCoreArchivePort) - 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) - } - if i.historyArchiveURL != "" { - // an archive URL was supplied explicitly - archiveURL = i.historyArchiveURL - } - - captiveCoreConfigPath := path.Join(GetCurrentDirectory(), "captive-core-integration-tests.cfg") - bindHost := "localhost" - stellarCoreURL := fmt.Sprintf("http://localhost:%d", stellarCorePort) - if i.runRPCInContainer() { - // The file will be inside the container - 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", stellarCorePort) - } - - // in the container - captiveCoreStoragePath := "/tmp/captive-core" - if !i.runRPCInContainer() { - captiveCoreStoragePath = i.t.TempDir() - } - - return map[string]string{ - "ENDPOINT": fmt.Sprintf("%s:%d", bindHost, sorobanRPCPort), - "ADMIN_ENDPOINT": fmt.Sprintf("%s:%d", bindHost, adminPort), - "STELLAR_CORE_URL": stellarCoreURL, - "CORE_REQUEST_TIMEOUT": "2s", - "STELLAR_CORE_BINARY_PATH": coreBinaryPath, - "CAPTIVE_CORE_CONFIG_PATH": captiveCoreConfigPath, - "CAPTIVE_CORE_STORAGE_PATH": captiveCoreStoragePath, - "STELLAR_CAPTIVE_CORE_HTTP_PORT": "0", - "FRIENDBOT_URL": friendbotURL, - "NETWORK_PASSPHRASE": StandaloneNetworkPassphrase, - "HISTORY_ARCHIVE_URLS": archiveURL, - "LOG_LEVEL": "debug", - "DB_PATH": sqlitePath, - "INGESTION_TIMEOUT": "10m", - "EVENT_LEDGER_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), - "TRANSACTION_RETENTION_WINDOW": strconv.Itoa(ledgerbucketwindow.OneDayOfLedgers), - "CHECKPOINT_FREQUENCY": strconv.Itoa(checkpointFrequency), - "MAX_HEALTHY_LEDGER_LATENCY": "10s", - "PREFLIGHT_ENABLE_DEBUG": "true", - } -} - -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 - refreshClient := func() { - if i.rpcClient != nil { - i.rpcClient.Close() - } - ch := jhttp.NewChannel(i.sorobanRPCURL(), 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") -} - -func (i *Test) createRPCContainerMountDir(rpcConfig map[string]string) string { - mountDir := i.t.TempDir() - // 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() - 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) - err = os.WriteFile(filepath.Join(mountDir, "stellar-core-integration-tests.cfg"), []byte(captiveCoreCfgContents), 0666) - require.NoError(i.t, err) - - // Generate config file - cfgFileContents := "" - for k, v := range rpcConfig { - cfgFileContents += fmt.Sprintf("%s=%q\n", k, v) - } - err = os.WriteFile(filepath.Join(mountDir, "soroban-rpc.config"), []byte(cfgFileContents), 0666) - require.NoError(i.t, err) - - return mountDir -} - -func (i *Test) createDaemon(env map[string]string) *daemon.Daemon { - var cfg config.Config - lookup := func(s string) (string, bool) { - ret, ok := env[s] - return ret, ok - } - 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) -} - -func (i *Test) getComposeCommand(args ...string) *exec.Cmd { - integrationYaml := filepath.Join(GetCurrentDirectory(), "docker-compose.yml") - configFiles := []string{"-f", integrationYaml} - if i.runRPCInContainer() { - rpcYaml := filepath.Join(GetCurrentDirectory(), "docker-compose.rpc.yml") - configFiles = append(configFiles, "-f", rpcYaml) - } - cmdline := append(configFiles, args...) - cmd := exec.Command("docker-compose", cmdline...) - - 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, - "RPC_IMAGE_TAG="+i.rpcContainerVersion, - "RPC_CONFIG_MOUNT_DIR="+i.rpcContainerConfigMountDir, - "RPC_SQLITE_MOUNT_DIR="+i.rpcContainerSQLiteMountDir, - "RPC_UID="+strconv.Itoa(os.Getuid()), - "RPC_GID="+strconv.Itoa(os.Getgid()), - ) - } - if len(cmd.Env) > 0 { - cmd.Env = append(cmd.Env, os.Environ()...) - } - 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) - 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 { - i.t.Fatalf("Compose command failed: %v", innerErr) - } -} - -func (i *Test) prepareShutdownHandlers() { - done := make(chan struct{}) - i.shutdown = func() { - close(done) - i.StopRPC() - if i.rpcClient != nil { - i.rpcClient.Close() - } - i.runComposeCommand("down", "-v") - if i.rpcContainerLogsCommand != nil { - i.rpcContainerLogsCommand.Wait() - } - } - - // Register shutdown handlers (on panic and ctrl+c) so the containers are - // stopped even if ingestion or testing fails. - i.t.Cleanup(i.Shutdown) - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - go func() { - select { - case <-c: - i.Shutdown() - os.Exit(int(syscall.SIGTERM)) - case <-done: - } - }() -} - -// Shutdown stops the integration tests and destroys all its associated -// resources. It will be implicitly called when the calling test (i.e. the -// `testing.Test` passed to `New()`) is finished if it hasn't been explicitly -// called before. -func (i *Test) Shutdown() { - i.shutdownOnce.Do(func() { - i.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 - } - - 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") -} - -// UpgradeProtocol arms Core with upgrade and blocks until protocol is upgraded. -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) - } - - i.t.Fatalf("could not upgrade protocol in 10s") -} - -func (i *Test) StopRPC() { - if i.daemon != nil { - i.daemon.Close() - i.daemon = nil - } - if i.runRPCInContainer() { - i.runComposeCommand("down", "rpc", "-v") - } -} - -//go:noinline -func GetCurrentDirectory() string { - _, currentFilename, _, _ := runtime.Caller(1) - return filepath.Dir(currentFilename) -} - -func GetCoreMaxSupportedProtocol() uint32 { - str := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL") - if str == "" { - return MaxSupportedProtocolVersion - } - version, err := strconv.ParseUint(str, 10, 32) - if err != nil { - return MaxSupportedProtocolVersion - } - - return uint32(version) -} 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) -} 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() }, } 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=