diff --git a/cmd/crates/soroban-test/tests/fixtures/test-wasms/hello_world/src/lib.rs b/cmd/crates/soroban-test/tests/fixtures/test-wasms/hello_world/src/lib.rs index 24cb2d8b2..f77c9be67 100644 --- a/cmd/crates/soroban-test/tests/fixtures/test-wasms/hello_world/src/lib.rs +++ b/cmd/crates/soroban-test/tests/fixtures/test-wasms/hello_world/src/lib.rs @@ -30,7 +30,7 @@ impl Contract { addr } - pub fn inc(env: Env) { + pub fn inc_persistent(env: Env) { let mut count: u32 = env.storage().persistent().get(&COUNTER).unwrap_or(0); // Panic if the value of COUNTER is not u32. log!(&env, "count: {}", count); @@ -41,6 +41,17 @@ impl Contract { env.storage().persistent().set(&COUNTER, &count); } + pub fn inc_tmp(env: Env) { + let mut count: u32 = env.storage().temporary().get(&COUNTER).unwrap_or(0); // Panic if the value of COUNTER is not u32. + log!(&env, "count: {}", count); + + // Increment the count. + count += 1; + + // Save the count. + env.storage().temporary().set(&COUNTER, &count); + } + #[allow(unused_variables)] pub fn multi_word_cmd(env: Env, contract_owner: String) {} /// Logs a string with `hello ` in front. diff --git a/cmd/soroban-rpc/internal/ingest/service.go b/cmd/soroban-rpc/internal/ingest/service.go index 7b0a38777..fc92b1809 100644 --- a/cmd/soroban-rpc/internal/ingest/service.go +++ b/cmd/soroban-rpc/internal/ingest/service.go @@ -2,6 +2,7 @@ package ingest import ( "context" + "crypto/sha256" "errors" "fmt" "sync" @@ -247,6 +248,10 @@ func (s *Service) ingest(ctx context.Context, sequence uint32) error { return err } + if err := s.evictLedgerEntries(tx, ledgerCloseMeta); err != nil { + return err + } + if err := s.ingestLedgerCloseMeta(tx, ledgerCloseMeta); err != nil { return err } @@ -279,3 +284,43 @@ func (s *Service) ingestLedgerCloseMeta(tx db.WriteTx, ledgerCloseMeta xdr.Ledge } return nil } + +func (s *Service) evictLedgerEntries(tx db.WriteTx, ledgerCloseMeta xdr.LedgerCloseMeta) error { + if ledgerCloseMeta.V != 2 { + return fmt.Errorf("unexpected close meta version: %d", ledgerCloseMeta.V) + } + keysToEvict := make([]xdr.LedgerKey, len(ledgerCloseMeta.V2.EvictedTemporaryLedgerKeys)+len(ledgerCloseMeta.V2.EvictedPersistentLedgerEntries)) + l := copy(keysToEvict, ledgerCloseMeta.V2.EvictedTemporaryLedgerKeys) + for i, entry := range ledgerCloseMeta.V2.EvictedPersistentLedgerEntries { + // TODO: we probably shouldn't be deleting evicted persistent ledger entries cold turkey. + // Otherwise preflighting will fail for restoreFootprint. + // For the restoreFootPrint preflighting we need to confirm that the entry existed and its size (for the resource estimation). + // so maybe we should store that. + key, err := entry.LedgerKey() + if err != nil { + return err + } + keysToEvict[l+i] = key + } + for _, key := range keysToEvict { + if err := tx.LedgerEntryWriter().DeleteLedgerEntry(key); err != nil { + return err + } + // Also delete the matching expiration ledger entry + bin, err := key.MarshalBinary() + if err != nil { + return err + } + expirationKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &xdr.LedgerKeyExpiration{ + KeyHash: sha256.Sum256(bin), + }, + } + if err := tx.LedgerEntryWriter().DeleteLedgerEntry(expirationKey); err != nil { + return err + } + + } + return nil +} diff --git a/cmd/soroban-rpc/internal/test/ingestion_test.go b/cmd/soroban-rpc/internal/test/ingestion_test.go new file mode 100644 index 000000000..916eb6f65 --- /dev/null +++ b/cmd/soroban-rpc/internal/test/ingestion_test.go @@ -0,0 +1,150 @@ +package test + +import ( + "context" + "crypto/sha256" + "testing" + "time" + + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/jhttp" + "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-tools/cmd/soroban-rpc/internal/methods" +) + +func TestEvictTemporaryLedgerEntries(t *testing.T) { + test := NewTest(t) + + ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) + client := jrpc2.NewClient(ch, nil) + + 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(t, address, helloWorldContract, StandaloneNetworkPassphrase), + }, + 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) + invokeIncTemporaryEntryParams := txnbuild.TransactionParams{ + SourceAccount: &account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + createInvokeHostOperation( + address, + contractID, + "inc_tmp", + ), + }, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + } + params = preflightTransactionParams(t, client, invokeIncTemporaryEntryParams) + tx, err = txnbuild.NewTransaction(params) + assert.NoError(t, err) + sendSuccessfulTransaction(t, client, sourceAccount, tx) + + 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.ContractDataDurabilityTemporary, + }, + } + + // make sure the ledger entry exists and so does the expiration entry counterpart + + 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) + require.NoError(t, err) + + binKey, err := key.MarshalBinary() + assert.NoError(t, err) + + expirationKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &xdr.LedgerKeyExpiration{ + KeyHash: sha256.Sum256(binKey), + }, + } + + keyB64, err = xdr.MarshalBase64(expirationKey) + require.NoError(t, err) + getExpirationLedgerEntryRequest := methods.GetLedgerEntryRequest{ + Key: keyB64, + } + + err = client.CallResult(context.Background(), "getLedgerEntry", getExpirationLedgerEntryRequest, &getLedgerEntryResult) + assert.NoError(t, err) + + // Wait until the entry gets evicted + evicted := false + for i := 0; i < 5000; i++ { + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryRequest, &getLedgerEntryResult) + if err != nil { + evicted = true + t.Logf("ledger entry evicted") + break + } + t.Log("waiting for ledger entry to get evicted") + time.Sleep(time.Second) + } + + require.True(t, evicted) + + // Make sure that the expiration ledger entry was also evicted + err = client.CallResult(context.Background(), "getLedgerEntry", getExpirationLedgerEntryRequest, &getLedgerEntryResult) + assert.Error(t, err) +} diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go index 1ea57f2c8..1568075b3 100644 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go @@ -657,14 +657,14 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { sendSuccessfulTransaction(t, client, sourceAccount, tx) contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - invokeIncPresistentEntryParams := txnbuild.TransactionParams{ + invokeIncPersistentEntryParams := txnbuild.TransactionParams{ SourceAccount: &account, IncrementSequenceNum: true, Operations: []txnbuild.Operation{ createInvokeHostOperation( address, contractID, - "inc", + "inc_persistent", ), }, BaseFee: txnbuild.MinBaseFee, @@ -672,7 +672,7 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { TimeBounds: txnbuild.NewInfiniteTimeout(), }, } - params = preflightTransactionParams(t, client, invokeIncPresistentEntryParams) + params = preflightTransactionParams(t, client, invokeIncPersistentEntryParams) tx, err = txnbuild.NewTransaction(params) assert.NoError(t, err) sendSuccessfulTransaction(t, client, sourceAccount, tx) @@ -707,11 +707,11 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { keyB64, err := xdr.MarshalBase64(expirationKey) require.NoError(t, err) - getLedgerEntryrequest := methods.GetLedgerEntryRequest{ + getLedgerEntryRequest := methods.GetLedgerEntryRequest{ Key: keyB64, } var getLedgerEntryResult methods.GetLedgerEntryResponse - err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryRequest, &getLedgerEntryResult) assert.NoError(t, err) var entry xdr.LedgerEntryData assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) @@ -747,7 +747,7 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { assert.NoError(t, err) sendSuccessfulTransaction(t, client, sourceAccount, tx) - err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryRequest, &getLedgerEntryResult) assert.NoError(t, err) assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) assert.Equal(t, xdr.LedgerEntryTypeExpiration, entry.Type) @@ -758,7 +758,7 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { waitForExpiration := func() { expired := false for i := 0; i < 50; i++ { - err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryRequest, &getLedgerEntryResult) assert.NoError(t, err) assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) assert.Equal(t, xdr.LedgerEntryTypeExpiration, entry.Type) @@ -806,7 +806,7 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { // Wait for expiration again and check the pre-restore field when trying to exec the contract again waitForExpiration() - simulationResult := simulateTransactionFromTxParams(t, client, invokeIncPresistentEntryParams) + simulationResult := simulateTransactionFromTxParams(t, client, invokeIncPersistentEntryParams) assert.NotZero(t, simulationResult.RestorePreamble) params = preflightTransactionParamsLocally(t, @@ -831,7 +831,7 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { // 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) + params = preflightTransactionParamsLocally(t, invokeIncPersistentEntryParams, simulationResult) tx, err = txnbuild.NewTransaction(params) assert.NoError(t, err) sendSuccessfulTransaction(t, client, sourceAccount, tx)