Skip to content

Commit

Permalink
Feat/restore preamble (#931)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Alfonso Acosta <[email protected]>
  • Loading branch information
willemneal and 2opremio authored Sep 14, 2023
1 parent 7d3b417 commit cdbbf85
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 61 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/soroban-rpc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -39,6 +39,7 @@ impl Contract {

// Save the count.
env.storage().persistent().set(&COUNTER, &count);
count
}

#[allow(unused_variables)]
Expand Down
38 changes: 27 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,24 @@ 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,
events,
))
}

pub async fn prepare_and_send_transaction(
Expand All @@ -654,7 +657,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 +686,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
89 changes: 73 additions & 16 deletions cmd/soroban-rpc/internal/test/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package test

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strings"
Expand All @@ -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"
)
Expand All @@ -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)
}

Expand All @@ -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 {
Expand All @@ -81,23 +131,31 @@ 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(),
Sequence: 1,
},
IncrementSequenceNum: false,
Operations: []txnbuild.Operation{&txnbuild.CreateAccount{
Destination: "GDIY6AQQ75WMD4W46EYB7O6UYMHOCGQHLAQGQTKHDX4J2DYQCHVCR4W4",
Amount: "100000",
Destination: account,
Amount: amount,
}},
BaseFee: txnbuild.MinBaseFee,
Memo: nil,
Expand All @@ -107,5 +165,4 @@ func NewCLITest(t *testing.T) *Test {
})
require.NoError(t, err)
sendSuccessfulTransaction(t, client, sourceAccount, tx)
return test
}
Loading

0 comments on commit cdbbf85

Please sign in to comment.