Skip to content

Commit

Permalink
Merge pull request #3703 from sisuresh/conservation
Browse files Browse the repository at this point in the history
Update ConservationOfLumens invariant to handle the SAC transfers

Reviewed-by: dmkozh
  • Loading branch information
latobarita committed Apr 27, 2023
2 parents 7e92cf9 + c0d2409 commit aa0d348
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 2 deletions.
120 changes: 118 additions & 2 deletions src/invariant/ConservationOfLumens.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

#include "invariant/ConservationOfLumens.h"
#include "crypto/SHA.h"
#include "invariant/InvariantManager.h"
#include "ledger/LedgerTxn.h"
#include "main/Application.h"
Expand All @@ -13,8 +14,15 @@
namespace stellar
{

#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
static int64_t
calculateDeltaBalance(Hash const& lumenContractID, SCVal const& balanceSymbol,
SCVal const& amountSymbol, LedgerEntry const* current,
LedgerEntry const* previous)
#else
static int64_t
calculateDeltaBalance(LedgerEntry const* current, LedgerEntry const* previous)
#endif
{
releaseAssert(current || previous);
auto let = current ? current->data.type() : previous->data.type();
Expand Down Expand Up @@ -71,7 +79,54 @@ calculateDeltaBalance(LedgerEntry const* current, LedgerEntry const* previous)
}
#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
case CONTRACT_DATA:
break;
{
auto const& contractData = current ? current->data.contractData()
: previous->data.contractData();
if (contractData.contractID != lumenContractID ||
contractData.key.type() != SCV_VEC || !contractData.key.vec() ||
contractData.key.vec().size() == 0)
{
return 0;
}

// The balanceSymbol should be the first entry in the SCVec
if (!(contractData.key.vec()->at(0) == balanceSymbol))
{
return 0;
}

auto getAmount = [&amountSymbol](LedgerEntry const* entry) -> int64_t {
if (!entry)
{
return 0;
}

// The amount should be the first entry in the SCMap
auto const& val = entry->data.contractData().val;
if (val.type() == SCV_MAP && val.map() && val.map()->size() != 0)
{
auto const& amountEntry = val.map()->at(0);
if (amountEntry.key == amountSymbol)
{
if (amountEntry.val.type() == SCV_I128)
{
auto lo = amountEntry.val.i128().lo;
auto hi = amountEntry.val.i128().hi;
if (lo > INT64_MAX || hi > 0)
{
// The amount isn't right, but it'll trigger the
// invariant.
return INT64_MAX;
}
return static_cast<int64_t>(lo);
}
}
}
return 0;
};

return getAmount(current) - getAmount(previous);
}
case CONTRACT_CODE:
break;
case CONFIG_SETTING:
Expand All @@ -81,30 +136,85 @@ calculateDeltaBalance(LedgerEntry const* current, LedgerEntry const* previous)
return 0;
}

#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
static int64_t
calculateDeltaBalance(
Hash const& lumenContractID, SCVal const& balanceSymbol,
SCVal const& amountSymbol,
std::shared_ptr<InternalLedgerEntry const> const& genCurrent,
std::shared_ptr<InternalLedgerEntry const> const& genPrevious)
#else
static int64_t
calculateDeltaBalance(
std::shared_ptr<InternalLedgerEntry const> const& genCurrent,
std::shared_ptr<InternalLedgerEntry const> const& genPrevious)
#endif
{
auto type = genCurrent ? genCurrent->type() : genPrevious->type();
if (type == InternalLedgerEntryType::LEDGER_ENTRY)
{
auto const* current = genCurrent ? &genCurrent->ledgerEntry() : nullptr;
auto const* previous =
genPrevious ? &genPrevious->ledgerEntry() : nullptr;

#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
return calculateDeltaBalance(lumenContractID, balanceSymbol,
amountSymbol, current, previous);
#else
return calculateDeltaBalance(current, previous);
#endif
}
return 0;
}

#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
ConservationOfLumens::ConservationOfLumens(Hash const& lumenContractID,
SCVal const& balanceSymbol,
SCVal const& amountSymbol)
: Invariant(false)
, mLumenContractID(lumenContractID)
, mBalanceSymbol(balanceSymbol)
, mAmountSymbol(amountSymbol)
{
}
#else
ConservationOfLumens::ConservationOfLumens() : Invariant(false)
{
}
#endif

