From cdbbf8554588bfbac0eb22e3b8753e3ca3add23f Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 14 Sep 2023 18:46:05 -0400 Subject: [PATCH] Feat/restore preamble (#931) --------- Co-authored-by: Alfonso Acosta --- .github/workflows/soroban-rpc.yml | 2 +- .../test-wasms/hello_world/src/lib.rs | 3 +- cmd/soroban-cli/src/rpc/mod.rs | 38 +++++--- cmd/soroban-cli/src/rpc/transaction.rs | 40 +++++++-- cmd/soroban-rpc/internal/test/cli_test.go | 89 +++++++++++++++---- .../test/simulate_transaction_test.go | 68 ++++++++------ 6 files changed, 179 insertions(+), 61 deletions(-) diff --git a/.github/workflows/soroban-rpc.yml b/.github/workflows/soroban-rpc.yml index 5afe44b52..95daa3e1e 100644 --- a/.github/workflows/soroban-rpc.yml +++ b/.github/workflows/soroban-rpc.yml @@ -123,7 +123,7 @@ jobs: - uses: ./.github/actions/setup-go with: go-version: ${{ matrix.go }} - + - uses: stellar/actions/rust-cache@main - name: Build soroban contract fixtures run: | rustup update 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..7bdf62194 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(env: Env) -> u32 { 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); @@ -39,6 +39,7 @@ impl Contract { // Save the count. env.storage().persistent().set(&COUNTER, &count); + count } #[allow(unused_variables)] diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/soroban-cli/src/rpc/mod.rs index e817b2a38..58702e6c7 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/soroban-cli/src/rpc/mod.rs @@ -7,8 +7,9 @@ use serde_aux::prelude::{deserialize_default_from_null, deserialize_number_from_ use soroban_env_host::xdr::{ self, AccountEntry, AccountId, ContractDataEntry, DiagnosticEvent, Error as XdrError, LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, PublicKey, ReadXdr, - SorobanAuthorizationEntry, SorobanResources, Transaction, TransactionEnvelope, TransactionMeta, - TransactionMetaV3, TransactionResult, TransactionV1Envelope, Uint256, VecM, WriteXdr, + SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, TransactionEnvelope, + TransactionMeta, TransactionMetaV3, TransactionResult, TransactionV1Envelope, Uint256, VecM, + WriteXdr, }; use soroban_env_host::xdr::{DepthLimitedRead, SorobanAuthorizedFunction}; use soroban_sdk::token; @@ -24,7 +25,7 @@ use tokio::time::sleep; use crate::utils::{self, contract_spec}; mod transaction; -use transaction::{assemble, sign_soroban_authorizations}; +use transaction::{assemble, build_restore_txn, sign_soroban_authorizations}; const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); @@ -231,12 +232,12 @@ pub struct SimulateTransactionResponse { rename = "latestLedger", deserialize_with = "deserialize_number_from_string" )] - pub latest_ledger: u64, + pub latest_ledger: u32, #[serde(skip_serializing_if = "Option::is_none", default)] pub error: Option, } -#[derive(serde::Deserialize, serde::Serialize, Debug)] +#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] pub struct RestorePreamble { #[serde(rename = "transactionData")] pub transaction_data: String, @@ -626,7 +627,7 @@ soroban config identity fund {address} --helper-url "# pub async fn prepare_transaction( &self, tx: &Transaction, - ) -> Result<(Transaction, Vec), Error> { + ) -> Result<(Transaction, Option, Vec), Error> { tracing::trace!(?tx); let sim_response = self .simulate_transaction(&TransactionEnvelope::Tx(TransactionV1Envelope { @@ -634,14 +635,16 @@ soroban config identity fund {address} --helper-url "# signatures: VecM::default(), })) .await?; - let events = sim_response .events .iter() .map(DiagnosticEvent::from_xdr_base64) .collect::, _>>()?; - - Ok((assemble(tx, &sim_response)?, events)) + Ok(( + assemble(tx, &sim_response)?, + sim_response.restore_preamble, + events, + )) } pub async fn prepare_and_send_transaction( @@ -654,7 +657,19 @@ soroban config identity fund {address} --helper-url "# log_resources: Option, ) -> Result<(TransactionResult, TransactionMeta, Vec), Error> { let GetLatestLedgerResponse { sequence, .. } = self.get_latest_ledger().await?; - let (unsigned_tx, events) = self.prepare_transaction(tx_without_preflight).await?; + let (mut unsigned_tx, restore_preamble, events) = + self.prepare_transaction(tx_without_preflight).await?; + if let Some(restore) = restore_preamble { + // Build and submit the restore transaction + self.send_transaction(&utils::sign_transaction( + source_key, + &build_restore_txn(&unsigned_tx, &restore)?, + network_passphrase, + )?) + .await?; + // Increment the original txn's seq_num so it doesn't conflict + unsigned_tx.seq_num = SequenceNumber(unsigned_tx.seq_num.0 + 1); + } let (part_signed_tx, signed_auth_entries) = sign_soroban_authorizations( &unsigned_tx, source_key, @@ -671,7 +686,8 @@ soroban config identity fund {address} --helper-url "# (part_signed_tx, events) } else { // re-simulate to calculate the new fees - self.prepare_transaction(&part_signed_tx).await? + let (tx, _, events) = self.prepare_transaction(&part_signed_tx).await?; + (tx, events) }; // Try logging stuff if requested diff --git a/cmd/soroban-cli/src/rpc/transaction.rs b/cmd/soroban-cli/src/rpc/transaction.rs index 551013ba6..520fa3601 100644 --- a/cmd/soroban-cli/src/rpc/transaction.rs +++ b/cmd/soroban-cli/src/rpc/transaction.rs @@ -1,13 +1,14 @@ use ed25519_dalek::Signer; use sha2::{Digest, Sha256}; use soroban_env_host::xdr::{ - AccountId, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, OperationBody, PublicKey, - ReadXdr, ScAddress, ScMap, ScSymbol, ScVal, SorobanAddressCredentials, - SorobanAuthorizationEntry, SorobanCredentials, SorobanTransactionData, Transaction, - TransactionExt, Uint256, VecM, WriteXdr, + AccountId, ExtensionPoint, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, Memo, + Operation, OperationBody, Preconditions, PublicKey, ReadXdr, RestoreFootprintOp, ScAddress, + ScMap, ScSymbol, ScVal, SorobanAddressCredentials, SorobanAuthorizationEntry, + SorobanCredentials, SorobanTransactionData, Transaction, TransactionExt, Uint256, VecM, + WriteXdr, }; -use crate::rpc::{Error, SimulateTransactionResponse}; +use crate::rpc::{Error, RestorePreamble, SimulateTransactionResponse}; // Apply the result of a simulateTransaction onto a transaction envelope, preparing it for // submission to the network. @@ -213,6 +214,35 @@ pub fn sign_soroban_authorization_entry( Ok(auth) } +pub fn build_restore_txn( + parent: &Transaction, + restore: &RestorePreamble, +) -> Result { + let transaction_data = + SorobanTransactionData::from_xdr_base64(restore.transaction_data.clone())?; + let fee = u32::try_from(restore.min_resource_fee) + .map_err(|_| Error::LargeFee(restore.min_resource_fee))?; + Ok(Transaction { + source_account: parent.source_account.clone(), + fee: parent + .fee + .checked_add(fee) + .ok_or(Error::LargeFee(restore.min_resource_fee))?, + seq_num: parent.seq_num.clone(), + cond: Preconditions::None, + memo: Memo::None, + operations: vec![Operation { + source_account: None, + body: OperationBody::RestoreFootprint(RestoreFootprintOp { + ext: ExtensionPoint::V0, + }), + }] + .try_into() + .unwrap(), + ext: TransactionExt::V1(transaction_data), + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go index 88bb75a5a..e4b5de52a 100644 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ b/cmd/soroban-rpc/internal/test/cli_test.go @@ -2,6 +2,7 @@ package test import ( "crypto/sha256" + "encoding/hex" "fmt" "os" "strings" @@ -11,8 +12,10 @@ import ( "github.com/creachadair/jrpc2/jhttp" "github.com/google/shlex" "github.com/stellar/go/keypair" + "github.com/stellar/go/strkey" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gotest.tools/v3/icmd" ) @@ -30,7 +33,13 @@ func TestCLIContractInstallAndDeploy(t *testing.T) { runSuccessfulCLICmd(t, "contract install --wasm "+helloWorldContractPath) wasm := getHelloWorldContract(t) contractHash := xdr.Hash(sha256.Sum256(wasm)) - output := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt 0 --wasm-hash %s", contractHash.HexString())) + output := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt %s --wasm-hash %s", hex.EncodeToString(testSalt[:]), contractHash.HexString())) + outputsContractIDInLastLine(t, output) +} + +func TestCLIContractDeploy(t *testing.T) { + NewCLITest(t) + output := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt %s --wasm %s", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) outputsContractIDInLastLine(t, output) } @@ -48,24 +57,65 @@ func outputsContractIDInLastLine(t *testing.T, output string) { require.Regexp(t, "^C", contractID) } -func TestCLIContractDeploy(t *testing.T) { - NewCLITest(t) - output := runSuccessfulCLICmd(t, "contract deploy --salt 0 --wasm "+helloWorldContractPath) - outputsContractIDInLastLine(t, output) -} - func TestCLIContractDeployAndInvoke(t *testing.T) { NewCLITest(t) - output := runSuccessfulCLICmd(t, "contract deploy --salt=0 --wasm "+helloWorldContractPath) - contractID := strings.TrimSpace(output) - output = runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- hello --world=world", contractID)) + contractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) + output := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- hello --world=world", contractID)) require.Contains(t, output, `["Hello","world"]`) } +func TestCLIRestorePreamble(t *testing.T) { + test := NewCLITest(t) + strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) + count := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) + require.Equal(t, "1", count) + count = runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) + require.Equal(t, "2", count) + + // 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) + ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) + client := jrpc2.NewClient(ch, nil) + contractIDBytes := strkey.MustDecode(strkey.VersionByteContract, strkeyContractID) + require.Len(t, contractIDBytes, 32) + var contractID [32]byte + copy(contractID[:], contractIDBytes) + 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, + }, + } + + binKey, err := key.MarshalBinary() + assert.NoError(t, err) + + expiration := xdr.LedgerKeyExpiration{ + KeyHash: sha256.Sum256(binKey), + } + waitForLedgerEntryToExpire(t, client, expiration) + + count = runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) + require.Equal(t, "3", count) +} + func runSuccessfulCLICmd(t *testing.T, cmd string) string { res := runCLICommand(t, cmd) - require.NoError(t, res.Error, fmt.Sprintf("stderr:\n%s\nstdout:\n%s\n", res.Stderr(), res.Stdout())) - return res.Stdout() + stdout, stderr := res.Stdout(), res.Stderr() + outputs := fmt.Sprintf("stderr:\n%s\nstdout:\n%s\n", stderr, stdout) + require.NoError(t, res.Error, outputs) + fmt.Printf(outputs) + return strings.TrimSpace(stdout) } func runCLICommand(t *testing.T, cmd string) *icmd.Result { @@ -81,14 +131,22 @@ func runCLICommand(t *testing.T, cmd string) *icmd.Result { return icmd.RunCmd(c) } +func getCLIDefaultAccount(t *testing.T) string { + return runSuccessfulCLICmd(t, "config identity address --hd-path 0") +} + func NewCLITest(t *testing.T) *Test { test := NewTest(t) + fundAccount(t, test, getCLIDefaultAccount(t), "1000000") + return test +} + +func fundAccount(t *testing.T, test *Test, account string, amount string) { ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - // Create default account used by the CLI tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ SourceAccount: &txnbuild.SimpleAccount{ AccountID: keypair.Root(StandaloneNetworkPassphrase).Address(), @@ -96,8 +154,8 @@ func NewCLITest(t *testing.T) *Test { }, IncrementSequenceNum: false, Operations: []txnbuild.Operation{&txnbuild.CreateAccount{ - Destination: "GDIY6AQQ75WMD4W46EYB7O6UYMHOCGQHLAQGQTKHDX4J2DYQCHVCR4W4", - Amount: "100000", + Destination: account, + Amount: amount, }}, BaseFee: txnbuild.MinBaseFee, Memo: nil, @@ -107,5 +165,4 @@ func NewCLITest(t *testing.T) *Test { }) require.NoError(t, err) sendSuccessfulTransaction(t, client, sourceAccount, tx) - return test } diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go index ffab2c9e3..fa31785d3 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: 4961700, + Instructions: 5007615, ReadBytes: 48, - WriteBytes: 5468, + WriteBytes: 5532, }, RefundableFee: 20056, } @@ -749,11 +749,12 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { binKey, err := key.MarshalBinary() assert.NoError(t, err) + expiration := xdr.LedgerKeyExpiration{ + KeyHash: sha256.Sum256(binKey), + } expirationKey := xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeExpiration, - Expiration: &xdr.LedgerKeyExpiration{ - KeyHash: sha256.Sum256(binKey), - }, + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &expiration, } keyB64, err := xdr.MarshalBase64(expirationKey) @@ -806,26 +807,7 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { assert.Greater(t, newExpirationSeq, initialExpirationSeq) // Wait until it expires - 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.LedgerEntryTypeExpiration, entry.Type) - // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger - currentLedger := getLedgerEntryResult.LatestLedger + 1 - if xdr.Uint32(currentLedger) > entry.Expiration.ExpirationLedgerSeq { - expired = true - t.Logf("ledger entry expired") - break - } - t.Log("waiting for ledger entry to expire at ledger", entry.Expiration.ExpirationLedgerSeq) - time.Sleep(time.Second) - } - require.True(t, expired) - } - waitForExpiration() + waitForLedgerEntryToExpire(t, client, expiration) // and restore it params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ @@ -855,7 +837,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 - waitForExpiration() + waitForLedgerEntryToExpire(t, client, expiration) simulationResult := simulateTransactionFromTxParams(t, client, invokeIncPresistentEntryParams) require.NotNil(t, simulationResult.RestorePreamble) @@ -888,3 +870,35 @@ func TestSimulateTransactionBumpAndRestoreFootprint(t *testing.T) { assert.NoError(t, err) sendSuccessfulTransaction(t, client, sourceAccount, tx) } + +func waitForLedgerEntryToExpire(t *testing.T, client *jrpc2.Client, expiration xdr.LedgerKeyExpiration) { + expirationKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &expiration, + } + + keyB64, err := xdr.MarshalBase64(expirationKey) + require.NoError(t, err) + getLedgerEntryrequest := methods.GetLedgerEntryRequest{ + Key: keyB64, + } + expired := false + for i := 0; i < 50; i++ { + var getLedgerEntryResult methods.GetLedgerEntryResponse + 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) + // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger + currentLedger := getLedgerEntryResult.LatestLedger + 1 + if xdr.Uint32(currentLedger) > entry.Expiration.ExpirationLedgerSeq { + expired = true + t.Logf("ledger entry expired") + break + } + t.Log("waiting for ledger entry to expire at ledger", entry.Expiration.ExpirationLedgerSeq) + time.Sleep(time.Second) + } + require.True(t, expired) +}