Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/restore preamble #931

Merged
merged 8 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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