diff --git a/Cargo.lock b/Cargo.lock index 04b3d397bb..31534ad3d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,7 +214,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stellar-contract-env-common" version = "0.0.0" -source = "git+https://github.com/stellar/rs-stellar-contract-env?rev=9a783c26cbc022e0351538a3e9582a58510bc694#9a783c26cbc022e0351538a3e9582a58510bc694" +source = "git+https://github.com/stellar/rs-stellar-contract-env?rev=7c6958bd460e00bc6378b23a133b9ab4736b56c2#7c6958bd460e00bc6378b23a133b9ab4736b56c2" dependencies = [ "static_assertions", "stellar-xdr", @@ -224,7 +224,7 @@ dependencies = [ [[package]] name = "stellar-contract-env-host" version = "0.0.0" -source = "git+https://github.com/stellar/rs-stellar-contract-env?rev=9a783c26cbc022e0351538a3e9582a58510bc694#9a783c26cbc022e0351538a3e9582a58510bc694" +source = "git+https://github.com/stellar/rs-stellar-contract-env?rev=7c6958bd460e00bc6378b23a133b9ab4736b56c2#7c6958bd460e00bc6378b23a133b9ab4736b56c2" dependencies = [ "im-rc", "num-bigint", @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "stellar-xdr" version = "0.0.0" -source = "git+https://github.com/stellar/rs-stellar-xdr?rev=2a8b24c2978303612c49afcf005c1d35c592c97c#2a8b24c2978303612c49afcf005c1d35c592c97c" +source = "git+https://github.com/stellar/rs-stellar-xdr?rev=b2d367f04706af7f21a7a6a7abb4920e18dacadb#b2d367f04706af7f21a7a6a7abb4920e18dacadb" dependencies = [ "base64", ] diff --git a/src/protocol-next/xdr/Stellar-transaction.x b/src/protocol-next/xdr/Stellar-transaction.x index 74d282b8c8..7e915593e1 100644 --- a/src/protocol-next/xdr/Stellar-transaction.x +++ b/src/protocol-next/xdr/Stellar-transaction.x @@ -2,6 +2,7 @@ // under the Apache License, Version 2.0. See the COPYING file at the root // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 +%#include "xdr/Stellar-contract.h" %#include "xdr/Stellar-ledger-entries.h" namespace stellar @@ -64,7 +65,8 @@ enum OperationType CLAWBACK_CLAIMABLE_BALANCE = 20, SET_TRUST_LINE_FLAGS = 21, LIQUIDITY_POOL_DEPOSIT = 22, - LIQUIDITY_POOL_WITHDRAW = 23 + LIQUIDITY_POOL_WITHDRAW = 23, + INVOKE_HOST_FUNCTION = 24 }; /* CreateAccount @@ -472,6 +474,24 @@ struct LiquidityPoolWithdrawOp int64 minAmountB; // minimum amount of second asset to withdraw }; +enum HostFunction +{ + HOST_FN_CALL = 0, + HOST_FN_CREATE_CONTRACT = 1 +}; + +struct InvokeHostFunctionOp +{ + // The host function to invoke + HostFunction function; + + // Parameters to the host function + SCVec parameters; + + // The footprint for this invocation + LedgerFootprint footprint; +}; + /* An operation is the lowest unit of work that a transaction does */ struct Operation { @@ -530,6 +550,8 @@ struct Operation LiquidityPoolDepositOp liquidityPoolDepositOp; case LIQUIDITY_POOL_WITHDRAW: LiquidityPoolWithdrawOp liquidityPoolWithdrawOp; + case INVOKE_HOST_FUNCTION: + InvokeHostFunctionOp invokeHostFunctionOp; } body; }; @@ -1595,6 +1617,25 @@ case LIQUIDITY_POOL_WITHDRAW_UNDER_MINIMUM: void; }; +enum InvokeHostFunctionResultCode +{ + // codes considered as "success" for the operation + INVOKE_HOST_FUNCTION_SUCCESS = 0, + + // codes considered as "failure" for the operation + INVOKE_HOST_FUNCTION_MALFORMED = -1, + INVOKE_HOST_FUNCTION_TRAPPED = -2 +}; + +union InvokeHostFunctionResult switch (InvokeHostFunctionResultCode code) +{ +case INVOKE_HOST_FUNCTION_SUCCESS: + void; +case INVOKE_HOST_FUNCTION_MALFORMED: +case INVOKE_HOST_FUNCTION_TRAPPED: + void; +}; + /* High level Operation Result */ enum OperationResultCode { @@ -1661,6 +1702,8 @@ case opINNER: LiquidityPoolDepositResult liquidityPoolDepositResult; case LIQUIDITY_POOL_WITHDRAW: LiquidityPoolWithdrawResult liquidityPoolWithdrawResult; + case INVOKE_HOST_FUNCTION: + InvokeHostFunctionResult invokeHostFunctionResult; } tr; case opBAD_AUTH: diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 62019acb5e..91dd8d3217 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -12,6 +12,6 @@ log = "0.4.17" cxx = "1.0" im-rc = "15.0.0" base64 = "0.13.0" -stellar-contract-env-host = { git = "https://github.com/stellar/rs-stellar-contract-env", rev = "9a783c26cbc022e0351538a3e9582a58510bc694", features = [ +stellar-contract-env-host = { git = "https://github.com/stellar/rs-stellar-contract-env", rev = "7c6958bd460e00bc6378b23a133b9ab4736b56c2", features = [ "vm", ] } diff --git a/src/rust/src/contract.rs b/src/rust/src/contract.rs index f7ea91473a..4bbae05dfb 100644 --- a/src/rust/src/contract.rs +++ b/src/rust/src/contract.rs @@ -2,39 +2,39 @@ // under the Apache License, Version 2.0. See the COPYING file at the root // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 -use crate::{log::partition::TX, rust_bridge::XDRBuf}; +use crate::{ + log::partition::TX, + rust_bridge::{Bytes, XDRBuf}, +}; use log::info; use std::io::Cursor; -use cxx::CxxString; use im_rc::OrdMap; use std::error::Error; use stellar_contract_env_host::{ - storage::{self, AccessType, Key, Storage}, + storage::{self, AccessType, Storage}, xdr, xdr::{ - ContractDataEntry, Hash, LedgerEntry, LedgerEntryData, LedgerKey, ReadXdr, ScObject, - ScStatic, ScVal, ScVec, WriteXdr, + HostFunction, LedgerEntry, LedgerEntryData, LedgerKey, LedgerKeyAccount, + LedgerKeyContractData, LedgerKeyTrustLine, ReadXdr, ScVec, WriteXdr, }, - Host, HostError, Vm, + Host, HostError, }; /// Helper for [`build_storage_footprint_from_xdr`] that inserts a copy of some /// [`AccessType`] `ty` into a [`storage::Footprint`] for every [`LedgerKey`] in /// `keys`. -fn populate_access_map_with_contract_data_keys( - access: &mut OrdMap, +fn populate_access_map( + access: &mut OrdMap, keys: Vec, ty: AccessType, ) -> Result<(), HostError> { for lk in keys { match lk { - LedgerKey::ContractData(xdr::LedgerKeyContractData { contract_id, key }) => { - let sk = Key { contract_id, key }; - access.insert(sk, ty.clone()); - } - _ => return Err(HostError::General("unexpected ledger key type").into()), - } + LedgerKey::Account(_) | LedgerKey::Trustline(_) | LedgerKey::ContractData(_) => (), + _ => return Err(HostError::General("unexpected ledger entry type")), + }; + access.insert(lk, ty.clone()); } Ok(()) } @@ -49,19 +49,28 @@ fn build_storage_footprint_from_xdr(footprint: &XDRBuf) -> Result Result { + match &le.data { + LedgerEntryData::Account(a) => Ok(LedgerKey::Account(LedgerKeyAccount { + account_id: a.account_id.clone(), + })), + LedgerEntryData::Trustline(tl) => Ok(LedgerKey::Trustline(LedgerKeyTrustLine { + account_id: tl.account_id.clone(), + asset: tl.asset.clone(), + })), + LedgerEntryData::ContractData(cd) => Ok(LedgerKey::ContractData(LedgerKeyContractData { + contract_id: cd.contract_id.clone(), + key: cd.key.clone(), + })), + _ => Err(HostError::General("unexpected ledger key")), + } +} + /// Deserializes a sequence of [`xdr::LedgerEntry`] structures from a vector of /// [`XDRBuf`] buffers, inserts them into an [`OrdMap`] with entries /// additionally keyed by the provided contract ID, checks that the entries @@ -69,92 +78,83 @@ fn build_storage_footprint_from_xdr(footprint: &XDRBuf) -> Result, -) -> Result>, HostError> { +) -> Result>, HostError> { let mut map = OrdMap::new(); for buf in ledger_entries { let le = LedgerEntry::read_xdr(&mut Cursor::new(buf.data.as_slice()))?; - match le.data { - LedgerEntryData::ContractData(ContractDataEntry { - key, - val, - contract_id, - }) => { - let sk = Key { contract_id, key }; - if !footprint.0.contains_key(&sk) { - return Err(HostError::General("ledger entry not found in footprint").into()); - } - map.insert(sk.clone(), Some(val)); - } - _ => return Err(HostError::General("unexpected ledger entry type").into()), + let key = ledger_entry_to_ledger_key(&le)?; + if !footprint.0.contains_key(&key) { + return Err(HostError::General("ledger entry not found in footprint").into()); } + map.insert(key, Some(le)); } for k in footprint.0.keys() { if !map.contains_key(k) { - return Err(HostError::General("ledger entry not found for footprint entry").into()); + map.insert(k.clone(), None); } } Ok(map) } -/// Looks up an [`ScObject`] un the provided map under the ledger key -/// [`ScStatic::LedgerKeyContractCodeWasm`] and returns a copy of its binary -/// data, which should be WASM bytecode. -fn extract_contract_wasm_from_storage_map( - contract_id: &Hash, - map: &OrdMap>, -) -> Result, HostError> { - let wasm_key = Key { - contract_id: contract_id.clone(), - key: ScVal::Static(ScStatic::LedgerKeyContractCodeWasm), - }; - let wasm = match map.get(&wasm_key) { - Some(Some(ScVal::Object(Some(ScObject::Binary(blob))))) => blob.clone(), - Some(_) => { - return Err(HostError::General( - "unexpected value type for LEDGER_KEY_CONTRACT_CODE_WASM", - ) - .into()) - } - None => { - return Err( - HostError::General("missing value for LEDGER_KEY_CONTRACT_CODE_WASM").into(), - ) +/// Iterates over the storage map and serializes the read-write ledger entries +/// back to XDR. +fn build_xdr_ledger_entries_from_storage_map( + footprint: &storage::Footprint, + storage_map: &OrdMap>, +) -> Result, HostError> { + let mut res = Vec::new(); + for (lk, ole) in storage_map { + let mut xdr_buf: Vec = Vec::new(); + match footprint.0.get(lk) { + Some(AccessType::ReadOnly) => (), + Some(AccessType::ReadWrite) => { + if let Some(le) = ole { + le.write_xdr(&mut Cursor::new(&mut xdr_buf))?; + res.push(Bytes { vec: xdr_buf }); + } + } + None => return Err(HostError::General("ledger entry not in footprint")), } - }; - Ok(wasm.to_vec()) + } + Ok(res) } -/// Deserializes an [`xdr::Hash`] contract ID, an [`ScVec`] XDR object of -/// arguments, an [`xdr::LedgerFootprint`] and a sequence of [`xdr::LedgerEntry`] -/// entries containing all the data the contract intends to read. Then loads -/// some WASM bytecode out of the provided ledger entries (keyed under -/// [`ScStatic::LedgerKeyContractCodeWasm`]), instantiates a [`Host`] and [`Vm`] -/// with the provided WASM, invokes the requested function in the WASM, and -/// serializes an [`xdr::ScVal`] back into a return value. -pub(crate) fn invoke_contract( - contract_id: &XDRBuf, - func: &CxxString, - args: &XDRBuf, - footprint: &XDRBuf, +/// Deserializes an [`xdr::HostFunction`] host function identifier, an [`xdr::ScVec`] XDR object of +/// arguments, an [`xdr::Footprint`] and a sequence of [`xdr::LedgerEntry`] entries containing all +/// the data the invocation intends to read. Then calls the host function with the specified +/// arguments, discards the [`xdr::ScVal`] return value, and returns the [`ReadWrite`] ledger +/// entries in serialized form. Ledger entries not returned have been deleted. +pub(crate) fn invoke_host_function( + hf_buf: &XDRBuf, + args_buf: &XDRBuf, + footprint_buf: &XDRBuf, ledger_entries: &Vec, -) -> Result, Box> { - let contract_id = Hash::read_xdr(&mut Cursor::new(contract_id.data.as_slice()))?; - let arg_scvals = ScVec::read_xdr(&mut Cursor::new(args.data.as_slice()))?; +) -> Result, Box> { + let hf = HostFunction::read_xdr(&mut Cursor::new(hf_buf.data.as_slice()))?; + let args = ScVec::read_xdr(&mut Cursor::new(args_buf.data.as_slice()))?; - let footprint = build_storage_footprint_from_xdr(footprint)?; + let footprint = build_storage_footprint_from_xdr(footprint_buf)?; let map = build_storage_map_from_xdr_ledger_entries(&footprint, ledger_entries)?; - let wasm = extract_contract_wasm_from_storage_map(&contract_id, &map)?; - - let func_str = func.to_str()?; let storage = Storage::with_enforcing_footprint_and_map(footprint, map); let mut host = Host::with_storage(storage); - let vm = Vm::new(&host, contract_id, wasm.as_slice())?; - info!(target: TX, "Invoking contract function '{}'", func); - let res = vm.invoke_function(&mut host, func_str, &arg_scvals)?; + match hf { + HostFunction::Call => { + info!(target: TX, "Invoking host function 'Call'"); + host.invoke_function(hf, args)?; + } + HostFunction::CreateContract => { + info!(target: TX, "Invoking host function 'CreateContract'"); + todo!(); + } + }; - let mut ret_xdr_buf: Vec = Vec::new(); - res.write_xdr(&mut Cursor::new(&mut ret_xdr_buf))?; - Ok(ret_xdr_buf) + let storage = host + .try_recover_storage() + .map_err(|_h| HostError::General("could not get storage from host"))?; + Ok(build_xdr_ledger_entries_from_storage_map( + &storage.footprint, + &storage.map, + )?) } diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index e3ad479857..82eec01115 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -17,6 +17,10 @@ mod rust_bridge { data: UniquePtr>, } + struct Bytes { + vec: Vec, + } + // LogLevel declares to cxx.rs a shared type that both Rust and C+++ will // understand. #[namespace = "stellar"] @@ -35,13 +39,12 @@ mod rust_bridge { extern "Rust" { fn to_base64(b: &CxxVector, mut s: Pin<&mut CxxString>); fn from_base64(s: &CxxString, mut b: Pin<&mut CxxVector>); - fn invoke_contract( - contract_id: &XDRBuf, - func: &CxxString, + fn invoke_host_function( + hf_buf: &XDRBuf, args: &XDRBuf, footprint: &XDRBuf, ledger_entries: &Vec, - ) -> Result>; + ) -> Result>; fn init_logging(maxLevel: LogLevel) -> Result<()>; } @@ -64,7 +67,7 @@ mod b64; use b64::{from_base64, to_base64}; mod contract; -use contract::invoke_contract; +use contract::invoke_host_function; mod log; use crate::log::init_logging; diff --git a/src/transactions/InvokeHostFunctionOpFrame.cpp b/src/transactions/InvokeHostFunctionOpFrame.cpp new file mode 100644 index 0000000000..3302d01211 --- /dev/null +++ b/src/transactions/InvokeHostFunctionOpFrame.cpp @@ -0,0 +1,141 @@ +// Copyright 2022 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + +// clang-format off +// This needs to be included first +#include "rust/RustVecXdrMarshal.h" +// clang-format on +#include "transactions/InvokeHostFunctionOpFrame.h" +#include "ledger/LedgerTxn.h" +#include "ledger/LedgerTxnEntry.h" +#include "rust/RustBridge.h" + +namespace stellar +{ + +template +XDRBuf +toXDRBuf(T const& t) +{ + return XDRBuf{ + std::make_unique>(xdr::xdr_to_opaque(t))}; +} + +InvokeHostFunctionOpFrame::InvokeHostFunctionOpFrame(Operation const& op, + OperationResult& res, + TransactionFrame& parentTx) + : OperationFrame(op, res, parentTx) + , mInvokeHostFunction(mOperation.body.invokeHostFunctionOp()) +{ +} + +ThresholdLevel +InvokeHostFunctionOpFrame::getThresholdLevel() const +{ + return ThresholdLevel::LOW; +} + +bool +InvokeHostFunctionOpFrame::isOpSupported(LedgerHeader const& header) const +{ + return header.ledgerVersion >= 20; +} + +bool +InvokeHostFunctionOpFrame::doApply(AbstractLedgerTxn& ltx) +{ + // Get the entries for the footprint + rust::Vec ledgerEntryXdrBufs; + ledgerEntryXdrBufs.reserve(mInvokeHostFunction.footprint.readOnly.size() + + mInvokeHostFunction.footprint.readWrite.size()); + for (auto const& lk : mInvokeHostFunction.footprint.readOnly) + { + // Load without record for readOnly to avoid writing them later + auto ltxe = ltx.loadWithoutRecord(lk); + if (ltxe) + { + ledgerEntryXdrBufs.emplace_back(toXDRBuf(ltxe.current())); + } + } + for (auto const& lk : mInvokeHostFunction.footprint.readWrite) + { + auto ltxe = ltx.load(lk); + if (ltxe) + { + ledgerEntryXdrBufs.emplace_back(toXDRBuf(ltxe.current())); + } + } + + rust::Vec retBufs; + try + { + retBufs = rust_bridge::invoke_host_function( + toXDRBuf(mInvokeHostFunction.function), + toXDRBuf(mInvokeHostFunction.parameters), + toXDRBuf(mInvokeHostFunction.footprint), ledgerEntryXdrBufs); + } + catch (std::exception& e) + { + innerResult().code(INVOKE_HOST_FUNCTION_TRAPPED); + return false; + } + + // Create or update every entry returned + std::unordered_set keys; + for (auto const& buf : retBufs) + { + LedgerEntry le; + xdr::xdr_from_opaque(buf.vec, le); + auto lk = LedgerEntryKey(le); + + auto ltxe = ltx.load(lk); + if (ltxe) + { + ltxe.current() = le; + } + else + { + ltx.create(le); + } + + keys.emplace(std::move(lk)); + } + + // Erase every entry not returned + for (auto const& lk : mInvokeHostFunction.footprint.readWrite) + { + if (keys.find(lk) == keys.end()) + { + auto ltxe = ltx.load(lk); + if (ltxe) + { + ltx.erase(lk); + } + } + } + + innerResult().code(INVOKE_HOST_FUNCTION_SUCCESS); + return true; +} + +bool +InvokeHostFunctionOpFrame::doCheckValid(uint32_t ledgerVersion) +{ + if (mParentTx.getNumOperations() > 1) + { + innerResult().code(INVOKE_HOST_FUNCTION_MALFORMED); + return false; + } + return true; +} + +void +InvokeHostFunctionOpFrame::insertLedgerKeysToPrefetch( + UnorderedSet& keys) const +{ +} +} +#endif // ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION diff --git a/src/transactions/InvokeHostFunctionOpFrame.h b/src/transactions/InvokeHostFunctionOpFrame.h new file mode 100644 index 0000000000..5f4f51ff5f --- /dev/null +++ b/src/transactions/InvokeHostFunctionOpFrame.h @@ -0,0 +1,45 @@ +#pragma once + +// Copyright 2022 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +#include "transactions/OperationFrame.h" + +namespace stellar +{ +class AbstractLedgerTxn; + +class InvokeHostFunctionOpFrame : public OperationFrame +{ + InvokeHostFunctionResult& + innerResult() + { + return mResult.tr().invokeHostFunctionResult(); + } + + InvokeHostFunctionOp const& mInvokeHostFunction; + + public: + InvokeHostFunctionOpFrame(Operation const& op, OperationResult& res, + TransactionFrame& parentTx); + + ThresholdLevel getThresholdLevel() const override; + + bool isOpSupported(LedgerHeader const& header) const override; + + bool doApply(AbstractLedgerTxn& ltx) override; + bool doCheckValid(uint32_t ledgerVersion) override; + + void + insertLedgerKeysToPrefetch(UnorderedSet& keys) const override; + + static InvokeHostFunctionResultCode + getInnerCode(OperationResult const& res) + { + return res.tr().invokeHostFunctionResult().code(); + } +}; +} +#endif // ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION diff --git a/src/transactions/OperationFrame.cpp b/src/transactions/OperationFrame.cpp index cc77947079..e15291c2f3 100644 --- a/src/transactions/OperationFrame.cpp +++ b/src/transactions/OperationFrame.cpp @@ -15,6 +15,7 @@ #include "transactions/CreatePassiveSellOfferOpFrame.h" #include "transactions/EndSponsoringFutureReservesOpFrame.h" #include "transactions/InflationOpFrame.h" +#include "transactions/InvokeHostFunctionOpFrame.h" #include "transactions/LiquidityPoolDepositOpFrame.h" #include "transactions/LiquidityPoolWithdrawOpFrame.h" #include "transactions/ManageBuyOfferOpFrame.h" @@ -113,6 +114,10 @@ OperationFrame::makeHelper(Operation const& op, OperationResult& res, return std::make_shared(op, res, tx); case LIQUIDITY_POOL_WITHDRAW: return std::make_shared(op, res, tx); +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + case INVOKE_HOST_FUNCTION: + return std::make_shared(op, res, tx); +#endif default: ostringstream err; err << "Unknown Tx type: " << op.body.type(); diff --git a/src/transactions/TransactionUtils.h b/src/transactions/TransactionUtils.h index 66189f00fd..92ace7d82a 100644 --- a/src/transactions/TransactionUtils.h +++ b/src/transactions/TransactionUtils.h @@ -140,7 +140,7 @@ LedgerTxnEntry loadLiquidityPool(AbstractLedgerTxn& ltx, PoolID const& poolID); #ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION LedgerTxnEntry loadContractData(AbstractLedgerTxn& ltx, Hash const& contractID, - SCVal dataKey); + SCVal const& dataKey); #endif void acquireLiabilities(AbstractLedgerTxn& ltx, LedgerTxnHeader const& header, diff --git a/src/transactions/contracts/ContractUtils.cpp b/src/transactions/contracts/ContractUtils.cpp deleted file mode 100644 index 3d1d8d605b..0000000000 --- a/src/transactions/contracts/ContractUtils.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2022 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION - -#include "transactions/contracts/ContractUtils.h" -#include "crypto/Hex.h" -#include "rust/RustBridge.h" -#include "rust/RustVecXdrMarshal.h" -#include "util/Logging.h" -#include "util/XDROperators.h" -#include "util/types.h" -#include "xdr/Stellar-contract.h" -#include "xdr/Stellar-transaction.h" - -#include - -namespace stellar -{ - -template -XDRBuf -toXDRBuf(T const& t) -{ - return XDRBuf{ - std::make_unique>(xdr::xdr_to_opaque(t))}; -} - -// Invoke a contract with a given function name and args vector, where -// ledgerEntries is a vector of buffers each of which contains a ledger entry in -// the footprint of the contract. The keys in footprint and ledgerEntries -// must be the same, and ledgerEntries must contain an entry -// -// { -// contract_id, -// key: ScVal::Static(ScStatic::LedgerKeyContractCodeWasm) -// val: ScVal::Object(Some(ScObject::Binary(blob))) -// } -// -// which contains the WASM blob that will be run. -SCVal -invokeContract(Hash const& contract_id, std::string const& funcName, - SCVec const& args, LedgerFootprint const& footprint, - std::vector>> ledgerEntries) -{ - ZoneScoped; - rust::Vec xdrBufs; - xdrBufs.reserve(ledgerEntries.size()); - for (auto& p : ledgerEntries) - { - xdrBufs.push_back(XDRBuf{std::move(p)}); - } - rust::Vec retBuf = rust_bridge::invoke_contract( - toXDRBuf(contract_id), funcName, toXDRBuf(args), toXDRBuf(footprint), - xdrBufs); - SCVal ret; - xdr::xdr_from_opaque(retBuf, ret); - return ret; -} - -} - -#endif diff --git a/src/transactions/contracts/ContractUtils.h b/src/transactions/contracts/ContractUtils.h deleted file mode 100644 index 42c1c7200d..0000000000 --- a/src/transactions/contracts/ContractUtils.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -// Copyright 2022 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION - -#include "xdr/Stellar-contract.h" -#include "xdr/Stellar-transaction.h" -#include -#include -#include - -namespace stellar -{ -SCVal invokeContract( - Hash const& contract_id, std::string const& funcName, SCVec const& args, - LedgerFootprint const& footprint, - std::vector>> ledgerEntries); -} -#endif diff --git a/src/transactions/test/ContractTests.cpp b/src/transactions/test/ContractTests.cpp deleted file mode 100644 index eb4ef30f27..0000000000 --- a/src/transactions/test/ContractTests.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2022 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION - -#include "crypto/SecretKey.h" -#include "ledger/LedgerTxn.h" -#include "lib/catch.hpp" -#include "rust/RustBridge.h" -#include "test/test.h" -#include "xdr/Stellar-ledger-entries.h" -#include -#include -#include -#include -#include - -#include "transactions/contracts/ContractUtils.h" -#include "xdr/Stellar-contract.h" - -using namespace stellar; - -// This is an example WASM from the SDK that unpacks two SCV_I32 arguments, adds -// them with an overflow check, and re-packs them as an SCV_I32 if successful. -// -// To regenerate, check out the SDK, install a nightly toolchain with -// the rust-src component (to enable the 'tiny' build) using the following: -// -// $ rustup component add rust-src --toolchain nightly -// -// then do: -// -// $ make tiny -// $ xxd -i target/wasm32-unknown-unknown/release/example_add_i32.wasm - -std::vector add_i32_wasm{ - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60, - 0x02, 0x7e, 0x7e, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01, - 0x00, 0x10, 0x06, 0x11, 0x02, 0x7f, 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00, - 0x0b, 0x7f, 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x07, 0x2b, 0x04, - 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x03, 0x61, 0x64, - 0x64, 0x00, 0x00, 0x0a, 0x5f, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x65, - 0x6e, 0x64, 0x03, 0x00, 0x0b, 0x5f, 0x5f, 0x68, 0x65, 0x61, 0x70, 0x5f, - 0x62, 0x61, 0x73, 0x65, 0x03, 0x01, 0x0a, 0x47, 0x01, 0x45, 0x01, 0x02, - 0x7f, 0x02, 0x40, 0x20, 0x00, 0x42, 0x0f, 0x83, 0x42, 0x03, 0x52, 0x20, - 0x01, 0x42, 0x0f, 0x83, 0x42, 0x03, 0x52, 0x72, 0x45, 0x04, 0x40, 0x20, - 0x01, 0x42, 0x04, 0x88, 0xa7, 0x22, 0x02, 0x41, 0x00, 0x48, 0x20, 0x02, - 0x20, 0x00, 0x42, 0x04, 0x88, 0xa7, 0x22, 0x03, 0x6a, 0x22, 0x02, 0x20, - 0x03, 0x48, 0x73, 0x45, 0x0d, 0x01, 0x0b, 0x00, 0x0b, 0x20, 0x02, 0xad, - 0x42, 0x04, 0x86, 0x42, 0x03, 0x84, 0x0b}; - -TEST_CASE("WASM test", "[wasm]") -{ - SCVal x, y; - - x.type(SCV_I32); - x.i32() = 5; - - y.type(SCV_I32); - y.i32() = 7; - - SCVec args{x, y}; - - namespace ch = std::chrono; - using clock = ch::high_resolution_clock; - using usec = ch::microseconds; - - Hash contract_id = HashUtils::pseudoRandomForTesting(); - - LedgerFootprint footprint; - LedgerEntry wasm_le; - LedgerKey wasm_lk; - SCVal wasm_key; - SCVal wasm_val; - - wasm_key.type(SCValType::SCV_STATIC); - wasm_key.ic() = SCStatic::SCS_LEDGER_KEY_CONTRACT_CODE_WASM; - - wasm_val.type(SCValType::SCV_OBJECT); - wasm_val.obj().activate(); - wasm_val.obj()->type(SCO_BINARY); - wasm_val.obj()->bin().assign(add_i32_wasm.begin(), add_i32_wasm.end()); - - wasm_lk.type(CONTRACT_DATA); - wasm_lk.contractData().contractID = contract_id; - wasm_lk.contractData().key = wasm_key; - - wasm_le.data.type(CONTRACT_DATA); - wasm_le.data.contractData().contractID = contract_id; - wasm_le.data.contractData().key = wasm_key; - wasm_le.data.contractData().val = wasm_val; - - footprint.readOnly.emplace_back(wasm_lk); - - std::unique_ptr> wasm_xdr_ptr = - std::make_unique>(xdr::xdr_to_opaque(wasm_le)); - std::vector>> xdr_buffers; - xdr_buffers.emplace_back(std::move(wasm_xdr_ptr)); - - auto begin = clock::now(); - auto res = invokeContract(contract_id, "add", args, footprint, - std::move(xdr_buffers)); - auto end = clock::now(); - - auto us = ch::duration_cast(end - begin); - - REQUIRE(res.type() == SCV_I32); - REQUIRE(res.i32() == x.i32() + y.i32()); - - LOG_INFO(DEFAULT_LOG, "calculated {} + {} = {} in {} usecs", x.i32(), - y.i32(), res.i32(), us.count()); -} - -#endif \ No newline at end of file diff --git a/src/transactions/test/InvokeHostFunctionTests.cpp b/src/transactions/test/InvokeHostFunctionTests.cpp new file mode 100644 index 0000000000..9f3fe09dad --- /dev/null +++ b/src/transactions/test/InvokeHostFunctionTests.cpp @@ -0,0 +1,310 @@ +// Copyright 2022 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + +#include "crypto/SecretKey.h" +#include "ledger/LedgerTxn.h" +#include "lib/catch.hpp" +#include "main/Application.h" +#include "rust/RustBridge.h" +#include "test/TestAccount.h" +#include "test/TestUtils.h" +#include "test/TxTests.h" +#include "test/test.h" +#include "transactions/TransactionUtils.h" +#include "xdr/Stellar-contract.h" +#include "xdr/Stellar-ledger-entries.h" +#include +#include +#include +#include +#include + +using namespace stellar; +using namespace stellar::txtest; + +// This is an example WASM from the SDK that unpacks two SCV_I32 arguments, adds +// them with an overflow check, and re-packs them as an SCV_I32 if successful. +// +// To regenerate, check out the SDK, install a nightly toolchain with +// the rust-src component (to enable the 'tiny' build) using the following: +// +// $ rustup component add rust-src --toolchain nightly +// +// then do: +// +// $ make tiny +// $ xxd -i target/wasm32-unknown-unknown/release/example_add_i32.wasm +std::vector addI32Wasm{ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60, + 0x02, 0x7e, 0x7e, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01, + 0x00, 0x10, 0x06, 0x11, 0x02, 0x7f, 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00, + 0x0b, 0x7f, 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x07, 0x2b, 0x04, + 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x03, 0x61, 0x64, + 0x64, 0x00, 0x00, 0x0a, 0x5f, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x65, + 0x6e, 0x64, 0x03, 0x00, 0x0b, 0x5f, 0x5f, 0x68, 0x65, 0x61, 0x70, 0x5f, + 0x62, 0x61, 0x73, 0x65, 0x03, 0x01, 0x0a, 0x47, 0x01, 0x45, 0x01, 0x02, + 0x7f, 0x02, 0x40, 0x20, 0x00, 0x42, 0x0f, 0x83, 0x42, 0x03, 0x52, 0x20, + 0x01, 0x42, 0x0f, 0x83, 0x42, 0x03, 0x52, 0x72, 0x45, 0x04, 0x40, 0x20, + 0x01, 0x42, 0x04, 0x88, 0xa7, 0x22, 0x02, 0x41, 0x00, 0x48, 0x20, 0x02, + 0x20, 0x00, 0x42, 0x04, 0x88, 0xa7, 0x22, 0x03, 0x6a, 0x22, 0x02, 0x20, + 0x03, 0x48, 0x73, 0x45, 0x0d, 0x01, 0x0b, 0x00, 0x0b, 0x20, 0x02, 0xad, + 0x42, 0x04, 0x86, 0x42, 0x03, 0x84, 0x0b}; + +// This is an example WASM from the SDK that can put and delete arbitrary +// contract data. +std::vector contractDataWasm{ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0c, 0x02, 0x60, + 0x02, 0x7e, 0x7e, 0x01, 0x7e, 0x60, 0x01, 0x7e, 0x01, 0x7e, 0x02, 0x0f, + 0x02, 0x01, 0x6c, 0x02, 0x24, 0x5f, 0x00, 0x00, 0x01, 0x6c, 0x02, 0x24, + 0x32, 0x00, 0x01, 0x03, 0x03, 0x02, 0x00, 0x01, 0x05, 0x03, 0x01, 0x00, + 0x10, 0x06, 0x19, 0x03, 0x7f, 0x01, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, + 0x7f, 0x00, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x7f, 0x00, 0x41, 0x80, + 0x80, 0xc0, 0x00, 0x0b, 0x07, 0x31, 0x05, 0x06, 0x6d, 0x65, 0x6d, 0x6f, + 0x72, 0x79, 0x02, 0x00, 0x03, 0x70, 0x75, 0x74, 0x00, 0x02, 0x03, 0x64, + 0x65, 0x6c, 0x00, 0x03, 0x0a, 0x5f, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, + 0x65, 0x6e, 0x64, 0x03, 0x01, 0x0b, 0x5f, 0x5f, 0x68, 0x65, 0x61, 0x70, + 0x5f, 0x62, 0x61, 0x73, 0x65, 0x03, 0x02, 0x0a, 0x35, 0x02, 0x1a, 0x01, + 0x01, 0x7f, 0x23, 0x00, 0x41, 0x10, 0x6b, 0x22, 0x02, 0x24, 0x00, 0x20, + 0x00, 0x20, 0x01, 0x10, 0x00, 0x20, 0x02, 0x41, 0x10, 0x6a, 0x24, 0x00, + 0x0b, 0x18, 0x01, 0x01, 0x7f, 0x23, 0x00, 0x41, 0x10, 0x6b, 0x22, 0x01, + 0x24, 0x00, 0x20, 0x00, 0x10, 0x01, 0x20, 0x01, 0x41, 0x10, 0x6a, 0x24, + 0x00, 0x0b}; + +template +SCVal +makeBinary(T begin, T end) +{ + SCVal val(SCValType::SCV_OBJECT); + val.obj().activate().type(SCO_BINARY); + val.obj()->bin().assign(begin, end); + return val; +} + +static SCVal +makeI32(int32_t i32) +{ + SCVal val(SCV_I32); + val.i32() = i32; + return val; +} + +static SCVal +makeSymbol(std::string const& str) +{ + SCVal val(SCV_SYMBOL); + val.sym().assign(str.begin(), str.end()); + return val; +} + +template +static LedgerKey +deployContract(AbstractLedgerTxn& ltx, T begin, T end) +{ + SCVal wasmKey(SCValType::SCV_STATIC); + wasmKey.ic() = SCStatic::SCS_LEDGER_KEY_CONTRACT_CODE_WASM; + + LedgerEntry le; + le.data.type(CONTRACT_DATA); + le.data.contractData().contractID = HashUtils::pseudoRandomForTesting(); + le.data.contractData().key = wasmKey; + le.data.contractData().val = makeBinary(begin, end); + + ltx.create(le); + return LedgerEntryKey(le); +} + +template +static LedgerKey +deployContract(Application& app, T begin, T end) +{ + LedgerTxn ltx(app.getLedgerTxnRoot()); + auto contract = deployContract(ltx, begin, end); + ltx.commit(); + return contract; +} + +TEST_CASE("invoke host function", "[tx][contract]") +{ + VirtualClock clock; + auto app = createTestApplication(clock, getTestConfig()); + auto root = TestAccount::createRoot(*app); + + SECTION("add i32") + { + auto contract = + deployContract(*app, addI32Wasm.begin(), addI32Wasm.end()); + auto const& contractID = contract.contractData().contractID; + + auto call = [&](SCVec const& parameters, bool success) { + Operation op; + op.body.type(INVOKE_HOST_FUNCTION); + auto& ihf = op.body.invokeHostFunctionOp(); + ihf.function = HOST_FN_CALL; + ihf.parameters = parameters; + ihf.footprint.readOnly = {contract}; + + auto tx = + transactionFrameFromOps(app->getNetworkID(), root, {op}, {}); + LedgerTxn ltx(app->getLedgerTxnRoot()); + TransactionMeta txm(2); + REQUIRE(tx->checkValid(ltx, 0, 0, 0)); + if (success) + { + REQUIRE(tx->apply(*app, ltx, txm)); + } + else + { + REQUIRE(!tx->apply(*app, ltx, txm)); + } + ltx.commit(); + }; + + auto scContractID = makeBinary(contractID.begin(), contractID.end()); + auto scFunc = makeSymbol("add"); + auto sc7 = makeI32(7); + auto sc16 = makeI32(16); + + // Too few parameters for call + call({}, false); + call({scContractID}, false); + + // To few parameters for "add" + call({scContractID, scFunc}, false); + call({scContractID, scFunc, sc7}, false); + + // Correct function call + call({scContractID, scFunc, sc7, sc16}, true); + + // Too many parameters for "add" + call({scContractID, scFunc, sc7, sc16, makeI32(0)}, false); + } + + SECTION("contract data") + { + auto contract = deployContract(*app, contractDataWasm.begin(), + contractDataWasm.end()); + auto const& contractID = contract.contractData().contractID; + + auto checkContractData = [&](SCVal const& key, SCVal const* val) { + LedgerTxn ltx(app->getLedgerTxnRoot()); + auto ltxe = loadContractData(ltx, contractID, key); + if (val) + { + REQUIRE(ltxe); + REQUIRE(ltxe.current().data.contractData().val == *val); + } + else + { + REQUIRE(!ltxe); + } + }; + + auto putWithFootprint = [&](std::string const& key, + std::string const& val, + xdr::xvector const& readOnly, + xdr::xvector const& readWrite, + bool success) { + auto keySymbol = makeSymbol(key); + auto valSymbol = makeSymbol(val); + + Operation op; + op.body.type(INVOKE_HOST_FUNCTION); + auto& ihf = op.body.invokeHostFunctionOp(); + ihf.function = HOST_FN_CALL; + ihf.parameters.emplace_back( + makeBinary(contractID.begin(), contractID.end())); + ihf.parameters.emplace_back(makeSymbol("put")); + ihf.parameters.emplace_back(keySymbol); + ihf.parameters.emplace_back(valSymbol); + ihf.footprint.readOnly = readOnly; + ihf.footprint.readWrite = readWrite; + + auto tx = + transactionFrameFromOps(app->getNetworkID(), root, {op}, {}); + LedgerTxn ltx(app->getLedgerTxnRoot()); + TransactionMeta txm(2); + REQUIRE(tx->checkValid(ltx, 0, 0, 0)); + if (success) + { + REQUIRE(tx->apply(*app, ltx, txm)); + ltx.commit(); + checkContractData(keySymbol, &valSymbol); + } + else + { + REQUIRE(!tx->apply(*app, ltx, txm)); + ltx.commit(); + } + }; + + auto put = [&](std::string const& key, std::string const& val) { + putWithFootprint(key, val, {contract}, + {contractDataKey(contractID, makeSymbol(key))}, + true); + }; + + auto delWithFootprint = [&](std::string const& key, + xdr::xvector const& readOnly, + xdr::xvector const& readWrite, + bool success) { + auto keySymbol = makeSymbol(key); + + Operation op; + op.body.type(INVOKE_HOST_FUNCTION); + auto& ihf = op.body.invokeHostFunctionOp(); + ihf.function = HOST_FN_CALL; + ihf.parameters.emplace_back( + makeBinary(contractID.begin(), contractID.end())); + ihf.parameters.emplace_back(makeSymbol("del")); + ihf.parameters.emplace_back(keySymbol); + ihf.footprint.readOnly = readOnly; + ihf.footprint.readWrite = readWrite; + + auto tx = + transactionFrameFromOps(app->getNetworkID(), root, {op}, {}); + LedgerTxn ltx(app->getLedgerTxnRoot()); + TransactionMeta txm(2); + REQUIRE(tx->checkValid(ltx, 0, 0, 0)); + if (success) + { + REQUIRE(tx->apply(*app, ltx, txm)); + ltx.commit(); + checkContractData(keySymbol, nullptr); + } + else + { + REQUIRE(!tx->apply(*app, ltx, txm)); + ltx.commit(); + } + }; + + auto del = [&](std::string const& key) { + delWithFootprint(key, {contract}, + {contractDataKey(contractID, makeSymbol(key))}, + true); + }; + + put("key1", "val1a"); + put("key2", "val2a"); + + // Failure: contract data isn't in footprint + putWithFootprint("key1", "val1b", {contract}, {}, false); + delWithFootprint("key1", {contract}, {}, false); + + // Failure: contract data is read only + auto cdk = contractDataKey(contractID, makeSymbol("key2")); + putWithFootprint("key2", "val2b", {contract, cdk}, {}, false); + delWithFootprint("key2", {contract, cdk}, {}, false); + + put("key1", "val1c"); + put("key2", "val2c"); + + del("key1"); + del("key2"); + } +} + +#endif