std::shared_ptr<Invariant>
ConservationOfLumens::registerInvariant(Application& app)
{
#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
// We need to keep track of lumens in the Stellar Asset Contract, so
// calculate the lumen contractID the key of the Balance entry, and the
// amount field within that entry.

// Calculate contractID
HashIDPreimage preImage;
preImage.type(ENVELOPE_TYPE_CONTRACT_ID_FROM_ASSET);
preImage.fromAsset().networkID = sha256(app.getConfig().NETWORK_PASSPHRASE);

Asset native;
native.type(ASSET_TYPE_NATIVE);
preImage.fromAsset().asset = native;

auto lumenContractID = xdrSha256(preImage);

// Calculate SCVal for balance key
SCVal balanceSymbol(SCV_SYMBOL);
balanceSymbol.sym() = "Balance";

// Calculate SCVal for amount key
SCVal amountSymbol(SCV_SYMBOL);
amountSymbol.sym() = "amount";

return app.getInvariantManager().registerInvariant<ConservationOfLumens>(
lumenContractID, balanceSymbol, amountSymbol);
#else
return app.getInvariantManager().registerInvariant<ConservationOfLumens>();
#endif
}

std::string
Expand All @@ -125,9 +235,15 @@ ConservationOfLumens::checkOnOperationApply(Operation const& operation,
int64_t deltaFeePool = lhCurr.feePool - lhPrev.feePool;
int64_t deltaBalances = std::accumulate(
ltxDelta.entry.begin(), ltxDelta.entry.end(), static_cast<int64_t>(0),
[](int64_t lhs, decltype(ltxDelta.entry)::value_type const& rhs) {
[this](int64_t lhs, decltype(ltxDelta.entry)::value_type const& rhs) {
#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
return lhs + stellar::calculateDeltaBalance(
mLumenContractID, mBalanceSymbol, mAmountSymbol,
rhs.second.current, rhs.second.previous);
#else
return lhs + stellar::calculateDeltaBalance(rhs.second.current,
rhs.second.previous);
#endif
});

if (result.tr().type() == INFLATION)
Expand Down
13 changes: 13 additions & 0 deletions src/invariant/ConservationOfLumens.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

#include "invariant/Invariant.h"
#include "main/Config.h"
#include <memory>

namespace stellar
Expand All @@ -20,7 +21,12 @@ struct LedgerTxnDelta;
class ConservationOfLumens : public Invariant
{
public:
#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
ConservationOfLumens(Hash const& lumenContractID,
SCVal const& balanceSymbol, SCVal const& amountSymbol);
#else
ConservationOfLumens();
#endif

static std::shared_ptr<Invariant> registerInvariant(Application& app);

Expand All @@ -30,5 +36,12 @@ class ConservationOfLumens : public Invariant
checkOnOperationApply(Operation const& operation,
OperationResult const& result,
LedgerTxnDelta const& ltxDelta) override;

private:
#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
Hash const mLumenContractID;
SCVal const mBalanceSymbol;
SCVal const mAmountSymbol;
#endif
};
}
151 changes: 151 additions & 0 deletions src/transactions/test/InvokeHostFunctionTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "transactions/InvokeHostFunctionOpFrame.h"
#include "transactions/SignatureUtils.h"
#include "transactions/TransactionUtils.h"
#include "util/Decoder.h"
#include "util/XDRCereal.h"
#include "xdr/Stellar-contract.h"
#include "xdr/Stellar-ledger-entries.h"
Expand Down Expand Up @@ -67,6 +68,18 @@ makeU64(uint64_t u64)
return val;
}

static SCVal
makeI128(uint64_t u64)
{
Int128Parts p;
p.hi = 0;
p.lo = u64;

SCVal val(SCV_I128);
val.i128() = p;
return val;
}

static SCVal
makeSymbol(std::string const& str)
{
Expand Down Expand Up @@ -558,4 +571,142 @@ TEST_CASE("complex contract", "[tx][contract]")
}
}

