Skip to content

Commit

Permalink
feat: build_restore_txn to restore entries listed in restore preamble
Browse files Browse the repository at this point in the history
  • Loading branch information
willemneal committed Sep 12, 2023
1 parent 38b5e24 commit cb25a0c
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 16 deletions.
40 changes: 29 additions & 11 deletions cmd/soroban-cli/src/rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");

Expand Down Expand Up @@ -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<String>,
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[derive(serde::Deserialize, serde::Serialize, Debug, Default)]
pub struct RestorePreamble {
#[serde(rename = "transactionData")]
pub transaction_data: String,
Expand Down Expand Up @@ -626,22 +627,26 @@ soroban config identity fund {address} --helper-url <url>"#
pub async fn prepare_transaction(
&self,
tx: &Transaction,
) -> Result<(Transaction, Vec<DiagnosticEvent>), Error> {
) -> Result<(Transaction, Option<RestorePreamble>, Vec<DiagnosticEvent>), Error> {
tracing::trace!(?tx);
let sim_response = self
.simulate_transaction(&TransactionEnvelope::Tx(TransactionV1Envelope {
tx: tx.clone(),
signatures: VecM::default(),
}))
.await?;

let events = sim_response
.events
.iter()
.map(DiagnosticEvent::from_xdr_base64)
.collect::<Result<Vec<_>, _>>()?;

Ok((assemble(tx, &sim_response)?, events))
Ok((
assemble(tx, &sim_response)?,
sim_response
.restore_preamble
.filter(|p| !p.transaction_data.is_empty()),
events,
))
}

pub async fn prepare_and_send_transaction(
Expand All @@ -654,7 +659,19 @@ soroban config identity fund {address} --helper-url <url>"#
log_resources: Option<LogResources>,
) -> Result<(TransactionResult, TransactionMeta, Vec<DiagnosticEvent>), 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,
Expand All @@ -671,7 +688,8 @@ soroban config identity fund {address} --helper-url <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
Expand Down
40 changes: 35 additions & 5 deletions cmd/soroban-cli/src/rpc/transaction.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -213,6 +214,35 @@ pub fn sign_soroban_authorization_entry(
Ok(auth)
}

pub fn build_restore_txn(
parent: &Transaction,
restore: &RestorePreamble,
) -> Result<Transaction, Error> {
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::*;
Expand Down
150 changes: 150 additions & 0 deletions cmd/soroban-rpc/internal/test/cli_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package test

import (
"context"
"crypto/sha256"
"fmt"
"os"
"strings"
"testing"
"time"

"github.com/creachadair/jrpc2"
"github.com/creachadair/jrpc2/jhttp"
"github.com/google/shlex"
"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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gotest.tools/v3/icmd"
)
Expand Down Expand Up @@ -62,6 +66,152 @@ func TestCLIContractDeployAndInvoke(t *testing.T) {
require.Contains(t, output, `["Hello","world"]`)
}

func TestCLISimulateTransactionBumpAndRestoreFootprint(t *testing.T) {
test := NewCLITest(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)
runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", contractID))

// get the counter ledger entry expiration
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)

expirationKey := xdr.LedgerKey{
Type: xdr.LedgerEntryTypeExpiration,
Expiration: &xdr.LedgerKeyExpiration{
KeyHash: sha256.Sum256(binKey),
},
}

keyB64, err := xdr.MarshalBase64(expirationKey)
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.LedgerEntryTypeExpiration, entry.Type)
initialExpirationSeq := entry.Expiration.ExpirationLedgerSeq

// bump the initial expiration
params = preflightTransactionParams(t, client, txnbuild.TransactionParams{
SourceAccount: &account,
IncrementSequenceNum: true,
Operations: []txnbuild.Operation{
&txnbuild.BumpFootprintExpiration{
LedgersToExpire: 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.LedgerEntryTypeExpiration, entry.Type)
newExpirationSeq := entry.Expiration.ExpirationLedgerSeq
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()
output := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", contractID))
require.Equal(t, "2", output)
}

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()))
Expand Down

0 comments on commit cb25a0c

Please sign in to comment.