TEST_CASE("Stellar asset contract XLM transfer", "[tx][contract]")
{
VirtualClock clock;
auto app = createTestApplication(clock, getTestConfig());
auto root = TestAccount::createRoot(*app);
auto xlm = txtest::makeNativeAsset();

// Create XLM contract
HashIDPreimage preImage;
preImage.type(ENVELOPE_TYPE_CONTRACT_ID_FROM_ASSET);
preImage.fromAsset().asset = xlm;
preImage.fromAsset().networkID = app->getNetworkID();
auto contractID = xdrSha256(preImage);

Operation createOp;
createOp.body.type(INVOKE_HOST_FUNCTION);
auto& createHF = createOp.body.invokeHostFunctionOp();
createHF.function.type(HOST_FUNCTION_TYPE_CREATE_CONTRACT);
auto& createContractArgs = createHF.function.createContractArgs();

SCContractExecutable exec;
exec.type(SCCONTRACT_EXECUTABLE_TOKEN);
createContractArgs.contractID.type(CONTRACT_ID_FROM_ASSET);
createContractArgs.contractID.asset() = xlm;
createContractArgs.source = exec;

{
LedgerFootprint lfp1;
auto key1 = LedgerKey(CONTRACT_DATA);
key1.contractData().contractID = contractID;
key1.contractData().key = makeSymbol("METADATA");

auto key2 = LedgerKey(CONTRACT_DATA);
key2.contractData().contractID = contractID;
SCVec vec = {makeSymbol("AssetInfo")};
SCVal vecKey(SCValType::SCV_VEC);
vecKey.vec().activate() = vec;
key2.contractData().key = vecKey;

SCVal scContractSourceRefKey(
SCValType::SCV_LEDGER_KEY_CONTRACT_EXECUTABLE);
auto key3 = LedgerKey(CONTRACT_DATA);
key3.contractData().contractID = contractID;
key3.contractData().key = scContractSourceRefKey;

lfp1.readWrite = {key1, key2, key3};
createHF.footprint = lfp1;
}

{
// submit operation
auto tx =
transactionFrameFromOps(app->getNetworkID(), root, {createOp}, {});

LedgerTxn ltx(app->getLedgerTxnRoot());
TransactionMetaFrame txm(ltx.loadHeader().current().ledgerVersion);
REQUIRE(tx->checkValid(ltx, 0, 0, 0));
REQUIRE(tx->apply(*app, ltx, txm));
ltx.commit();
}

// now transfer 10 XLM from root to contractID
auto scContractID = makeBinary(contractID.begin(), contractID.end());

SCAddress fromAccount(SC_ADDRESS_TYPE_ACCOUNT);
fromAccount.accountId() = root.getPublicKey();
SCVal from(SCV_ADDRESS);
from.address() = fromAccount;

SCAddress toContract(SC_ADDRESS_TYPE_CONTRACT);
toContract.contractId() = sha256("contract");
SCVal to(SCV_ADDRESS);
to.address() = toContract;

auto fn = makeSymbol("transfer");
Operation transfer;
transfer.body.type(INVOKE_HOST_FUNCTION);
auto& ihf = transfer.body.invokeHostFunctionOp();
ihf.function.type(HOST_FUNCTION_TYPE_INVOKE_CONTRACT);
ihf.function.invokeArgs() = {scContractID, fn, from, to, makeI128(10)};

{
LedgerFootprint lfp2;
// in the second op, we read what was written
lfp2.readOnly = createHF.footprint.readWrite;

LedgerKey accountKey(ACCOUNT);
accountKey.account().accountID = root.getPublicKey();

// build balance key
auto key2 = LedgerKey(CONTRACT_DATA);
key2.contractData().contractID = contractID;

SCVec vec = {makeSymbol("Balance"), to};
SCVal balanceKey(SCValType::SCV_VEC);
balanceKey.vec().activate() = vec;
key2.contractData().key = balanceKey;

SCNonceKey nonce;
nonce.nonce_address = fromAccount;
SCVal nonceKey(SCV_LEDGER_KEY_NONCE);
nonceKey.nonce_key() = nonce;

// build nonce key
auto key3 = LedgerKey(CONTRACT_DATA);
key3.contractData().contractID = contractID;
key3.contractData().key = nonceKey;

lfp2.readWrite = {accountKey, key2, key3};

ihf.footprint = lfp2;
}

// build auth
AuthorizedInvocation ai;
ai.contractID = contractID;
ai.functionName = fn.sym();
ai.args = {from, to, makeI128(10)};

ContractAuth a;
a.rootInvocation = ai;
ihf.auth = {a};

// This will fail in the ConservationOfLumens invariant if the transfer is
// not accounted for.
{
// submit operation
auto tx =
transactionFrameFromOps(app->getNetworkID(), root, {transfer}, {});

LedgerTxn ltx(app->getLedgerTxnRoot());
TransactionMetaFrame txm(ltx.loadHeader().current().ledgerVersion);
REQUIRE(tx->checkValid(ltx, 0, 0, 0));
REQUIRE(tx->apply(*app, ltx, txm));
ltx.commit();
}
}

#endif

0 comments on commit aa0d348

Please sign in to comment.