From 770cd8308b91390830e440bbdac5f47faec0301c Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:52:50 -0700 Subject: [PATCH 01/21] bump protocol version --- src/main/Config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/Config.cpp b/src/main/Config.cpp index fffd08d23b..b577900006 100644 --- a/src/main/Config.cpp +++ b/src/main/Config.cpp @@ -25,7 +25,7 @@ namespace stellar { -const uint32 Config::CURRENT_LEDGER_PROTOCOL_VERSION = 13; +const uint32 Config::CURRENT_LEDGER_PROTOCOL_VERSION = 14; const uint32 Config::MAXIMUM_QUORUM_NESTING_LEVEL = 4; // Options that must only be used for testing From bba5707ab0646c342d46ce048d2fbedc1aca6332 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Tue, 30 Jun 2020 12:52:54 -0700 Subject: [PATCH 02/21] Add new LedgerEntry and LedgerKey types for ClaimableBalance --- src/xdr/Stellar-ledger-entries.x | 91 +++++++++++++++++++++++++++++++- src/xdr/Stellar-ledger.x | 6 +++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/xdr/Stellar-ledger-entries.x b/src/xdr/Stellar-ledger-entries.x index 433d4b6560..9d6a1f4183 100644 --- a/src/xdr/Stellar-ledger-entries.x +++ b/src/xdr/Stellar-ledger-entries.x @@ -78,7 +78,8 @@ enum LedgerEntryType ACCOUNT = 0, TRUSTLINE = 1, OFFER = 2, - DATA = 3 + DATA = 3, + CLAIMABLE_BALANCE = 4 }; struct Signer @@ -260,6 +261,92 @@ struct DataEntry ext; }; +enum ClaimPredicateType +{ + CLAIM_PREDICATE_UNCONDITIONAL = 0, + CLAIM_PREDICATE_AND = 1, + CLAIM_PREDICATE_OR = 2, + CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME = 3, + CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME = 4, + CLAIM_PREDICATE_BEFORE_RELATIVE_TIME = 5, + CLAIM_PREDICATE_AFTER_RELATIVE_TIME = 6 +}; + +union ClaimPredicate switch (ClaimPredicateType type) +{ +case CLAIM_PREDICATE_UNCONDITIONAL: + void; +case CLAIM_PREDICATE_AND: + ClaimPredicate andPredicates<2>; +case CLAIM_PREDICATE_OR: + ClaimPredicate orPredicates<2>; +case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME: + int64 absBefore; // Predicate will be true if closeTime < absBefore +case CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME: + int64 absAfter; // Predicate will be true if closeTime >= absAfter +case CLAIM_PREDICATE_BEFORE_RELATIVE_TIME: + int64 relBefore; // Seconds since closeTime of the ledger in which the + // ClaimableBalanceEntry was created +case CLAIM_PREDICATE_AFTER_RELATIVE_TIME: + int64 relAfter; // Seconds since closeTime of the ledger in which the + // ClaimableBalanceEntry was created +}; + +enum ClaimantType +{ + CLAIMANT_TYPE_V0 = 0 +}; + +union Claimant switch (ClaimantType type) +{ +case CLAIMANT_TYPE_V0: + struct + { + AccountID destination; // The account that can use this condition + ClaimPredicate predicate; // Claimable if predicate is true + } v0; +}; + +enum ClaimableBalanceIDType +{ + CLAIMABLE_BALANCE_ID_TYPE_V0 = 0 +}; + +union ClaimableBalanceID switch (ClaimableBalanceIDType type) +{ +case CLAIMABLE_BALANCE_ID_TYPE_V0: + Hash v0; +}; + +struct ClaimableBalanceEntry +{ + // Unique identifier for this ClaimableBalanceEntry + ClaimableBalanceID balanceID; + + // Account that created this ClaimableBalanceEntry + AccountID createdBy; + + // List of claimants with associated predicate + Claimant claimants<10>; + + // Any asset including native + Asset asset; + + // Amount of asset + int64 amount; + + // Amount of native asset to pay the reserve + int64 reserve; + + // reserved for future use + union switch (int v) + { + case 0: + void; + } + ext; +}; + struct LedgerEntry { uint32 lastModifiedLedgerSeq; // ledger the LedgerEntry was last changed @@ -274,6 +361,8 @@ struct LedgerEntry OfferEntry offer; case DATA: DataEntry data; + case CLAIMABLE_BALANCE: + ClaimableBalanceEntry claimableBalance; } data; diff --git a/src/xdr/Stellar-ledger.x b/src/xdr/Stellar-ledger.x index 6c2bb88e47..b6c9c24110 100644 --- a/src/xdr/Stellar-ledger.x +++ b/src/xdr/Stellar-ledger.x @@ -143,6 +143,12 @@ case DATA: AccountID accountID; string64 dataName; } data; + +case CLAIMABLE_BALANCE: + struct + { + ClaimableBalanceID balanceID; + } claimableBalance; }; enum BucketEntryType From 288ceb43f4ef28ee120ceaba36c5afc0e2794723 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Tue, 30 Jun 2020 12:53:00 -0700 Subject: [PATCH 03/21] Refactor LedgerCmp and expand it for new LedgerEntryTypes --- src/bucket/LedgerCmp.h | 66 ++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/bucket/LedgerCmp.h b/src/bucket/LedgerCmp.h index e2e332aa63..80bd08bcb2 100644 --- a/src/bucket/LedgerCmp.h +++ b/src/bucket/LedgerCmp.h @@ -9,6 +9,29 @@ namespace stellar { + +template +bool +lexCompare(T&& lhs1, T&& rhs1) +{ + return lhs1 < rhs1; +} + +template +bool +lexCompare(T&& lhs1, T&& rhs1, U&&... args) +{ + if (lhs1 < rhs1) + { + return true; + } + else if (rhs1 < lhs1) + { + return false; + } + return lexCompare(std::forward(args)...); +} + /** * Compare two LedgerEntries or LedgerKeys for 'identity', not content. * @@ -42,45 +65,20 @@ struct LedgerEntryIdCmp switch (aty) { - case ACCOUNT: return a.account().accountID < b.account().accountID; - case TRUSTLINE: - { - auto const& atl = a.trustLine(); - auto const& btl = b.trustLine(); - if (atl.accountID < btl.accountID) - return true; - if (btl.accountID < atl.accountID) - return false; - { - return atl.asset < btl.asset; - } - } - + return lexCompare(a.trustLine().accountID, b.trustLine().accountID, + a.trustLine().asset, b.trustLine().asset); case OFFER: - { - auto const& aof = a.offer(); - auto const& bof = b.offer(); - if (aof.sellerID < bof.sellerID) - return true; - if (bof.sellerID < aof.sellerID) - return false; - return aof.offerID < bof.offerID; - } + return lexCompare(a.offer().sellerID, b.offer().sellerID, + a.offer().offerID, b.offer().offerID); case DATA: - { - auto const& ad = a.data(); - auto const& bd = b.data(); - if (ad.accountID < bd.accountID) - return true; - if (bd.accountID < ad.accountID) - return false; - { - return ad.dataName < bd.dataName; - } - } + return lexCompare(a.data().accountID, b.data().accountID, + a.data().dataName, b.data().dataName); + case CLAIMABLE_BALANCE: + return a.claimableBalance().balanceID < + b.claimableBalance().balanceID; } return false; } From a4a1ea086e3e51ce6da388e05efb28d79a942786 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Tue, 30 Jun 2020 12:53:05 -0700 Subject: [PATCH 04/21] Update LedgerEntryKey for ClaimableBalance --- src/util/types.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/util/types.cpp b/src/util/types.cpp index a2f66d79a6..3a34aca5b0 100644 --- a/src/util/types.cpp +++ b/src/util/types.cpp @@ -18,32 +18,34 @@ LedgerKey LedgerEntryKey(LedgerEntry const& e) { auto& d = e.data; - LedgerKey k; + LedgerKey k(d.type()); switch (d.type()) { - case ACCOUNT: - k.type(ACCOUNT); k.account().accountID = d.account().accountID; break; case TRUSTLINE: - k.type(TRUSTLINE); k.trustLine().accountID = d.trustLine().accountID; k.trustLine().asset = d.trustLine().asset; break; case OFFER: - k.type(OFFER); k.offer().sellerID = d.offer().sellerID; k.offer().offerID = d.offer().offerID; break; case DATA: - k.type(DATA); k.data().accountID = d.data().accountID; k.data().dataName = d.data().dataName; break; + + case CLAIMABLE_BALANCE: + k.claimableBalance().balanceID = d.claimableBalance().balanceID; + break; + + default: + abort(); } return k; } From 74b7f964d6cb7a0f14d270b41b540faa7bb40d9e Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Tue, 30 Jun 2020 12:53:10 -0700 Subject: [PATCH 05/21] Update LedgerTestUtils for ClaimableBalance --- src/ledger/test/LedgerTestUtils.cpp | 49 ++++++++++++++++++++++++++--- src/ledger/test/LedgerTestUtils.h | 5 +++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/ledger/test/LedgerTestUtils.cpp b/src/ledger/test/LedgerTestUtils.cpp index 2980c9e0c0..0ab9c2061f 100644 --- a/src/ledger/test/LedgerTestUtils.cpp +++ b/src/ledger/test/LedgerTestUtils.cpp @@ -90,6 +90,10 @@ randomlyModifyEntry(LedgerEntry& e) e.data.data().dataValue = autocheck::generator{}(8); makeValid(e.data.data()); break; + case CLAIMABLE_BALANCE: + e.data.claimableBalance().amount = autocheck::generator{}(); + makeValid(e.data.claimableBalance()); + break; } } @@ -186,6 +190,19 @@ makeValid(DataEntry& d) replaceControlCharacters(d.dataName, 1); } +void +makeValid(ClaimableBalanceEntry& c) +{ + c.amount = std::abs(c.amount); + clampLow(1, c.amount); + + c.reserve = std::abs(c.reserve); + clampLow(1, c.reserve); + + c.asset.type(ASSET_TYPE_CREDIT_ALPHANUM4); + strToAssetCode(c.asset.alphaNum4().assetCode, "CAD"); +} + void makeValid(std::vector& lhv, LedgerHeaderHistoryEntry firstLedger, @@ -251,20 +268,21 @@ static auto validLedgerEntryGenerator = autocheck::map( le.lastModifiedLedgerSeq = le.lastModifiedLedgerSeq & INT32_MAX; switch (led.type()) { + case ACCOUNT: + makeValid(led.account()); + break; case TRUSTLINE: makeValid(led.trustLine()); break; - case OFFER: makeValid(led.offer()); break; - - case ACCOUNT: - makeValid(led.account()); - break; case DATA: makeValid(led.data()); break; + case CLAIMABLE_BALANCE: + makeValid(led.claimableBalance()); + break; } return le; @@ -299,6 +317,13 @@ static auto validDataEntryGenerator = autocheck::map( }, autocheck::generator()); +static auto validClaimableBalanceEntryGenerator = autocheck::map( + [](ClaimableBalanceEntry&& c, size_t s) { + makeValid(c); + return c; + }, + autocheck::generator()); + LedgerEntry generateValidLedgerEntry(size_t b) { @@ -364,6 +389,20 @@ generateValidDataEntries(size_t n) return vecgen(n); } +ClaimableBalanceEntry +generateValidClaimableBalanceEntry(size_t b) +{ + return validClaimableBalanceEntryGenerator(b); +} + +std::vector +generateValidClaimableBalanceEntries(size_t n) +{ + static auto vecgen = + autocheck::list_of(validClaimableBalanceEntryGenerator); + return vecgen(n); +} + std::vector generateLedgerHeadersForCheckpoint( LedgerHeaderHistoryEntry firstLedger, uint32_t size, diff --git a/src/ledger/test/LedgerTestUtils.h b/src/ledger/test/LedgerTestUtils.h index f189f337d3..ac60b42497 100644 --- a/src/ledger/test/LedgerTestUtils.h +++ b/src/ledger/test/LedgerTestUtils.h @@ -26,6 +26,7 @@ void makeValid(AccountEntry& a); void makeValid(TrustLineEntry& tl); void makeValid(OfferEntry& o); void makeValid(DataEntry& d); +void makeValid(ClaimableBalanceEntry& c); void makeValid(LedgerHeaderHistoryEntry& lh, LedgerHeaderHistoryEntry firstLedger, HistoryManager::LedgerVerificationStatus state); @@ -45,6 +46,10 @@ std::vector generateValidOfferEntries(size_t n); DataEntry generateValidDataEntry(size_t b = 3); std::vector generateValidDataEntries(size_t n); +ClaimableBalanceEntry generateValidClaimableBalanceEntry(size_t b = 3); +std::vector +generateValidClaimableBalanceEntries(size_t n); + std::vector generateLedgerHeadersForCheckpoint( LedgerHeaderHistoryEntry firstLedger, uint32_t size, HistoryManager::LedgerVerificationStatus state = From 901e6bbc2d23dc9826d92d30305b1bf941b47e1c Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Tue, 30 Jun 2020 12:53:16 -0700 Subject: [PATCH 06/21] Update LedgerHashUtils for ClaimableBalance --- src/ledger/LedgerHashUtils.h | 92 +++++++++++++++--------------------- 1 file changed, 37 insertions(+), 55 deletions(-) diff --git a/src/ledger/LedgerHashUtils.h b/src/ledger/LedgerHashUtils.h index 371698411b..25d425745c 100644 --- a/src/ledger/LedgerHashUtils.h +++ b/src/ledger/LedgerHashUtils.h @@ -11,6 +11,38 @@ // implements a default hasher for "LedgerKey" namespace std { +template <> class hash +{ + public: + size_t + operator()(stellar::Asset const& asset) const + { + size_t res = asset.type(); + switch (asset.type()) + { + case stellar::ASSET_TYPE_NATIVE: + break; + case stellar::ASSET_TYPE_CREDIT_ALPHANUM4: + { + auto& a4 = asset.alphaNum4(); + res ^= stellar::shortHash::computeHash( + stellar::ByteSlice(a4.issuer.ed25519().data(), 8)); + res ^= a4.assetCode[0]; + break; + } + case stellar::ASSET_TYPE_CREDIT_ALPHANUM12: + { + auto& a12 = asset.alphaNum12(); + res ^= stellar::shortHash::computeHash( + stellar::ByteSlice(a12.issuer.ed25519().data(), 8)); + res ^= a12.assetCode[0]; + break; + } + } + return res; + } +}; + template <> class hash { public: @@ -29,29 +61,7 @@ template <> class hash auto& tl = lk.trustLine(); res = stellar::shortHash::computeHash( stellar::ByteSlice(tl.accountID.ed25519().data(), 8)); - switch (lk.trustLine().asset.type()) - { - case stellar::ASSET_TYPE_NATIVE: - break; - case stellar::ASSET_TYPE_CREDIT_ALPHANUM4: - { - auto& tl4 = tl.asset.alphaNum4(); - res ^= stellar::shortHash::computeHash( - stellar::ByteSlice(tl4.issuer.ed25519().data(), 8)); - res ^= tl4.assetCode[0]; - break; - } - case stellar::ASSET_TYPE_CREDIT_ALPHANUM12: - { - auto& tl12 = tl.asset.alphaNum12(); - res ^= stellar::shortHash::computeHash( - stellar::ByteSlice(tl12.issuer.ed25519().data(), 8)); - res ^= tl12.assetCode[0]; - break; - } - default: - abort(); - } + res ^= hash()(tl.asset); break; } case stellar::DATA: @@ -64,42 +74,14 @@ template <> class hash res = stellar::shortHash::computeHash(stellar::ByteSlice( &lk.offer().offerID, sizeof(lk.offer().offerID))); break; + case stellar::CLAIMABLE_BALANCE: + res = stellar::shortHash::computeHash(stellar::ByteSlice( + lk.claimableBalance().balanceID.v0().data(), 8)); + break; default: abort(); } return res; } }; - -template <> class hash -{ - public: - size_t - operator()(stellar::Asset const& asset) const - { - size_t res = asset.type(); - switch (asset.type()) - { - case stellar::ASSET_TYPE_NATIVE: - break; - case stellar::ASSET_TYPE_CREDIT_ALPHANUM4: - { - auto& a4 = asset.alphaNum4(); - res ^= stellar::shortHash::computeHash( - stellar::ByteSlice(a4.issuer.ed25519().data(), 8)); - res ^= a4.assetCode[0]; - break; - } - case stellar::ASSET_TYPE_CREDIT_ALPHANUM12: - { - auto& a12 = asset.alphaNum12(); - res ^= stellar::shortHash::computeHash( - stellar::ByteSlice(a12.issuer.ed25519().data(), 8)); - res ^= a12.assetCode[0]; - break; - } - } - return res; - } -}; } From ea6830315ac15041b68845e45e100cf86c5a03a1 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Tue, 30 Jun 2020 12:53:24 -0700 Subject: [PATCH 07/21] Update LedgerTxnTests for ClaimableBalance --- src/ledger/test/LedgerTxnTests.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ledger/test/LedgerTxnTests.cpp b/src/ledger/test/LedgerTxnTests.cpp index ea47f1a868..43e9e090e5 100644 --- a/src/ledger/test/LedgerTxnTests.cpp +++ b/src/ledger/test/LedgerTxnTests.cpp @@ -84,6 +84,12 @@ generateLedgerEntryWithSameKey(LedgerEntry const& leBase) le.data.trustLine().accountID = leBase.data.trustLine().accountID; le.data.trustLine().asset = leBase.data.trustLine().asset; break; + case CLAIMABLE_BALANCE: + le.data.claimableBalance() = + LedgerTestUtils::generateValidClaimableBalanceEntry(); + le.data.claimableBalance().balanceID = + leBase.data.claimableBalance().balanceID; + break; default: REQUIRE(false); } From 6c71d5606178c7d4f2b4cd7b3765b4057f35c736 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Tue, 30 Jun 2020 12:53:29 -0700 Subject: [PATCH 08/21] Implement SQL for ClaimableBalance --- src/database/Database.cpp | 2 + src/ledger/LedgerTxnClaimableBalanceSQL.cpp | 384 ++++++++++++++++++++ src/ledger/LedgerTxnImpl.h | 8 + 3 files changed, 394 insertions(+) create mode 100644 src/ledger/LedgerTxnClaimableBalanceSQL.cpp diff --git a/src/database/Database.cpp b/src/database/Database.cpp index 3860e95965..19ef42c9ad 100644 --- a/src/database/Database.cpp +++ b/src/database/Database.cpp @@ -282,6 +282,8 @@ Database::applySchemaUpgrade(unsigned long vers) // and writes those tables. addTextColumn("offers", "extension"); addTextColumn("accountdata", "extension"); + + mApp.getLedgerTxnRoot().dropClaimableBalances(); } break; default: diff --git a/src/ledger/LedgerTxnClaimableBalanceSQL.cpp b/src/ledger/LedgerTxnClaimableBalanceSQL.cpp new file mode 100644 index 0000000000..3f336ac0d9 --- /dev/null +++ b/src/ledger/LedgerTxnClaimableBalanceSQL.cpp @@ -0,0 +1,384 @@ +// Copyright 2020 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 + +#include "ledger/LedgerTxnImpl.h" +#include "util/Decoder.h" +#include "util/types.h" + +namespace stellar +{ + +template +std::string +toOpaqueBase64(T const& input) +{ + return decoder::encode_b64(xdr::xdr_to_opaque(input)); +} + +template +void +fromOpaqueBase64(T& res, std::string const& opaqueBase64) +{ + std::vector opaque; + decoder::decode_b64(opaqueBase64, opaque); + xdr::xdr_from_opaque(opaque, res); +} + +std::shared_ptr +LedgerTxnRoot::Impl::loadClaimableBalance(LedgerKey const& key) const +{ + auto balanceID = toOpaqueBase64(key.claimableBalance().balanceID); + + std::string claimableBalanceEntryStr; + LedgerEntry le; + + std::string sql = "SELECT ledgerentry " + "FROM claimablebalance " + "WHERE balanceid= :balanceid"; + auto prep = mDatabase.getPreparedStatement(sql); + auto& st = prep.statement(); + st.exchange(soci::into(claimableBalanceEntryStr)); + st.exchange(soci::use(balanceID)); + st.define_and_bind(); + st.execute(true); + if (!st.got_data()) + { + return nullptr; + } + + fromOpaqueBase64(le, claimableBalanceEntryStr); + assert(le.data.type() == CLAIMABLE_BALANCE); + + return std::make_shared(std::move(le)); +} + +class BulkLoadClaimableBalanceOperation + : public DatabaseTypeSpecificOperation> +{ + Database& mDb; + std::vector mBalanceIDs; + + std::vector + executeAndFetch(soci::statement& st) + { + std::string balanceIdStr, claimableBalanceEntryStr; + + st.exchange(soci::into(balanceIdStr)); + st.exchange(soci::into(claimableBalanceEntryStr)); + st.define_and_bind(); + { + auto timer = mDb.getSelectTimer("claimablebalance"); + st.execute(true); + } + + std::vector res; + while (st.got_data()) + { + res.emplace_back(); + auto& le = res.back(); + + fromOpaqueBase64(le, claimableBalanceEntryStr); + assert(le.data.type() == CLAIMABLE_BALANCE); + + st.fetch(); + } + return res; + } + + public: + BulkLoadClaimableBalanceOperation(Database& db, + std::unordered_set const& keys) + : mDb(db) + { + mBalanceIDs.reserve(keys.size()); + for (auto const& k : keys) + { + assert(k.type() == CLAIMABLE_BALANCE); + mBalanceIDs.emplace_back( + toOpaqueBase64(k.claimableBalance().balanceID)); + } + } + + std::vector + doSqliteSpecificOperation(soci::sqlite3_session_backend* sq) override + { + std::vector cstrBalanceIDs; + cstrBalanceIDs.reserve(mBalanceIDs.size()); + for (size_t i = 0; i < mBalanceIDs.size(); ++i) + { + cstrBalanceIDs.emplace_back(mBalanceIDs[i].c_str()); + } + + std::string sql = "WITH r AS (SELECT value FROM carray(?, ?, 'char*')) " + "SELECT balanceid, ledgerentry " + "FROM claimablebalance " + "WHERE balanceid IN r"; + + auto prep = mDb.getPreparedStatement(sql); + auto be = prep.statement().get_backend(); + if (be == nullptr) + { + throw std::runtime_error("no sql backend"); + } + auto sqliteStatement = + dynamic_cast(be); + auto st = sqliteStatement->stmt_; + + sqlite3_reset(st); + sqlite3_bind_pointer(st, 1, cstrBalanceIDs.data(), "carray", 0); + sqlite3_bind_int(st, 2, static_cast(cstrBalanceIDs.size())); + return executeAndFetch(prep.statement()); + } + +#ifdef USE_POSTGRES + std::vector + doPostgresSpecificOperation(soci::postgresql_session_backend* pg) override + { + std::string strBalanceIDs; + marshalToPGArray(pg->conn_, strBalanceIDs, mBalanceIDs); + + std::string sql = "WITH r AS (SELECT unnest(:v1::TEXT[])) " + "SELECT balanceid, ledgerentry " + "FROM claimablebalance " + "WHERE balanceid IN (SELECT * from r)"; + + auto prep = mDb.getPreparedStatement(sql); + auto& st = prep.statement(); + st.exchange(soci::use(strBalanceIDs)); + return executeAndFetch(st); + } +#endif +}; + +std::unordered_map> +LedgerTxnRoot::Impl::bulkLoadClaimableBalance( + std::unordered_set const& keys) const +{ + if (!keys.empty()) + { + BulkLoadClaimableBalanceOperation op(mDatabase, keys); + return populateLoadedEntries( + keys, mDatabase.doDatabaseTypeSpecificOperation(op)); + } + else + { + return {}; + } +} + +class BulkDeleteClaimableBalanceOperation + : public DatabaseTypeSpecificOperation +{ + Database& mDb; + LedgerTxnConsistency mCons; + std::vector mBalanceIDs; + + public: + BulkDeleteClaimableBalanceOperation( + Database& db, LedgerTxnConsistency cons, + std::vector const& entries) + : mDb(db), mCons(cons) + { + mBalanceIDs.reserve(entries.size()); + for (auto const& e : entries) + { + assert(!e.entryExists()); + assert(e.key().type() == CLAIMABLE_BALANCE); + mBalanceIDs.emplace_back( + toOpaqueBase64(e.key().claimableBalance().balanceID)); + } + } + + void + doSociGenericOperation() + { + std::string sql = "DELETE FROM claimablebalance WHERE balanceid = :id"; + auto prep = mDb.getPreparedStatement(sql); + auto& st = prep.statement(); + st.exchange(soci::use(mBalanceIDs)); + st.define_and_bind(); + { + auto timer = mDb.getDeleteTimer("claimablebalance"); + st.execute(true); + } + if (static_cast(st.get_affected_rows()) != mBalanceIDs.size() && + mCons == LedgerTxnConsistency::EXACT) + { + throw std::runtime_error("Could not update data in SQL"); + } + } + + void + doSqliteSpecificOperation(soci::sqlite3_session_backend* sq) override + { + doSociGenericOperation(); + } + +#ifdef USE_POSTGRES + void + doPostgresSpecificOperation(soci::postgresql_session_backend* pg) override + { + std::string strBalanceIDs; + marshalToPGArray(pg->conn_, strBalanceIDs, mBalanceIDs); + + std::string sql = "WITH r AS (SELECT unnest(:v1::TEXT[])) " + "DELETE FROM claimablebalance " + "WHERE balanceid IN (SELECT * FROM r)"; + + auto prep = mDb.getPreparedStatement(sql); + auto& st = prep.statement(); + st.exchange(soci::use(strBalanceIDs)); + st.define_and_bind(); + { + auto timer = mDb.getDeleteTimer("claimablebalance"); + st.execute(true); + } + if (static_cast(st.get_affected_rows()) != mBalanceIDs.size() && + mCons == LedgerTxnConsistency::EXACT) + { + throw std::runtime_error("Could not update data in SQL"); + } + } +#endif +}; + +void +LedgerTxnRoot::Impl::bulkDeleteClaimableBalance( + std::vector const& entries, LedgerTxnConsistency cons) +{ + BulkDeleteClaimableBalanceOperation op(mDatabase, cons, entries); + mDatabase.doDatabaseTypeSpecificOperation(op); +} + +class BulkUpsertClaimableBalanceOperation + : public DatabaseTypeSpecificOperation +{ + Database& mDb; + std::vector mBalanceIDs; + std::vector mClaimableBalanceEntrys; + std::vector mLastModifieds; + + void + accumulateEntry(LedgerEntry const& entry) + { + assert(entry.data.type() == CLAIMABLE_BALANCE); + mBalanceIDs.emplace_back( + toOpaqueBase64(entry.data.claimableBalance().balanceID)); + mClaimableBalanceEntrys.emplace_back(toOpaqueBase64(entry)); + mLastModifieds.emplace_back( + unsignedToSigned(entry.lastModifiedLedgerSeq)); + } + + public: + BulkUpsertClaimableBalanceOperation( + Database& Db, std::vector const& entryIter) + : mDb(Db) + { + for (auto const& e : entryIter) + { + assert(e.entryExists()); + accumulateEntry(e.entry()); + } + } + + void + doSociGenericOperation() + { + std::string sql = "INSERT INTO claimablebalance " + "(balanceid, ledgerentry, lastmodified) " + "VALUES " + "( :id, :v1, :v2 ) " + "ON CONFLICT (balanceid) DO UPDATE SET " + "balanceid = excluded.balanceid, ledgerentry = " + "excluded.ledgerentry, lastmodified = " + "excluded.lastmodified"; + + auto prep = mDb.getPreparedStatement(sql); + soci::statement& st = prep.statement(); + st.exchange(soci::use(mBalanceIDs)); + st.exchange(soci::use(mClaimableBalanceEntrys)); + st.exchange(soci::use(mLastModifieds)); + st.define_and_bind(); + { + auto timer = mDb.getUpsertTimer("claimablebalance"); + st.execute(true); + } + if (static_cast(st.get_affected_rows()) != mBalanceIDs.size()) + { + throw std::runtime_error("Could not update data in SQL"); + } + } + + void + doSqliteSpecificOperation(soci::sqlite3_session_backend* sq) override + { + doSociGenericOperation(); + } + +#ifdef USE_POSTGRES + void + doPostgresSpecificOperation(soci::postgresql_session_backend* pg) override + { + std::string strBalanceIDs, strClaimableBalanceEntry, strLastModifieds; + + PGconn* conn = pg->conn_; + marshalToPGArray(conn, strBalanceIDs, mBalanceIDs); + marshalToPGArray(conn, strClaimableBalanceEntry, + mClaimableBalanceEntrys); + marshalToPGArray(conn, strLastModifieds, mLastModifieds); + + std::string sql = "WITH r AS " + "(SELECT unnest(:ids::TEXT[]), unnest(:v1::TEXT[]), " + "unnest(:v2::INT[]))" + "INSERT INTO claimablebalance " + "(balanceid, ledgerentry, lastmodified) " + "SELECT * FROM r " + "ON CONFLICT (balanceid) DO UPDATE SET " + "balanceid = excluded.balanceid, ledgerentry = " + "excluded.ledgerentry, " + "lastmodified = excluded.lastmodified"; + + auto prep = mDb.getPreparedStatement(sql); + soci::statement& st = prep.statement(); + st.exchange(soci::use(strBalanceIDs)); + st.exchange(soci::use(strClaimableBalanceEntry)); + st.exchange(soci::use(strLastModifieds)); + st.define_and_bind(); + { + auto timer = mDb.getUpsertTimer("claimablebalance"); + st.execute(true); + } + if (static_cast(st.get_affected_rows()) != mBalanceIDs.size()) + { + throw std::runtime_error("Could not update data in SQL"); + } + } +#endif +}; + +void +LedgerTxnRoot::Impl::bulkUpsertClaimableBalance( + std::vector const& entries) +{ + BulkUpsertClaimableBalanceOperation op(mDatabase, entries); + mDatabase.doDatabaseTypeSpecificOperation(op); +} + +void +LedgerTxnRoot::Impl::dropClaimableBalances() +{ + throwIfChild(); + mEntryCache.clear(); + mBestOffersCache.clear(); + + std::string coll = mDatabase.getSimpleCollationClause(); + + mDatabase.getSession() << "DROP TABLE IF EXISTS claimablebalance;"; + mDatabase.getSession() << "CREATE TABLE claimablebalance (" + << "balanceid VARCHAR(48) " << coll + << " PRIMARY KEY, " + << "ledgerentry TEXT NOT NULL, " + << "lastmodified INT NOT NULL);"; +} +} diff --git a/src/ledger/LedgerTxnImpl.h b/src/ledger/LedgerTxnImpl.h index faa205a4a0..717048dc62 100644 --- a/src/ledger/LedgerTxnImpl.h +++ b/src/ledger/LedgerTxnImpl.h @@ -694,6 +694,8 @@ class LedgerTxnRoot::Impl int64_t minBalance) const; std::shared_ptr loadTrustLine(LedgerKey const& key) const; + std::shared_ptr + loadClaimableBalance(LedgerKey const& key) const; void bulkApply(BulkLedgerEntryChangeAccumulator& bleca, size_t bufferThreshold, LedgerTxnConsistency cons); @@ -709,6 +711,9 @@ class LedgerTxnRoot::Impl void bulkUpsertAccountData(std::vector const& entries); void bulkDeleteAccountData(std::vector const& entries, LedgerTxnConsistency cons); + void bulkUpsertClaimableBalance(std::vector const& entries); + void bulkDeleteClaimableBalance(std::vector const& entries, + LedgerTxnConsistency cons); static std::string tableFromLedgerEntryType(LedgerEntryType let); @@ -742,6 +747,8 @@ class LedgerTxnRoot::Impl bulkLoadOffers(std::unordered_set const& keys) const; std::unordered_map> bulkLoadData(std::unordered_set const& keys) const; + std::unordered_map> + bulkLoadClaimableBalance(std::unordered_set const& keys) const; public: // Constructor has the strong exception safety guarantee @@ -770,6 +777,7 @@ class LedgerTxnRoot::Impl void dropData(); void dropOffers(); void dropTrustLines(); + void dropClaimableBalances(); #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION void resetForFuzzer(); From ba6155091bcc41aa986bb0613f2c66405b7160a6 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:53:34 -0700 Subject: [PATCH 09/21] Use claimable balance SQL in LedgerTxn --- src/database/Database.cpp | 1 + src/ledger/InMemoryLedgerTxnRoot.cpp | 5 ++++ src/ledger/InMemoryLedgerTxnRoot.h | 1 + src/ledger/LedgerTxn.cpp | 45 +++++++++++++++++++++++++++- src/ledger/LedgerTxn.h | 6 ++++ src/ledger/LedgerTxnImpl.h | 14 +++++++++ 6 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/database/Database.cpp b/src/database/Database.cpp index 19ef42c9ad..236b9abd9b 100644 --- a/src/database/Database.cpp +++ b/src/database/Database.cpp @@ -655,6 +655,7 @@ Database::initialize() mApp.getLedgerTxnRoot().dropOffers(); mApp.getLedgerTxnRoot().dropTrustLines(); mApp.getLedgerTxnRoot().dropData(); + mApp.getLedgerTxnRoot().dropClaimableBalances(); } OverlayManager::dropAll(*this); PersistentState::dropAll(*this); diff --git a/src/ledger/InMemoryLedgerTxnRoot.cpp b/src/ledger/InMemoryLedgerTxnRoot.cpp index 7a11bb0847..49ed8826ca 100644 --- a/src/ledger/InMemoryLedgerTxnRoot.cpp +++ b/src/ledger/InMemoryLedgerTxnRoot.cpp @@ -116,6 +116,11 @@ InMemoryLedgerTxnRoot::dropTrustLines() { } +void +InMemoryLedgerTxnRoot::dropClaimableBalances() +{ +} + double InMemoryLedgerTxnRoot::getPrefetchHitRate() const { diff --git a/src/ledger/InMemoryLedgerTxnRoot.h b/src/ledger/InMemoryLedgerTxnRoot.h index 9ec10d500a..f20496120c 100644 --- a/src/ledger/InMemoryLedgerTxnRoot.h +++ b/src/ledger/InMemoryLedgerTxnRoot.h @@ -54,6 +54,7 @@ class InMemoryLedgerTxnRoot : public AbstractLedgerTxnParent void dropData() override; void dropOffers() override; void dropTrustLines() override; + void dropClaimableBalances() override; double getPrefetchHitRate() const override; uint32_t prefetch(std::unordered_set const& keys) override; }; diff --git a/src/ledger/LedgerTxn.cpp b/src/ledger/LedgerTxn.cpp index 0835054ab6..22aca98b02 100644 --- a/src/ledger/LedgerTxn.cpp +++ b/src/ledger/LedgerTxn.cpp @@ -1538,6 +1538,13 @@ LedgerTxn::dropTrustLines() throw std::runtime_error("called dropTrustLines on non-root LedgerTxn"); } +void +LedgerTxn::dropClaimableBalances() +{ + throw std::runtime_error( + "called dropClaimableBalances on non-root LedgerTxn"); +} + double LedgerTxn::getPrefetchHitRate() const { @@ -2017,6 +2024,9 @@ BulkLedgerEntryChangeAccumulator::accumulate(EntryIterator const& iter) case DATA: accum(iter, mAccountDataToUpsert, mAccountDataToDelete); break; + case CLAIMABLE_BALANCE: + accum(iter, mClaimableBalanceToUpsert, mClaimableBalanceToDelete); + break; default: abort(); } @@ -2075,6 +2085,18 @@ LedgerTxnRoot::Impl::bulkApply(BulkLedgerEntryChangeAccumulator& bleca, bulkDeleteAccountData(deleteAccountData, cons); deleteAccountData.clear(); } + auto& upsertClaimableBalance = bleca.getClaimableBalanceToUpsert(); + if (upsertClaimableBalance.size() > bufferThreshold) + { + bulkUpsertClaimableBalance(upsertClaimableBalance); + upsertClaimableBalance.clear(); + } + auto& deleteClaimableBalance = bleca.getClaimableBalanceToDelete(); + if (deleteClaimableBalance.size() > bufferThreshold) + { + bulkDeleteClaimableBalance(deleteClaimableBalance, cons); + deleteClaimableBalance.clear(); + } } void @@ -2149,6 +2171,8 @@ LedgerTxnRoot::Impl::tableFromLedgerEntryType(LedgerEntryType let) return "offers"; case TRUSTLINE: return "trustlines"; + case CLAIMABLE_BALANCE: + return "claimablebalance"; default: throw std::runtime_error("Unknown ledger entry type"); } @@ -2211,7 +2235,7 @@ LedgerTxnRoot::Impl::deleteObjectsModifiedOnOrAfterLedger(uint32_t ledger) const mEntryCache.clear(); mBestOffersCache.clear(); - for (auto let : {ACCOUNT, DATA, TRUSTLINE, OFFER}) + for (auto let : {ACCOUNT, DATA, TRUSTLINE, OFFER, CLAIMABLE_BALANCE}) { std::string query = "DELETE FROM " + tableFromLedgerEntryType(let) + " WHERE lastmodified >= :v1"; @@ -2243,6 +2267,12 @@ LedgerTxnRoot::dropTrustLines() mImpl->dropTrustLines(); } +void +LedgerTxnRoot::dropClaimableBalances() +{ + mImpl->dropClaimableBalances(); +} + uint32_t LedgerTxnRoot::prefetch(std::unordered_set const& keys) { @@ -2259,6 +2289,7 @@ LedgerTxnRoot::Impl::prefetch(std::unordered_set const& keys) std::unordered_set offers; std::unordered_set trustlines; std::unordered_set data; + std::unordered_set claimablebalance; auto cacheResult = [&](std::unordered_map const& keys) data.clear(); } break; + case CLAIMABLE_BALANCE: + insertIfNotLoaded(claimablebalance, key); + if (data.size() == mBulkLoadBatchSize) + { + cacheResult(bulkLoadClaimableBalance(claimablebalance)); + claimablebalance.clear(); + } + break; } } @@ -2328,6 +2367,7 @@ LedgerTxnRoot::Impl::prefetch(std::unordered_set const& keys) cacheResult(bulkLoadOffers(offers)); cacheResult(bulkLoadTrustLines(trustlines)); cacheResult(bulkLoadData(data)); + cacheResult(bulkLoadClaimableBalance(claimablebalance)); return total; } @@ -2622,6 +2662,9 @@ LedgerTxnRoot::Impl::getNewestVersion(LedgerKey const& key) const case TRUSTLINE: entry = loadTrustLine(key); break; + case CLAIMABLE_BALANCE: + entry = loadClaimableBalance(key); + break; default: throw std::runtime_error("Unknown key type"); } diff --git a/src/ledger/LedgerTxn.h b/src/ledger/LedgerTxn.h index 69a6af90ed..e3e5ce0828 100644 --- a/src/ledger/LedgerTxn.h +++ b/src/ledger/LedgerTxn.h @@ -427,6 +427,10 @@ class AbstractLedgerTxnParent // other than a (real or stub) root LedgerTxn. virtual void dropTrustLines() = 0; + // Delete all claimable balance ledger entries. Will throw when called on + // anything other than a (real or stub) root LedgerTxn. + virtual void dropClaimableBalances() = 0; + // Return the current cache hit rate for prefetched ledger entries, as a // fraction from 0.0 to 1.0. Will throw when called on anything other than a // (real or stub) root LedgerTxn. @@ -670,6 +674,7 @@ class LedgerTxn final : public AbstractLedgerTxn void dropData() override; void dropOffers() override; void dropTrustLines() override; + void dropClaimableBalances() override; double getPrefetchHitRate() const override; uint32_t prefetch(std::unordered_set const& keys) override; @@ -707,6 +712,7 @@ class LedgerTxnRoot : public AbstractLedgerTxnParent void dropData() override; void dropOffers() override; void dropTrustLines() override; + void dropClaimableBalances() override; #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION void resetForFuzzer(); diff --git a/src/ledger/LedgerTxnImpl.h b/src/ledger/LedgerTxnImpl.h index 717048dc62..16bc098249 100644 --- a/src/ledger/LedgerTxnImpl.h +++ b/src/ledger/LedgerTxnImpl.h @@ -80,6 +80,8 @@ class BulkLedgerEntryChangeAccumulator std::vector mAccountsToDelete; std::vector mAccountDataToUpsert; std::vector mAccountDataToDelete; + std::vector mClaimableBalanceToUpsert; + std::vector mClaimableBalanceToDelete; std::vector mOffersToUpsert; std::vector mOffersToDelete; std::vector mTrustLinesToUpsert; @@ -134,6 +136,18 @@ class BulkLedgerEntryChangeAccumulator return mAccountDataToDelete; } + std::vector& + getClaimableBalanceToUpsert() + { + return mClaimableBalanceToUpsert; + } + + std::vector& + getClaimableBalanceToDelete() + { + return mClaimableBalanceToDelete; + } + void accumulate(EntryIterator const& iter); }; From caf454ec13ffd7f9e45c0231b4112e12c422619f Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:53:39 -0700 Subject: [PATCH 10/21] add XDR for claimable balance ops --- src/xdr/Stellar-ledger-entries.x | 3 +- src/xdr/Stellar-transaction.x | 93 +++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/xdr/Stellar-ledger-entries.x b/src/xdr/Stellar-ledger-entries.x index 9d6a1f4183..0605db67b0 100644 --- a/src/xdr/Stellar-ledger-entries.x +++ b/src/xdr/Stellar-ledger-entries.x @@ -385,6 +385,7 @@ enum EnvelopeType ENVELOPE_TYPE_TX = 2, ENVELOPE_TYPE_AUTH = 3, ENVELOPE_TYPE_SCPVALUE = 4, - ENVELOPE_TYPE_TX_FEE_BUMP = 5 + ENVELOPE_TYPE_TX_FEE_BUMP = 5, + ENVELOPE_TYPE_OP_ID = 6 }; } diff --git a/src/xdr/Stellar-transaction.x b/src/xdr/Stellar-transaction.x index bee28cbade..9f9b918f6d 100644 --- a/src/xdr/Stellar-transaction.x +++ b/src/xdr/Stellar-transaction.x @@ -41,7 +41,9 @@ enum OperationType MANAGE_DATA = 10, BUMP_SEQUENCE = 11, MANAGE_BUY_OFFER = 12, - PATH_PAYMENT_STRICT_SEND = 13 + PATH_PAYMENT_STRICT_SEND = 13, + CREATE_CLAIMABLE_BALANCE = 14, + CLAIM_CLAIMABLE_BALANCE = 15 }; /* CreateAccount @@ -292,6 +294,30 @@ struct BumpSequenceOp SequenceNumber bumpTo; }; +/* Creates a claimable balance entry + + Threshold: med + + Result: CreateClaimableBalanceResult +*/ +struct CreateClaimableBalanceOp +{ + Asset asset; + int64 amount; + Claimant claimants<10>; +}; + +/* Claims a claimable balance entry + + Threshold: low + + Result: ClaimClaimableBalanceResult +*/ +struct ClaimClaimableBalanceOp +{ + ClaimableBalanceID balanceID; +}; + /* An operation is the lowest unit of work that a transaction does */ struct Operation { @@ -330,10 +356,25 @@ struct Operation ManageBuyOfferOp manageBuyOfferOp; case PATH_PAYMENT_STRICT_SEND: PathPaymentStrictSendOp pathPaymentStrictSendOp; + case CREATE_CLAIMABLE_BALANCE: + CreateClaimableBalanceOp createClaimableBalanceOp; + case CLAIM_CLAIMABLE_BALANCE: + ClaimClaimableBalanceOp claimClaimableBalanceOp; } body; }; +union OperationID switch (EnvelopeType type) +{ +case ENVELOPE_TYPE_OP_ID: + struct + { + MuxedAccount sourceAccount; + SequenceNumber seqNum; + uint32 opNum; + } id; +}; + enum MemoType { MEMO_NONE = 0, @@ -595,7 +636,8 @@ struct SimplePaymentResult int64 amount; }; -union PathPaymentStrictReceiveResult switch (PathPaymentStrictReceiveResultCode code) +union PathPaymentStrictReceiveResult switch ( + PathPaymentStrictReceiveResultCode code) { case PATH_PAYMENT_STRICT_RECEIVE_SUCCESS: struct @@ -908,6 +950,49 @@ case BUMP_SEQUENCE_SUCCESS: default: void; }; + +/******* CreateClaimableBalance Result ********/ + +enum CreateClaimableBalanceResultCode +{ + CREATE_CLAIMABLE_BALANCE_SUCCESS = 0, + CREATE_CLAIMABLE_BALANCE_MALFORMED = -1, + CREATE_CLAIMABLE_BALANCE_LOW_RESERVE = -2, + CREATE_CLAIMABLE_BALANCE_NO_TRUST = -3, + CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED = -4, + CREATE_CLAIMABLE_BALANCE_UNDERFUNDED = -5 +}; + +union CreateClaimableBalanceResult switch ( + CreateClaimableBalanceResultCode code) +{ +case CREATE_CLAIMABLE_BALANCE_SUCCESS: + ClaimableBalanceID balanceID; +default: + void; +}; + +/******* ClaimClaimableBalance Result ********/ + +enum ClaimClaimableBalanceResultCode +{ + CLAIM_CLAIMABLE_BALANCE_SUCCESS = 0, + CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST = -1, + CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM = -2, + CLAIM_CLAIMABLE_BALANCE_LINE_FULL = -3, + CLAIM_CLAIMABLE_BALANCE_NO_TRUST = -4, + CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED = -5 + +}; + +union ClaimClaimableBalanceResult switch (ClaimClaimableBalanceResultCode code) +{ +case CLAIM_CLAIMABLE_BALANCE_SUCCESS: + void; +default: + void; +}; + /* High level Operation Result */ enum OperationResultCode @@ -954,6 +1039,10 @@ case opINNER: ManageBuyOfferResult manageBuyOfferResult; case PATH_PAYMENT_STRICT_SEND: PathPaymentStrictSendResult pathPaymentStrictSendResult; + case CREATE_CLAIMABLE_BALANCE: + CreateClaimableBalanceResult createClaimableBalanceResult; + case CLAIM_CLAIMABLE_BALANCE: + ClaimClaimableBalanceResult claimClaimableBalanceResult; } tr; default: From 36852dd40ac332f5d695584d8f2cb9d0f2974957 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:53:44 -0700 Subject: [PATCH 11/21] add loadClaimableBalance to TransactionUtils --- src/transactions/TransactionUtils.cpp | 15 +++++++++++++++ src/transactions/TransactionUtils.h | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/src/transactions/TransactionUtils.cpp b/src/transactions/TransactionUtils.cpp index 1562447267..01cede16a6 100644 --- a/src/transactions/TransactionUtils.cpp +++ b/src/transactions/TransactionUtils.cpp @@ -69,6 +69,14 @@ dataKey(AccountID const& accountID, std::string const& dataName) return key; } +LedgerKey +claimableBalanceKey(ClaimableBalanceID const& balanceID) +{ + LedgerKey key(CLAIMABLE_BALANCE); + key.claimableBalance().balanceID = balanceID; + return key; +} + LedgerTxnEntry loadAccount(AbstractLedgerTxn& ltx, AccountID const& accountID) { @@ -98,6 +106,13 @@ loadOffer(AbstractLedgerTxn& ltx, AccountID const& sellerID, int64_t offerID) return ltx.load(offerKey(sellerID, offerID)); } +LedgerTxnEntry +loadClaimableBalance(AbstractLedgerTxn& ltx, + ClaimableBalanceID const& balanceID) +{ + return ltx.load(claimableBalanceKey(balanceID)); +} + TrustLineWrapper loadTrustLine(AbstractLedgerTxn& ltx, AccountID const& accountID, Asset const& asset) diff --git a/src/transactions/TransactionUtils.h b/src/transactions/TransactionUtils.h index f01d6a61c3..6fa21489f9 100644 --- a/src/transactions/TransactionUtils.h +++ b/src/transactions/TransactionUtils.h @@ -24,6 +24,7 @@ LedgerKey accountKey(AccountID const& accountID); LedgerKey trustlineKey(AccountID const& accountID, Asset const& asset); LedgerKey offerKey(AccountID const& sellerID, uint64_t offerID); LedgerKey dataKey(AccountID const& accountID, std::string const& dataName); +LedgerKey claimableBalanceKey(ClaimableBalanceID const& balanceID); uint32_t const FIRST_PROTOCOL_SUPPORTING_OPERATION_LIMITS = 11; uint32_t const ACCOUNT_SUBENTRY_LIMIT = 1000; @@ -40,6 +41,9 @@ LedgerTxnEntry loadData(AbstractLedgerTxn& ltx, AccountID const& accountID, LedgerTxnEntry loadOffer(AbstractLedgerTxn& ltx, AccountID const& sellerID, int64_t offerID); +LedgerTxnEntry loadClaimableBalance(AbstractLedgerTxn& ltx, + ClaimableBalanceID const& balanceID); + TrustLineWrapper loadTrustLine(AbstractLedgerTxn& ltx, AccountID const& accountID, Asset const& asset); From 908cd231ecb0ee1a71239c634b37cdbea4b4dbac Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:53:49 -0700 Subject: [PATCH 12/21] fix invariant tests --- src/invariant/AccountSubEntriesCountIsValid.cpp | 5 +++++ .../BucketListIsConsistentWithDatabase.cpp | 13 ++++++++++++- .../test/AccountSubEntriesCountIsValidTests.cpp | 4 +++- .../BucketListIsConsistentWithDatabaseTests.cpp | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/invariant/AccountSubEntriesCountIsValid.cpp b/src/invariant/AccountSubEntriesCountIsValid.cpp index 5a41a97433..c4bef73c88 100644 --- a/src/invariant/AccountSubEntriesCountIsValid.cpp +++ b/src/invariant/AccountSubEntriesCountIsValid.cpp @@ -74,6 +74,11 @@ updateChangedSubEntriesCount( calculateDelta(current, previous); break; } + case CLAIMABLE_BALANCE: + { + // claimable balance is not a subentry + break; + } default: abort(); } diff --git a/src/invariant/BucketListIsConsistentWithDatabase.cpp b/src/invariant/BucketListIsConsistentWithDatabase.cpp index f11f5874a0..0443aef568 100644 --- a/src/invariant/BucketListIsConsistentWithDatabase.cpp +++ b/src/invariant/BucketListIsConsistentWithDatabase.cpp @@ -80,7 +80,8 @@ BucketListIsConsistentWithDatabase::checkOnBucketApply( std::shared_ptr bucket, uint32_t oldestLedger, uint32_t newestLedger) { - uint64_t nAccounts = 0, nTrustLines = 0, nOffers = 0, nData = 0; + uint64_t nAccounts = 0, nTrustLines = 0, nOffers = 0, nData = 0, + nClaimableBalance = 0; { LedgerTxn ltx(mApp.getLedgerTxnRoot()); @@ -134,6 +135,9 @@ BucketListIsConsistentWithDatabase::checkOnBucketApply( case DATA: ++nData; break; + case CLAIMABLE_BALANCE: + ++nClaimableBalance; + break; default: abort(); } @@ -178,6 +182,13 @@ BucketListIsConsistentWithDatabase::checkOnBucketApply( { return fmt::format(countFormat, "Data", nData, nDataInDb); } + uint64_t nClaimableBalanceInDb = + ltxRoot.countObjects(CLAIMABLE_BALANCE, range); + if (nClaimableBalanceInDb != nClaimableBalance) + { + return fmt::format(countFormat, "ClaimableBalance", nClaimableBalance, + nClaimableBalanceInDb); + } return {}; } diff --git a/src/invariant/test/AccountSubEntriesCountIsValidTests.cpp b/src/invariant/test/AccountSubEntriesCountIsValidTests.cpp index 2f98343377..644f290c15 100644 --- a/src/invariant/test/AccountSubEntriesCountIsValidTests.cpp +++ b/src/invariant/test/AccountSubEntriesCountIsValidTests.cpp @@ -50,7 +50,7 @@ generateRandomSubEntry(LedgerEntry const& acc) do { le = LedgerTestUtils::generateValidLedgerEntry(5); - } while (le.data.type() == ACCOUNT); + } while (le.data.type() == ACCOUNT || le.data.type() == CLAIMABLE_BALANCE); le.lastModifiedLedgerSeq = acc.lastModifiedLedgerSeq; switch (le.data.type()) @@ -67,6 +67,7 @@ generateRandomSubEntry(LedgerEntry const& acc) le.data.data().accountID = acc.data.account().accountID; le.data.data().dataName = validDataNameGenerator(64); break; + case CLAIMABLE_BALANCE: case ACCOUNT: default: abort(); @@ -86,6 +87,7 @@ generateRandomModifiedSubEntry(LedgerEntry const& acc, LedgerEntry const& se) switch (se.data.type()) { case ACCOUNT: + case CLAIMABLE_BALANCE: break; case OFFER: res.data.offer().offerID = se.data.offer().offerID; diff --git a/src/invariant/test/BucketListIsConsistentWithDatabaseTests.cpp b/src/invariant/test/BucketListIsConsistentWithDatabaseTests.cpp index de38698a41..da57e569fc 100644 --- a/src/invariant/test/BucketListIsConsistentWithDatabaseTests.cpp +++ b/src/invariant/test/BucketListIsConsistentWithDatabaseTests.cpp @@ -416,6 +416,17 @@ class ApplyBucketsWorkModifyEntry : public ApplyBucketsWork entry.data.data().dataName = data.dataName; } + void + modifyClaimableBalanceEntry(LedgerEntry& entry) + { + ClaimableBalanceEntry const& cb = mEntry.data.claimableBalance(); + entry.lastModifiedLedgerSeq = mEntry.lastModifiedLedgerSeq; + entry.data.claimableBalance() = + LedgerTestUtils::generateValidClaimableBalanceEntry(5); + + entry.data.claimableBalance().balanceID = cb.balanceID; + } + public: ApplyBucketsWorkModifyEntry( Application& app, @@ -452,6 +463,9 @@ class ApplyBucketsWorkModifyEntry : public ApplyBucketsWork case DATA: modifyDataEntry(entry.current()); break; + case CLAIMABLE_BALANCE: + modifyClaimableBalanceEntry(entry.current()); + break; default: REQUIRE(false); } From 2bb700880cc65a955a5176c692d39c84dbb2fde7 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:53:54 -0700 Subject: [PATCH 13/21] Create ClaimClaimableBalanceOpFrame --- .../ClaimClaimableBalanceOpFrame.cpp | 168 ++++++++++++++++++ .../ClaimClaimableBalanceOpFrame.h | 45 +++++ 2 files changed, 213 insertions(+) create mode 100644 src/transactions/ClaimClaimableBalanceOpFrame.cpp create mode 100644 src/transactions/ClaimClaimableBalanceOpFrame.h diff --git a/src/transactions/ClaimClaimableBalanceOpFrame.cpp b/src/transactions/ClaimClaimableBalanceOpFrame.cpp new file mode 100644 index 0000000000..4de2601ddf --- /dev/null +++ b/src/transactions/ClaimClaimableBalanceOpFrame.cpp @@ -0,0 +1,168 @@ +// Copyright 2020 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 + +#include "transactions/ClaimClaimableBalanceOpFrame.h" +#include "crypto/SHA.h" +#include "ledger/LedgerTxn.h" +#include "ledger/LedgerTxnEntry.h" +#include "ledger/LedgerTxnHeader.h" +#include "ledger/TrustLineWrapper.h" +#include "transactions/TransactionUtils.h" + +namespace stellar +{ + +ClaimClaimableBalanceOpFrame::ClaimClaimableBalanceOpFrame( + Operation const& op, OperationResult& res, TransactionFrame& parentTx) + : OperationFrame(op, res, parentTx) + , mClaimClaimableBalance(mOperation.body.claimClaimableBalanceOp()) +{ +} + +ThresholdLevel +ClaimClaimableBalanceOpFrame::getThresholdLevel() const +{ + return ThresholdLevel::LOW; +} + +bool +ClaimClaimableBalanceOpFrame::isVersionSupported(uint32_t protocolVersion) const +{ + return protocolVersion >= 14; +} + +bool +validatePredicate(ClaimPredicate const& pred, TimePoint closeTime) +{ + switch (pred.type()) + { + case CLAIM_PREDICATE_UNCONDITIONAL: + break; + case CLAIM_PREDICATE_AND: + { + auto const& andPredicates = pred.andPredicates(); + + return validatePredicate(andPredicates.at(0), closeTime) && + validatePredicate(andPredicates.at(1), closeTime); + } + + case CLAIM_PREDICATE_OR: + { + auto const& orPredicates = pred.orPredicates(); + + return validatePredicate(orPredicates.at(0), closeTime) || + validatePredicate(orPredicates.at(1), closeTime); + } + + case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME: + return static_cast(pred.absBefore()) > closeTime; + case CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME: + return static_cast(pred.absAfter()) <= closeTime; + default: + throw std::runtime_error("Invalid ClaimPredicate"); + } + + return true; +} + +bool +ClaimClaimableBalanceOpFrame::doApply(AbstractLedgerTxn& ltx) +{ + auto claimableBalanceLtxEntry = + stellar::loadClaimableBalance(ltx, mClaimClaimableBalance.balanceID); + if (!claimableBalanceLtxEntry) + { + innerResult().code(CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST); + return false; + } + + auto const& claimableBalance = + claimableBalanceLtxEntry.current().data.claimableBalance(); + + auto it = std::find_if( + std::begin(claimableBalance.claimants), + std::end(claimableBalance.claimants), [&](Claimant const& claimant) { + return claimant.v0().destination == getSourceID(); + }); + + auto header = ltx.loadHeader(); + if (it == claimableBalance.claimants.end() || + !validatePredicate(it->v0().predicate, + header.current().scpValue.closeTime)) + { + innerResult().code(CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM); + return false; + } + + // Try to give reserve back to createdBy account. If we can't, try to give + // it back to sourceAccount + if (!loadAccountAndAddBalance(ltx, header, claimableBalance.createdBy, + claimableBalance.reserve)) + { + if (!loadAccountAndAddBalance(ltx, header, getSourceID(), + claimableBalance.reserve)) + { + innerResult().code(CLAIM_CLAIMABLE_BALANCE_LINE_FULL); + return false; + } + } + + auto const& asset = claimableBalance.asset; + auto amount = claimableBalance.amount; + if (asset.type() == ASSET_TYPE_NATIVE) + { + if (!loadAccountAndAddBalance(ltx, header, getSourceID(), amount)) + { + innerResult().code(CLAIM_CLAIMABLE_BALANCE_LINE_FULL); + return false; + } + } + else + { + auto trustline = loadTrustLine(ltx, getSourceID(), asset); + if (!trustline) + { + innerResult().code(CLAIM_CLAIMABLE_BALANCE_NO_TRUST); + return false; + } + if (!trustline.isAuthorized()) + { + innerResult().code(CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED); + return false; + } + if (!trustline.addBalance(header, amount)) + { + innerResult().code(CLAIM_CLAIMABLE_BALANCE_LINE_FULL); + return false; + } + } + + claimableBalanceLtxEntry.erase(); + + innerResult().code(CLAIM_CLAIMABLE_BALANCE_SUCCESS); + return true; +} + +bool +ClaimClaimableBalanceOpFrame::doCheckValid(uint32_t ledgerVersion) +{ + return true; +} + +bool +ClaimClaimableBalanceOpFrame::loadAccountAndAddBalance( + AbstractLedgerTxn& ltx, LedgerTxnHeader const& header, + AccountID const& accountID, int64_t amount) +{ + auto account = stellar::loadAccount(ltx, accountID); + return account && addBalance(header, account, amount); +} + +void +ClaimClaimableBalanceOpFrame::insertLedgerKeysToPrefetch( + std::unordered_set& keys) const +{ + keys.emplace(claimableBalanceKey(mClaimClaimableBalance.balanceID)); +} +} diff --git a/src/transactions/ClaimClaimableBalanceOpFrame.h b/src/transactions/ClaimClaimableBalanceOpFrame.h new file mode 100644 index 0000000000..f6ccec6128 --- /dev/null +++ b/src/transactions/ClaimClaimableBalanceOpFrame.h @@ -0,0 +1,45 @@ +#pragma once + +// Copyright 2020 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 + +#include "transactions/OperationFrame.h" + +namespace stellar +{ + +class AbstractLedgerTxn; + +class ClaimClaimableBalanceOpFrame : public OperationFrame +{ + ThresholdLevel getThresholdLevel() const override; + ClaimClaimableBalanceResult& + innerResult() + { + return mResult.tr().claimClaimableBalanceResult(); + } + + bool loadAccountAndAddBalance(AbstractLedgerTxn& ltx, + LedgerTxnHeader const& header, + AccountID const& accountID, int64_t amount); + ClaimClaimableBalanceOp const& mClaimClaimableBalance; + + public: + ClaimClaimableBalanceOpFrame(Operation const& op, OperationResult& res, + TransactionFrame& parentTx); + + bool isVersionSupported(uint32_t protocolVersion) const override; + + bool doApply(AbstractLedgerTxn& ltx) override; + bool doCheckValid(uint32_t ledgerVersion) override; + void insertLedgerKeysToPrefetch( + std::unordered_set& keys) const override; + + static ClaimClaimableBalanceResultCode + getInnerCode(OperationResult const& res) + { + return res.tr().claimClaimableBalanceResult().code(); + } +}; +} From fd0588a2ba5c1160b6177ac262ed8aeb2cb895b2 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:53:58 -0700 Subject: [PATCH 14/21] Create CreateClaimableBalanceOpFrame --- .../CreateClaimableBalanceOpFrame.cpp | 276 ++++++++++++++++++ .../CreateClaimableBalanceOpFrame.h | 42 +++ 2 files changed, 318 insertions(+) create mode 100644 src/transactions/CreateClaimableBalanceOpFrame.cpp create mode 100644 src/transactions/CreateClaimableBalanceOpFrame.h diff --git a/src/transactions/CreateClaimableBalanceOpFrame.cpp b/src/transactions/CreateClaimableBalanceOpFrame.cpp new file mode 100644 index 0000000000..fd87bd867b --- /dev/null +++ b/src/transactions/CreateClaimableBalanceOpFrame.cpp @@ -0,0 +1,276 @@ +// Copyright 2020 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 + +#include "transactions/CreateClaimableBalanceOpFrame.h" +#include "crypto/SHA.h" +#include "ledger/LedgerTxn.h" +#include "ledger/LedgerTxnEntry.h" +#include "ledger/LedgerTxnHeader.h" +#include "ledger/TrustLineWrapper.h" +#include "transactions/TransactionUtils.h" + +namespace stellar +{ + +int64_t +relativeToAbsolute(TimePoint closeTime, int64_t relative) +{ + return closeTime > static_cast(INT64_MAX - relative) + ? INT64_MAX + : closeTime + relative; +} + +// convert all relative predicates to absolute predicates +void +updatePredicatesForApply(ClaimPredicate& pred, TimePoint closeTime) +{ + switch (pred.type()) + { + case CLAIM_PREDICATE_AND: + { + auto& andPredicates = pred.andPredicates(); + + updatePredicatesForApply(andPredicates.at(0), closeTime); + updatePredicatesForApply(andPredicates.at(1), closeTime); + + break; + } + + case CLAIM_PREDICATE_OR: + { + auto& orPredicates = pred.orPredicates(); + + updatePredicatesForApply(orPredicates.at(0), closeTime); + updatePredicatesForApply(orPredicates.at(1), closeTime); + + break; + } + case CLAIM_PREDICATE_BEFORE_RELATIVE_TIME: + { + auto relBefore = pred.relBefore(); + pred.type(CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME); + pred.absBefore() = relativeToAbsolute(closeTime, relBefore); + + break; + } + case CLAIM_PREDICATE_AFTER_RELATIVE_TIME: + { + auto relAfter = pred.relAfter(); + pred.type(CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME); + pred.absAfter() = relativeToAbsolute(closeTime, relAfter); + + break; + } + case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME: + case CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME: + case CLAIM_PREDICATE_UNCONDITIONAL: + break; + default: + throw std::runtime_error("Invalid ClaimPredicate"); + } +} + +bool +validatePredicate(ClaimPredicate const& pred, uint32_t depth) +{ + if (depth > 4) + { + return false; + } + + switch (pred.type()) + { + case CLAIM_PREDICATE_UNCONDITIONAL: + break; + case CLAIM_PREDICATE_AND: + { + auto const& andPredicates = pred.andPredicates(); + if (andPredicates.size() != 2) + { + return false; + } + return validatePredicate(andPredicates[0], depth + 1) && + validatePredicate(andPredicates[1], depth + 1); + } + + case CLAIM_PREDICATE_OR: + { + auto const& orPredicates = pred.orPredicates(); + if (orPredicates.size() != 2) + { + return false; + } + return validatePredicate(orPredicates[0], depth + 1) && + validatePredicate(orPredicates[1], depth + 1); + } + + case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME: + return pred.absBefore() >= 0; + case CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME: + return pred.absAfter() >= 0; + case CLAIM_PREDICATE_BEFORE_RELATIVE_TIME: + return pred.relBefore() >= 0; + case CLAIM_PREDICATE_AFTER_RELATIVE_TIME: + return pred.relAfter() >= 0; + default: + throw std::runtime_error("Invalid ClaimPredicate"); + } + + return true; +} + +CreateClaimableBalanceOpFrame::CreateClaimableBalanceOpFrame( + Operation const& op, OperationResult& res, TransactionFrame& parentTx, + uint32_t index) + : OperationFrame(op, res, parentTx) + , mCreateClaimableBalance(mOperation.body.createClaimableBalanceOp()) + , mOpIndex(index) +{ +} + +bool +CreateClaimableBalanceOpFrame::isVersionSupported( + uint32_t protocolVersion) const +{ + return protocolVersion >= 14; +} + +bool +CreateClaimableBalanceOpFrame::doApply(AbstractLedgerTxn& ltx) +{ + auto header = ltx.loadHeader(); + auto sourceAccount = loadSourceAccount(ltx, header); + + auto const& claimants = mCreateClaimableBalance.claimants; + + // Deduct claimants.size() * baseReserve of native asset + int64_t entryCost = + int64_t(header.current().baseReserve) * claimants.size(); + + if (getAvailableBalance(header, sourceAccount) < entryCost) + { + innerResult().code(CREATE_CLAIMABLE_BALANCE_LOW_RESERVE); + return false; + } + + auto costOk = addBalance(header, sourceAccount, -entryCost); + assert(costOk); + + // Deduct amount of asset from sourceAccount + auto const& asset = mCreateClaimableBalance.asset; + auto amount = mCreateClaimableBalance.amount; + + if (asset.type() == ASSET_TYPE_NATIVE) + { + if (getAvailableBalance(header, sourceAccount) < amount) + { + innerResult().code(CREATE_CLAIMABLE_BALANCE_UNDERFUNDED); + return false; + } + + auto amountOk = addBalance(header, sourceAccount, -amount); + assert(amountOk); + } + else + { + auto trustline = loadTrustLine(ltx, getSourceID(), asset); + if (!trustline) + { + innerResult().code(CREATE_CLAIMABLE_BALANCE_NO_TRUST); + return false; + } + if (!trustline.isAuthorized()) + { + innerResult().code(CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED); + return false; + } + + if (!trustline.addBalance(header, -amount)) + { + innerResult().code(CREATE_CLAIMABLE_BALANCE_UNDERFUNDED); + return false; + } + } + + // Create claimable balance entry + LedgerEntry newClaimableBalance; + newClaimableBalance.data.type(CLAIMABLE_BALANCE); + + auto& claimableBalanceEntry = newClaimableBalance.data.claimableBalance(); + claimableBalanceEntry.createdBy = getSourceID(); + claimableBalanceEntry.amount = amount; + claimableBalanceEntry.asset = asset; + claimableBalanceEntry.reserve = entryCost; + + OperationID operationID; + operationID.type(ENVELOPE_TYPE_OP_ID); + operationID.id().sourceAccount = toMuxedAccount(mParentTx.getSourceID()); + operationID.id().seqNum = mParentTx.getSeqNum(); + operationID.id().opNum = mOpIndex; + + claimableBalanceEntry.balanceID.v0() = + sha256(xdr::xdr_to_opaque(operationID)); + + claimableBalanceEntry.claimants = claimants; + for (auto& claimant : claimableBalanceEntry.claimants) + { + updatePredicatesForApply(claimant.v0().predicate, + header.current().scpValue.closeTime); + } + + ltx.create(newClaimableBalance); + + innerResult().code(CREATE_CLAIMABLE_BALANCE_SUCCESS); + innerResult().balanceID() = claimableBalanceEntry.balanceID; + return true; +} + +bool +CreateClaimableBalanceOpFrame::doCheckValid(uint32_t ledgerVersion) +{ + auto const& claimants = mCreateClaimableBalance.claimants; + + if (!isAssetValid(mCreateClaimableBalance.asset) || + mCreateClaimableBalance.amount <= 0 || claimants.empty()) + { + innerResult().code(CREATE_CLAIMABLE_BALANCE_MALFORMED); + return false; + } + + // check for duplicates + std::unordered_set dests; + for (auto const& claimant : claimants) + { + auto const& dest = claimant.v0().destination; + if (!dests.emplace(dest).second) + { + innerResult().code(CREATE_CLAIMABLE_BALANCE_MALFORMED); + return false; + } + } + + for (auto const& claimant : claimants) + { + if (!validatePredicate(claimant.v0().predicate, 1)) + { + innerResult().code(CREATE_CLAIMABLE_BALANCE_MALFORMED); + return false; + } + } + + return true; +} + +void +CreateClaimableBalanceOpFrame::insertLedgerKeysToPrefetch( + std::unordered_set& keys) const +{ + // Prefetch trustline for non-native assets + if (mCreateClaimableBalance.asset.type() != ASSET_TYPE_NATIVE) + { + keys.emplace( + trustlineKey(getSourceID(), mCreateClaimableBalance.asset)); + } +} +} diff --git a/src/transactions/CreateClaimableBalanceOpFrame.h b/src/transactions/CreateClaimableBalanceOpFrame.h new file mode 100644 index 0000000000..976b160eea --- /dev/null +++ b/src/transactions/CreateClaimableBalanceOpFrame.h @@ -0,0 +1,42 @@ +#pragma once + +// Copyright 2020 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 + +#include "transactions/OperationFrame.h" + +namespace stellar +{ + +class AbstractLedgerTxn; + +class CreateClaimableBalanceOpFrame : public OperationFrame +{ + CreateClaimableBalanceResult& + innerResult() + { + return mResult.tr().createClaimableBalanceResult(); + } + CreateClaimableBalanceOp const& mCreateClaimableBalance; + + uint32_t mOpIndex; + + public: + CreateClaimableBalanceOpFrame(Operation const& op, OperationResult& res, + TransactionFrame& parentTx, uint32_t index); + + bool isVersionSupported(uint32_t protocolVersion) const override; + + bool doApply(AbstractLedgerTxn& ltx) override; + bool doCheckValid(uint32_t ledgerVersion) override; + void insertLedgerKeysToPrefetch( + std::unordered_set& keys) const override; + + static CreateClaimableBalanceResultCode + getInnerCode(OperationResult const& res) + { + return res.tr().createClaimableBalanceResult().code(); + } +}; +} From 2c44ab3bb6386a7fa75f00e3fb993316f29dab43 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:54:02 -0700 Subject: [PATCH 15/21] instantiate ClaimableBalance ops and pass index to create op --- src/transactions/OperationFrame.cpp | 9 ++++++++- src/transactions/OperationFrame.h | 2 +- src/transactions/TransactionFrame.cpp | 3 ++- src/transactions/simulation/TxSimTransactionFrame.cpp | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/transactions/OperationFrame.cpp b/src/transactions/OperationFrame.cpp index d8960d999e..8bd3a27bbc 100644 --- a/src/transactions/OperationFrame.cpp +++ b/src/transactions/OperationFrame.cpp @@ -6,7 +6,9 @@ #include "transactions/AllowTrustOpFrame.h" #include "transactions/BumpSequenceOpFrame.h" #include "transactions/ChangeTrustOpFrame.h" +#include "transactions/ClaimClaimableBalanceOpFrame.h" #include "transactions/CreateAccountOpFrame.h" +#include "transactions/CreateClaimableBalanceOpFrame.h" #include "transactions/CreatePassiveSellOfferOpFrame.h" #include "transactions/InflationOpFrame.h" #include "transactions/ManageBuyOfferOpFrame.h" @@ -47,7 +49,7 @@ getNeededThreshold(LedgerTxnEntry const& account, ThresholdLevel const level) shared_ptr OperationFrame::makeHelper(Operation const& op, OperationResult& res, - TransactionFrame& tx) + TransactionFrame& tx, uint32_t index) { switch (op.body.type()) { @@ -79,6 +81,11 @@ OperationFrame::makeHelper(Operation const& op, OperationResult& res, return std::make_shared(op, res, tx); case PATH_PAYMENT_STRICT_SEND: return std::make_shared(op, res, tx); + case CREATE_CLAIMABLE_BALANCE: + return std::make_shared(op, res, tx, + index); + case CLAIM_CLAIMABLE_BALANCE: + return std::make_shared(op, res, tx); default: ostringstream err; err << "Unknown Tx type: " << op.body.type(); diff --git a/src/transactions/OperationFrame.h b/src/transactions/OperationFrame.h index eaeea6a6ba..01b840e2cf 100644 --- a/src/transactions/OperationFrame.h +++ b/src/transactions/OperationFrame.h @@ -54,7 +54,7 @@ class OperationFrame public: static std::shared_ptr makeHelper(Operation const& op, OperationResult& res, - TransactionFrame& parentTx); + TransactionFrame& parentTx, uint32_t index); OperationFrame(Operation const& op, OperationResult& res, TransactionFrame& parentTx); diff --git a/src/transactions/TransactionFrame.cpp b/src/transactions/TransactionFrame.cpp index b24361a3ae..9481619bf6 100644 --- a/src/transactions/TransactionFrame.cpp +++ b/src/transactions/TransactionFrame.cpp @@ -263,7 +263,8 @@ std::shared_ptr TransactionFrame::makeOperation(Operation const& op, OperationResult& res, size_t index) { - return OperationFrame::makeHelper(op, res, *this); + return OperationFrame::makeHelper(op, res, *this, + static_cast(index)); } void diff --git a/src/transactions/simulation/TxSimTransactionFrame.cpp b/src/transactions/simulation/TxSimTransactionFrame.cpp index f90149181b..f23232c4f0 100644 --- a/src/transactions/simulation/TxSimTransactionFrame.cpp +++ b/src/transactions/simulation/TxSimTransactionFrame.cpp @@ -54,7 +54,8 @@ TxSimTransactionFrame::makeOperation(Operation const& op, OperationResult& res, return std::make_shared( op, res, *this, resultFromArchive, mCount); default: - return OperationFrame::makeHelper(op, res, *this); + return OperationFrame::makeHelper(op, res, *this, + static_cast(index)); } } From 5161d4d41ec262e53fcd04ddf5b8f58badb96e8e Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:54:06 -0700 Subject: [PATCH 16/21] fix invariants for ClaimableBalance --- src/invariant/ConservationOfLumens.cpp | 18 ++++++ src/invariant/LedgerEntryIsValid.cpp | 80 ++++++++++++++++++++++++++ src/invariant/LedgerEntryIsValid.h | 4 ++ 3 files changed, 102 insertions(+) diff --git a/src/invariant/ConservationOfLumens.cpp b/src/invariant/ConservationOfLumens.cpp index 14d641410b..28112f0a7e 100644 --- a/src/invariant/ConservationOfLumens.cpp +++ b/src/invariant/ConservationOfLumens.cpp @@ -23,6 +23,24 @@ calculateDeltaBalance(std::shared_ptr const& current, return (current ? current->data.account().balance : 0) - (previous ? previous->data.account().balance : 0); } + if (let == CLAIMABLE_BALANCE) + { + int64_t reserveDelta = + (current ? current->data.claimableBalance().reserve : 0) - + (previous ? previous->data.claimableBalance().reserve : 0); + + auto const& asset = current ? current->data.claimableBalance().asset + : previous->data.claimableBalance().asset; + + if (asset.type() != ASSET_TYPE_NATIVE) + { + return reserveDelta; + } + + return ((current ? current->data.claimableBalance().amount : 0) - + (previous ? previous->data.claimableBalance().amount : 0)) + + reserveDelta; + } return 0; } diff --git a/src/invariant/LedgerEntryIsValid.cpp b/src/invariant/LedgerEntryIsValid.cpp index c31a02fa06..8954d497f2 100644 --- a/src/invariant/LedgerEntryIsValid.cpp +++ b/src/invariant/LedgerEntryIsValid.cpp @@ -102,6 +102,8 @@ LedgerEntryIsValid::checkIsValid(LedgerEntry const& le, uint32_t ledgerSeq, return checkIsValid(le.data.offer(), version); case DATA: return checkIsValid(le.data.data(), version); + case CLAIMABLE_BALANCE: + return checkIsValid(le.data.claimableBalance(), version); default: return "LedgerEntry has invalid type"; } @@ -226,4 +228,82 @@ LedgerEntryIsValid::checkIsValid(DataEntry const& de, uint32 version) const } return {}; } + +bool +LedgerEntryIsValid::validatePredicate(ClaimPredicate const& pred, + uint32_t depth) const +{ + if (depth > 4) + { + return false; + } + + switch (pred.type()) + { + case CLAIM_PREDICATE_UNCONDITIONAL: + break; + case CLAIM_PREDICATE_AND: + { + auto const& andPredicates = pred.andPredicates(); + if (andPredicates.size() != 2) + { + return false; + } + return validatePredicate(andPredicates[0], depth + 1) && + validatePredicate(andPredicates[1], depth + 1); + } + + case CLAIM_PREDICATE_OR: + { + auto const& orPredicates = pred.orPredicates(); + if (orPredicates.size() != 2) + { + return false; + } + return validatePredicate(orPredicates[0], depth + 1) && + validatePredicate(orPredicates[1], depth + 1); + } + + case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME: + return pred.absBefore() >= 0; + case CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME: + return pred.absAfter() >= 0; + default: + return false; + } + + return true; +} + +std::string +LedgerEntryIsValid::checkIsValid(ClaimableBalanceEntry const& cbe, + uint32 version) const +{ + if (cbe.claimants.empty()) + { + return "ClaimableBalance claimants is empty"; + } + if (!isAssetValid(cbe.asset)) + { + return "ClaimableBalance asset is invalid"; + } + if (cbe.amount <= 0) + { + return "ClaimableBalance amount is not positive"; + } + if (cbe.reserve <= 0) + { + return "ClaimableBalance reserve is not positive"; + } + + for (auto const& claimant : cbe.claimants) + { + if (!validatePredicate(claimant.v0().predicate, 1)) + { + return "ClaimableBalance claimant is invalid"; + } + } + + return {}; +} } diff --git a/src/invariant/LedgerEntryIsValid.h b/src/invariant/LedgerEntryIsValid.h index c9f828cfb5..aa69a13594 100644 --- a/src/invariant/LedgerEntryIsValid.h +++ b/src/invariant/LedgerEntryIsValid.h @@ -42,5 +42,9 @@ class LedgerEntryIsValid : public Invariant std::string checkIsValid(TrustLineEntry const& tl, uint32 version) const; std::string checkIsValid(OfferEntry const& oe, uint32 version) const; std::string checkIsValid(DataEntry const& de, uint32 version) const; + std::string checkIsValid(ClaimableBalanceEntry const& cbe, + uint32 version) const; + + bool validatePredicate(ClaimPredicate const& pred, uint32_t depth) const; }; } From 7f07aa16baf8f7ef074c5bab0975677b14f5454e Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:54:10 -0700 Subject: [PATCH 17/21] add tests for ClaimableBalance ops --- src/test/TestAccount.cpp | 62 + src/test/TestAccount.h | 8 + src/test/TestExceptions.cpp | 50 + src/test/TestExceptions.h | 14 + src/test/TxTests.cpp | 21 + src/test/TxTests.h | 5 + .../test/ClaimableBalanceTests.cpp | 1068 +++++++++++++++++ 7 files changed, 1228 insertions(+) create mode 100644 src/transactions/test/ClaimableBalanceTests.cpp diff --git a/src/test/TestAccount.cpp b/src/test/TestAccount.cpp index 98d7a34d88..0b8a435287 100644 --- a/src/test/TestAccount.cpp +++ b/src/test/TestAccount.cpp @@ -3,6 +3,7 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "test/TestAccount.h" +#include "ledger/LedgerManager.h" #include "ledger/LedgerTxn.h" #include "ledger/LedgerTxnEntry.h" #include "ledger/LedgerTxnHeader.h" @@ -229,6 +230,67 @@ TestAccount::bumpSequence(SequenceNumber to) applyTx(tx({txtest::bumpSequence(to)}), mApp, false); } +ClaimableBalanceID +TestAccount::createClaimableBalance(Asset const& asset, int64_t amount, + xdr::xvector const& claimants) +{ + auto transaction = + tx({txtest::createClaimableBalance(asset, amount, claimants)}); + applyTx(transaction, mApp); + + auto returnedBalanceID = transaction->getResult() + .result.results()[0] + .tr() + .createClaimableBalanceResult() + .balanceID(); + + // validate balanceID returned is what we expect + REQUIRE(returnedBalanceID == getBalanceID(0)); + + LedgerTxn ltx(mApp.getLedgerTxnRoot()); + auto entry = stellar::loadClaimableBalance(ltx, returnedBalanceID); + REQUIRE(entry); + + auto const& claimableBalance = entry.current().data.claimableBalance(); + REQUIRE(claimableBalance.asset == asset); + REQUIRE(claimableBalance.amount == amount); + REQUIRE(claimableBalance.balanceID == returnedBalanceID); + REQUIRE(claimableBalance.createdBy == getPublicKey()); + REQUIRE(static_cast(claimableBalance.reserve) == + claimants.size() * mApp.getLedgerManager().getLastReserve()); + + return returnedBalanceID; +} + +void +TestAccount::claimClaimableBalance(ClaimableBalanceID const& balanceID) +{ + applyTx(tx({txtest::claimClaimableBalance(balanceID)}), mApp); + + LedgerTxn ltx(mApp.getLedgerTxnRoot()); + REQUIRE(!stellar::loadClaimableBalance(ltx, balanceID)); +} + +ClaimableBalanceID +TestAccount::getBalanceID(uint32_t opIndex, SequenceNumber sn) +{ + if (sn == 0) + { + sn = getLastSequenceNumber(); + } + + OperationID operationID; + operationID.type(ENVELOPE_TYPE_OP_ID); + operationID.id().sourceAccount = toMuxedAccount(getPublicKey()); + operationID.id().seqNum = sn; + operationID.id().opNum = opIndex; + + ClaimableBalanceID balanceID; + balanceID.v0() = sha256(xdr::xdr_to_opaque(operationID)); + + return balanceID; +} + int64_t TestAccount::manageOffer(int64_t offerID, Asset const& selling, Asset const& buying, Price const& price, diff --git a/src/test/TestAccount.h b/src/test/TestAccount.h index 2b79541d01..fb340b2151 100644 --- a/src/test/TestAccount.h +++ b/src/test/TestAccount.h @@ -56,6 +56,14 @@ class TestAccount void bumpSequence(SequenceNumber to); + ClaimableBalanceID + createClaimableBalance(Asset const& asset, int64_t amount, + xdr::xvector const& claimants); + + void claimClaimableBalance(ClaimableBalanceID const& balanceID); + + ClaimableBalanceID getBalanceID(uint32_t opIndex, SequenceNumber sn = 0); + int64_t manageOffer(int64_t offerID, Asset const& selling, Asset const& buying, Price const& price, int64_t amount, diff --git a/src/test/TestExceptions.cpp b/src/test/TestExceptions.cpp index 43ed24367b..24e4e30062 100644 --- a/src/test/TestExceptions.cpp +++ b/src/test/TestExceptions.cpp @@ -350,6 +350,50 @@ throwIf(BumpSequenceResult const& result) } } +void +throwIf(CreateClaimableBalanceResult const& result) +{ + switch (result.code()) + { + case CREATE_CLAIMABLE_BALANCE_MALFORMED: + throw ex_CREATE_CLAIMABLE_BALANCE_MALFORMED{}; + case CREATE_CLAIMABLE_BALANCE_LOW_RESERVE: + throw ex_CREATE_CLAIMABLE_BALANCE_LOW_RESERVE{}; + case CREATE_CLAIMABLE_BALANCE_NO_TRUST: + throw ex_CREATE_CLAIMABLE_BALANCE_NO_TRUST{}; + case CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED: + throw ex_CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED{}; + case CREATE_CLAIMABLE_BALANCE_UNDERFUNDED: + throw ex_CREATE_CLAIMABLE_BALANCE_UNDERFUNDED{}; + case CREATE_CLAIMABLE_BALANCE_SUCCESS: + break; + default: + throw ex_UNKNOWN{}; + } +} + +void +throwIf(ClaimClaimableBalanceResult const& result) +{ + switch (result.code()) + { + case CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST: + throw ex_CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST{}; + case CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM: + throw ex_CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM{}; + case CLAIM_CLAIMABLE_BALANCE_LINE_FULL: + throw ex_CLAIM_CLAIMABLE_BALANCE_LINE_FULL{}; + case CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED: + throw ex_CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED{}; + case CLAIM_CLAIMABLE_BALANCE_NO_TRUST: + throw ex_CLAIM_CLAIMABLE_BALANCE_NO_TRUST{}; + case CLAIM_CLAIMABLE_BALANCE_SUCCESS: + break; + default: + throw ex_UNKNOWN{}; + } +} + void throwIf(TransactionResult const& result) { @@ -434,6 +478,12 @@ throwIf(TransactionResult const& result) case PATH_PAYMENT_STRICT_SEND: throwIf(opResult.tr().pathPaymentStrictSendResult()); break; + case CREATE_CLAIMABLE_BALANCE: + throwIf(opResult.tr().createClaimableBalanceResult()); + break; + case CLAIM_CLAIMABLE_BALANCE: + throwIf(opResult.tr().claimClaimableBalanceResult()); + break; } } } diff --git a/src/test/TestExceptions.h b/src/test/TestExceptions.h index b5bed3e4fa..b4fe0ba555 100644 --- a/src/test/TestExceptions.h +++ b/src/test/TestExceptions.h @@ -141,5 +141,19 @@ TEST_EXCEPTION(ex_SET_OPTIONS_BAD_SIGNER) TEST_EXCEPTION(ex_SET_OPTIONS_INVALID_HOME_DOMAIN) TEST_EXCEPTION(ex_INFLATION_NOT_TIME) + +TEST_EXCEPTION(ex_CREATE_CLAIMABLE_BALANCE_SUCCESS) +TEST_EXCEPTION(ex_CREATE_CLAIMABLE_BALANCE_MALFORMED) +TEST_EXCEPTION(ex_CREATE_CLAIMABLE_BALANCE_LOW_RESERVE) +TEST_EXCEPTION(ex_CREATE_CLAIMABLE_BALANCE_NO_TRUST) +TEST_EXCEPTION(ex_CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED) +TEST_EXCEPTION(ex_CREATE_CLAIMABLE_BALANCE_UNDERFUNDED) + +TEST_EXCEPTION(ex_CLAIM_CLAIMABLE_BALANCE_SUCCESS) +TEST_EXCEPTION(ex_CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST) +TEST_EXCEPTION(ex_CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM) +TEST_EXCEPTION(ex_CLAIM_CLAIMABLE_BALANCE_LINE_FULL) +TEST_EXCEPTION(ex_CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED) +TEST_EXCEPTION(ex_CLAIM_CLAIMABLE_BALANCE_NO_TRUST) } } diff --git a/src/test/TxTests.cpp b/src/test/TxTests.cpp index 19b84c0d54..6cc34b8f74 100644 --- a/src/test/TxTests.cpp +++ b/src/test/TxTests.cpp @@ -1061,6 +1061,27 @@ bumpSequence(SequenceNumber to) return op; } +Operation +createClaimableBalance(Asset const& asset, int64_t amount, + xdr::xvector const& claimants) +{ + Operation op; + op.body.type(CREATE_CLAIMABLE_BALANCE); + op.body.createClaimableBalanceOp().asset = asset; + op.body.createClaimableBalanceOp().amount = amount; + op.body.createClaimableBalanceOp().claimants = claimants; + return op; +} + +Operation +claimClaimableBalance(ClaimableBalanceID const& balanceID) +{ + Operation op; + op.body.type(CLAIM_CLAIMABLE_BALANCE); + op.body.claimClaimableBalanceOp().balanceID = balanceID; + return op; +} + OperationFrame const& getFirstOperationFrame(TransactionFrame const& tx) { diff --git a/src/test/TxTests.h b/src/test/TxTests.h index 6d80ae0984..aa9c3ff310 100644 --- a/src/test/TxTests.h +++ b/src/test/TxTests.h @@ -122,6 +122,11 @@ Operation payment(PublicKey const& to, int64_t amount); Operation payment(PublicKey const& to, Asset const& asset, int64_t amount); +Operation createClaimableBalance(Asset const& asset, int64_t amount, + xdr::xvector const& claimants); + +Operation claimClaimableBalance(ClaimableBalanceID const& balanceID); + TransactionFramePtr createPaymentTx(Application& app, SecretKey const& from, PublicKey const& to, SequenceNumber seq, int64_t amount); diff --git a/src/transactions/test/ClaimableBalanceTests.cpp b/src/transactions/test/ClaimableBalanceTests.cpp new file mode 100644 index 0000000000..a06c6badc5 --- /dev/null +++ b/src/transactions/test/ClaimableBalanceTests.cpp @@ -0,0 +1,1068 @@ +// Copyright 2020 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 + +#include "ledger/LedgerTxn.h" +#include "lib/catch.hpp" +#include "main/Application.h" +#include "test/TestAccount.h" +#include "test/TestExceptions.h" +#include "test/TestUtils.h" +#include "test/TxTests.h" +#include "test/test.h" +#include "util/Math.h" +#include + +#include "transactions/TransactionUtils.h" + +using namespace stellar; +using namespace stellar::txtest; + +static Claimant +makeClaimant(AccountID const& account, ClaimPredicate const& pred) +{ + Claimant c; + c.v0().destination = account; + c.v0().predicate = pred; + + return c; +} + +static ClaimPredicate +makeSimplePredicate(uint32_t levels) +{ + ClaimPredicate pred; + if (levels == 0) + { + pred.type(CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME).absBefore() = INT64_MAX; + return pred; + } + + auto& orPred = pred.type(CLAIM_PREDICATE_OR).orPredicates(); + auto nextLevel = makeSimplePredicate(levels - 1); + orPred.emplace_back(nextLevel); + orPred.emplace_back(nextLevel); + + return pred; +} + +static void +randomizePredicatePos(ClaimPredicate pred1, ClaimPredicate pred2, + xdr::xvector& vec) +{ + std::uniform_int_distribution dist(0, 1); + bool randBool = dist(gRandomEngine); + + auto const& firstPred = randBool ? pred1 : pred2; + auto const& secondPred = randBool ? pred2 : pred1; + + vec.emplace_back(firstPred); + vec.emplace_back(secondPred); +} + +static ClaimPredicate +makePredicate(ClaimPredicateType type, bool satisfied, TimePoint nextCloseTime, + uint32_t secondsToNextClose) +{ + ClaimPredicate pred; + pred.type(type); + switch (type) + { + case ClaimPredicateType::CLAIM_PREDICATE_UNCONDITIONAL: + break; + case ClaimPredicateType::CLAIM_PREDICATE_AND: + { + auto pred1 = + makePredicate(CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME, satisfied, + nextCloseTime, secondsToNextClose); + auto pred2 = makePredicate(CLAIM_PREDICATE_BEFORE_RELATIVE_TIME, true, + nextCloseTime, secondsToNextClose); + + randomizePredicatePos(pred1, pred2, pred.andPredicates()); + break; + } + + case ClaimPredicateType::CLAIM_PREDICATE_OR: + { + auto pred1 = + makePredicate(CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME, satisfied, + nextCloseTime, secondsToNextClose); + auto pred2 = makePredicate(CLAIM_PREDICATE_BEFORE_RELATIVE_TIME, false, + nextCloseTime, secondsToNextClose); + + randomizePredicatePos(pred1, pred2, pred.orPredicates()); + break; + } + + case ClaimPredicateType::CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME: + pred.absBefore() = satisfied ? nextCloseTime + 1 : nextCloseTime; + break; + case ClaimPredicateType::CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME: + pred.absAfter() = satisfied ? nextCloseTime : nextCloseTime + 1; + break; + case ClaimPredicateType::CLAIM_PREDICATE_BEFORE_RELATIVE_TIME: + pred.relBefore() = + satisfied ? secondsToNextClose + 1 : secondsToNextClose; + break; + case ClaimPredicateType::CLAIM_PREDICATE_AFTER_RELATIVE_TIME: + pred.relAfter() = + satisfied ? secondsToNextClose : secondsToNextClose + 1; + break; + default: + abort(); + } + + return pred; +} + +static ClaimPredicate +connectPredicate(bool isAnd, ClaimPredicate left, ClaimPredicate right) +{ + ClaimPredicate pred; + if (isAnd) + { + auto& andPred = pred.type(CLAIM_PREDICATE_AND).andPredicates(); + andPred.emplace_back(left); + andPred.emplace_back(right); + } + else + { + auto& orPred = pred.type(CLAIM_PREDICATE_OR).orPredicates(); + orPred.emplace_back(left); + orPred.emplace_back(right); + } + + return pred; +} + +static void +validateBalancesOnCreateAndClaim(TestAccount& createAcc, TestAccount& claimAcc, + Asset const& asset, int64_t amount, + xdr::xvector const& claimants, + TestApplication& app, + bool mergeCreateAcc = false) +{ + + auto const& lm = app.getLedgerManager(); + + bool const isNative = asset.type() == ASSET_TYPE_NATIVE; + + auto getAccAssetBalance = [&](TestAccount const& acc) { + return isNative ? acc.getBalance() : acc.loadTrustLine(asset).balance; + }; + + // verify the delta in balances + auto createAccNativeBeforeCreate = createAcc.getBalance(); + auto claimAccBalanceBeforeCreate = getAccAssetBalance(claimAcc); + + auto createAccAssetBeforeCreate = getAccAssetBalance(createAcc); + auto balanceID = createAcc.createClaimableBalance(asset, amount, claimants); + + // if the asset is non-native, then the createAcc has two balances that + // changed (native for reserve and non-native for amount). Check + // non-native here + REQUIRE((isNative || createAccAssetBeforeCreate - amount == + getAccAssetBalance(createAcc))); + + // move close time forward + closeLedgerOn(app, lm.getLastClosedLedgerNum() + 1, 2, 1, 2016); + + auto createAccNativeAfterCreate = createAcc.getBalance(); + auto claimAccBalanceAfterCreate = getAccAssetBalance(claimAcc); + + int64_t reserve = claimants.size() * lm.getLastReserve(); + REQUIRE(createAccNativeBeforeCreate - (isNative ? amount : 0) - reserve - + static_cast(lm.getLastTxFee()) == + createAccNativeAfterCreate); + + // the claim account doesn't change on the create + REQUIRE(claimAccBalanceBeforeCreate == claimAccBalanceAfterCreate); + + if (mergeCreateAcc) + { + auto root = TestAccount::createRoot(app); + createAcc.merge(root); + } + + claimAcc.claimClaimableBalance(balanceID); + + if (!mergeCreateAcc) + { + // check that reserve is sent back to createAcc + auto createAccNativeAfterClaim = createAcc.getBalance(); + REQUIRE(createAccNativeAfterCreate + reserve == + createAccNativeAfterClaim); + } + + int64_t fee = isNative ? lm.getLastTxFee() : 0; + reserve = mergeCreateAcc ? reserve : 0; + + auto claimAccBalanceAfterClaim = getAccAssetBalance(claimAcc); + REQUIRE(claimAccBalanceAfterCreate + amount - fee + reserve == + claimAccBalanceAfterClaim); +} + +TEST_CASE("claimableBalance", "[tx][managedata]") +{ + Config const& cfg = getTestConfig(); + + VirtualClock clock; + auto app = createTestApplication(clock, cfg); + + app->start(); + + // set up world + auto root = TestAccount::createRoot(*app); + auto const& lm = app->getLedgerManager(); + + int64_t const trustLineLimit = INT64_MAX; + int64_t const minBalance1 = lm.getLastMinBalance(1); + int64_t const minBalance3 = lm.getLastMinBalance(3); + + auto acc1 = root.create("acc1", minBalance3); + auto acc2 = root.create("acc2", minBalance3); + + auto issuer = root.create("issuer", minBalance3); + auto usd = makeAsset(issuer, "USD"); + auto native = makeNativeAsset(); + + // acc2 is authorized + acc2.changeTrust(usd, trustLineLimit); + + auto simplePred = makeSimplePredicate(3); // validPredicate + + xdr::xvector validClaimants{makeClaimant(acc2, simplePred)}; + + SECTION("not supported before version 14") + { + for_versions_to(13, *app, [&] { + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(native, 100, validClaimants), + ex_opNOT_SUPPORTED); + + REQUIRE_THROWS_AS(acc1.claimClaimableBalance(ClaimableBalanceID{}), + ex_opNOT_SUPPORTED); + }); + } + // generalize for native vs non-native assets + for_versions_from(14, *app, [&] { + auto balanceTestByAsset = [&](Asset const& asset, int64_t amount) { + bool const isNative = asset.type() == ASSET_TYPE_NATIVE; + + auto fundForClaimableBalance = [&]() { + // provide enough balance to acc1 to pay amount on the claimable + // balance + if (!isNative) + { + issuer.pay(acc1, usd, amount); + } + else + { + root.pay(acc1, amount); + } + }; + + // close ledger to change closeTime from 0 so we can test predicates + closeLedgerOn(*app, lm.getLastClosedLedgerNum() + 1, 1, 1, 2016); + + // authorize trustline + if (!isNative) + { + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, amount, validClaimants), + ex_CREATE_CLAIMABLE_BALANCE_NO_TRUST); + } + + // create and modify trustlines regardless of asset type so the + // number of subentries are the same same in both scenarios. + // create unauthorized trustline + issuer.setOptions( + setFlags(static_cast(AUTH_REQUIRED_FLAG))); + acc1.changeTrust(usd, trustLineLimit); + + if (!isNative) + { + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, amount, validClaimants), + ex_CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED); + } + + issuer.allowTrust(usd, acc1); + + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, amount, validClaimants), + ex_CREATE_CLAIMABLE_BALANCE_UNDERFUNDED); + + // Include second claimant. An additional baseReserve is + // required, but will not be available. Account was created with + // 3 baseReserves, but we need 4 (1 for account, 1 for + // trustline, and 2 for claimable balance entry) + + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, amount, + {makeClaimant(acc2, simplePred), + makeClaimant(issuer, simplePred)}), + ex_CREATE_CLAIMABLE_BALANCE_LOW_RESERVE); + + fundForClaimableBalance(); + + SECTION("valid predicate and claimant combinations") + { + Claimant c; + c.v0().destination = acc2; + + for (uint32_t i = 0; i < 4; i++) + { + SECTION(fmt::format("predicate at level {}", i)) + { + acc2.claimClaimableBalance(acc1.createClaimableBalance( + asset, amount, + {makeClaimant(acc2, makeSimplePredicate(i))})); + } + } + + for (size_t i = 1; i <= 10; ++i) + { + auto pred = makeSimplePredicate(3); + + SECTION(fmt::format("number of claimants={}", i)) + { + // make sure we have enough to pay reserve. Account + // should already have enough for 1 claimant, so no need + // to add in that case + if (i > 1) + { + root.pay(acc1, lm.getLastReserve() * (i - 1)); + } + + c.v0().predicate = pred; + xdr::xvector claimants{c}; + + // i - 1 because the first claimant is already in + // claimants + std::generate_n( + std::back_inserter(claimants), i - 1, + [&claimants, &pred] { + Claimant c; + c.v0().destination = + getAccount(std::to_string(claimants.size())) + .getPublicKey(); + c.v0().predicate = pred; + return c; + }); + + validateBalancesOnCreateAndClaim( + acc1, acc2, asset, amount, claimants, *app); + } + } + } + + SECTION("validity checks") + { + SECTION("invalid amount") + { + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, 0, validClaimants), + ex_CREATE_CLAIMABLE_BALANCE_MALFORMED); + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, -1, validClaimants), + ex_CREATE_CLAIMABLE_BALANCE_MALFORMED); + } + SECTION("invalid claimants") + { + SECTION("empty claimants") + { + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, amount, {}), + ex_CREATE_CLAIMABLE_BALANCE_MALFORMED); + } + SECTION("duplicate claimants") + { + validClaimants.push_back(validClaimants.back()); + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, amount, + validClaimants), + ex_CREATE_CLAIMABLE_BALANCE_MALFORMED); + } + SECTION("invalid predicate") + { + auto invalidPredicateTest = + [&](ClaimPredicate const& pred) { + validClaimants.back().v0().predicate = pred; + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(asset, amount, + validClaimants), + ex_CREATE_CLAIMABLE_BALANCE_MALFORMED); + }; + + SECTION("invalid andPredicate size") + { + ClaimPredicate pred; + pred.type(CLAIM_PREDICATE_AND) + .andPredicates() + .emplace_back(makeSimplePredicate(1)); + invalidPredicateTest(pred); + } + SECTION("invalid orPredicate size") + { + ClaimPredicate pred; + pred.type(CLAIM_PREDICATE_OR) + .orPredicates() + .emplace_back(makeSimplePredicate(1)); + invalidPredicateTest(pred); + } + SECTION("invalid absBefore") + { + ClaimPredicate pred; + pred.type(CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME) + .absBefore() = -1; + invalidPredicateTest(pred); + } + SECTION("invalid absAfter") + { + ClaimPredicate pred; + pred.type(CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME) + .absAfter() = -1; + invalidPredicateTest(pred); + } + SECTION("invalid relBefore") + { + ClaimPredicate pred; + pred.type(CLAIM_PREDICATE_BEFORE_RELATIVE_TIME) + .relBefore() = -1; + invalidPredicateTest(pred); + } + SECTION("invalid relAfter") + { + ClaimPredicate pred; + pred.type(CLAIM_PREDICATE_AFTER_RELATIVE_TIME) + .relAfter() = -1; + invalidPredicateTest(pred); + } + SECTION("invalid predicate height") + { + invalidPredicateTest(makeSimplePredicate(4)); + } + } + } + } + + SECTION("claim balance") + { + uint32_t dayInSeconds = 86400; + TimePoint nextCloseTime; + { + LedgerTxn ltx(app->getLedgerTxnRoot()); + + // closeLedgerOn will move close time forward by a day + // (86400 seconds) + nextCloseTime = + ltx.loadHeader().current().scpValue.closeTime + + dayInSeconds; + } + + auto absBeforeFalse = + makePredicate(CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME, false, + nextCloseTime, dayInSeconds); + auto absAfterFalse = + makePredicate(CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME, false, + nextCloseTime, dayInSeconds); + auto relBeforeFalse = + makePredicate(CLAIM_PREDICATE_BEFORE_RELATIVE_TIME, false, + nextCloseTime, dayInSeconds); + auto relAfterFalse = + makePredicate(CLAIM_PREDICATE_AFTER_RELATIVE_TIME, false, + nextCloseTime, dayInSeconds); + + auto absBeforeTrue = + makePredicate(CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME, true, + nextCloseTime, dayInSeconds); + auto absAfterTrue = + makePredicate(CLAIM_PREDICATE_AFTER_ABSOLUTE_TIME, true, + nextCloseTime, dayInSeconds); + auto relBeforeTrue = + makePredicate(CLAIM_PREDICATE_BEFORE_RELATIVE_TIME, true, + nextCloseTime, dayInSeconds); + auto relAfterTrue = + makePredicate(CLAIM_PREDICATE_AFTER_RELATIVE_TIME, true, + nextCloseTime, dayInSeconds); + + SECTION("predicate not satisfied") + { + auto predicateTest = [&](ClaimPredicate const& pred) { + auto balanceID = acc1.createClaimableBalance( + asset, amount, {makeClaimant(acc2, pred)}); + + // move closeTime forward by a day + closeLedgerOn(*app, lm.getLastClosedLedgerNum() + 1, 2, + 1, 2016); + REQUIRE_THROWS_AS( + acc2.claimClaimableBalance(balanceID), + ex_CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM); + }; + + // validate 6 + SECTION("absBefore not satisfied") + { + predicateTest(absBeforeFalse); + } + SECTION("absAfter not satisfied") + { + predicateTest(absAfterFalse); + } + SECTION("relBefore not satisfied") + { + predicateTest(relBeforeFalse); + } + SECTION("relAfter not satisfied") + { + predicateTest(relAfterFalse); + } + SECTION("relAfter max not satisfied") + { + ClaimPredicate pred; + pred.type(CLAIM_PREDICATE_AFTER_RELATIVE_TIME); + pred.relAfter() = INT64_MAX; + predicateTest(pred); + } + SECTION("and predicate not satisfied") + { + predicateTest(makePredicate(CLAIM_PREDICATE_AND, false, + nextCloseTime, + dayInSeconds)); + } + SECTION("or predicate not satisfied") + { + predicateTest(makePredicate(CLAIM_PREDICATE_OR, false, + nextCloseTime, + dayInSeconds)); + } + SECTION("complex") + { + // will fail because left will return false to the top + // and predicate + auto right = + connectPredicate(true, absAfterTrue, absBeforeTrue); + auto left = connectPredicate(false, relBeforeFalse, + absAfterFalse); + predicateTest(connectPredicate(true, left, right)); + } + SECTION("complex 2") + { + auto right = + connectPredicate(true, absAfterTrue, absBeforeTrue); + auto left = connectPredicate(false, relBeforeFalse, + absAfterFalse); + auto connectedRight = + connectPredicate(true, left, right); + + predicateTest(connectPredicate(false, relAfterFalse, + connectedRight)); + } + SECTION("complex 3") + { + // full tree with all ORs. No predicate is satisfied + auto tree1 = connectPredicate(false, absAfterFalse, + absBeforeFalse); + auto tree2 = connectPredicate(false, relAfterFalse, + relBeforeFalse); + auto tree3 = connectPredicate(false, absAfterFalse, + relBeforeFalse); + auto tree4 = connectPredicate(false, relAfterFalse, + absBeforeFalse); + + auto c1 = connectPredicate(false, tree1, tree2); + auto c2 = connectPredicate(false, tree3, tree4); + predicateTest(connectPredicate(false, c1, c2)); + } + } + + SECTION("predicate satisfied") + { + auto predicateTest = [&](ClaimPredicate const& pred) { + auto balanceID = acc1.createClaimableBalance( + asset, amount, {makeClaimant(acc2, pred)}); + + // move closeTime forward by a day + closeLedgerOn(*app, lm.getLastClosedLedgerNum() + 1, 2, + 1, 2016); + acc2.claimClaimableBalance(balanceID); + }; + + SECTION("Unconditional") + { + ClaimPredicate p; + p.type(CLAIM_PREDICATE_UNCONDITIONAL); + predicateTest(p); + } + SECTION("absBefore satisfied") + { + predicateTest(absBeforeTrue); + } + SECTION("absAfter satisfied") + { + predicateTest(absAfterTrue); + } + SECTION("relBefore satisfied") + { + predicateTest(relBeforeTrue); + } + SECTION("relAfter satisfied") + { + predicateTest(relAfterTrue); + } + SECTION("relBefore max satisfied") + { + ClaimPredicate pred; + pred.type(CLAIM_PREDICATE_BEFORE_RELATIVE_TIME); + pred.relBefore() = INT64_MAX; + predicateTest(pred); + } + SECTION("and predicate satisfied") + { + predicateTest(makePredicate(CLAIM_PREDICATE_AND, true, + nextCloseTime, + dayInSeconds)); + } + SECTION("or predicate satisfied") + { + predicateTest(makePredicate(CLAIM_PREDICATE_OR, true, + nextCloseTime, + dayInSeconds)); + } + SECTION("complex 1") + { + auto right = + connectPredicate(true, absAfterTrue, absBeforeTrue); + auto left = connectPredicate(false, relBeforeTrue, + absAfterFalse); + predicateTest(connectPredicate(true, left, right)); + } + SECTION("complex 2") + { + auto right = + connectPredicate(true, absAfterTrue, absBeforeTrue); + auto left = connectPredicate(false, relBeforeTrue, + absAfterFalse); + auto connectedRight = + connectPredicate(true, left, right); + + predicateTest(connectPredicate(false, relAfterFalse, + connectedRight)); + } + SECTION("complex 3") + { + // full tree with all ANDs. All predicates are satisfied + auto tree1 = + connectPredicate(true, absAfterTrue, absBeforeTrue); + auto tree2 = + connectPredicate(true, relAfterTrue, relBeforeTrue); + auto tree3 = + connectPredicate(true, absAfterTrue, relBeforeTrue); + auto tree4 = + connectPredicate(true, relAfterTrue, absBeforeTrue); + + auto c1 = connectPredicate(true, tree1, tree2); + auto c2 = connectPredicate(true, tree3, tree4); + predicateTest(connectPredicate(true, c1, c2)); + } + SECTION("complex 4") + { + // full tree with all ORs. One predicate is satisfied + auto tree1 = connectPredicate(false, absAfterFalse, + absBeforeFalse); + auto tree2 = connectPredicate(false, relAfterFalse, + relBeforeFalse); + // absAfterTrue is used here + auto tree3 = connectPredicate(false, absAfterTrue, + relBeforeFalse); + auto tree4 = connectPredicate(false, relAfterFalse, + absBeforeFalse); + + auto c1 = connectPredicate(false, tree1, tree2); + auto c2 = connectPredicate(false, tree3, tree4); + predicateTest(connectPredicate(false, c1, c2)); + } + } + + SECTION("balance does not exist") + { + acc1.createClaimableBalance(asset, amount, validClaimants); + REQUIRE_THROWS_AS( + acc1.claimClaimableBalance(ClaimableBalanceID{}), + ex_CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST); + } + + SECTION("no destination match") + { + auto simpleBalanceID = acc1.createClaimableBalance( + asset, amount, validClaimants); + // issuer is not one of the claimants + REQUIRE_THROWS_AS( + issuer.claimClaimableBalance(simpleBalanceID), + ex_CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM); + } + } + + SECTION("multiple create and claims") + { + validateBalancesOnCreateAndClaim(acc1, acc2, asset, amount, + validClaimants, *app); + + fundForClaimableBalance(); + validateBalancesOnCreateAndClaim(acc1, acc2, asset, amount, + validClaimants, *app); + } + + SECTION("balanceID relies on sequence number") + { + // create two claimable balances (one here and one in + // validateBalancesOnCreateAndClaim) with the same parameters + auto balanceID = + acc1.createClaimableBalance(asset, amount, validClaimants); + + // add reserve so we can create another balance entry + root.pay(acc1, minBalance1); + + fundForClaimableBalance(); + validateBalancesOnCreateAndClaim(acc1, acc2, asset, amount, + validClaimants, *app); + + { + // check that the original claimable balance still exists. + // The balance created in validateBalancesOnCreateAndClaim + // is the one that was claimed + LedgerTxn ltx(app->getLedgerTxnRoot()); + REQUIRE(stellar::loadClaimableBalance(ltx, balanceID)); + } + } + + SECTION("successful createdBy == claimant") + { + auto createdByIsClaimant = [&](bool createAndClaimInSameTx) { + auto nativeBalancePre = acc1.getBalance(); + auto nonNativeBalancePre = + isNative ? 0 : acc1.loadTrustLine(asset).balance; + + if (createAndClaimInSameTx) + { + auto id = acc1.getBalanceID( + 0, acc1.getLastSequenceNumber() + 1); + auto tx = + acc1.tx({acc1.op(createClaimableBalance( + asset, amount, + {makeClaimant(acc1, simplePred)})), + acc1.op(claimClaimableBalance(id))}); + + // use closeLedger so we can apply a multi op tx and get + // the fee charged + closeLedgerOn(*app, lm.getLastClosedLedgerNum() + 1, 2, + 1, 2016, {tx}); + } + else + { + acc1.claimClaimableBalance(acc1.createClaimableBalance( + asset, amount, {makeClaimant(acc1, simplePred)})); + } + + auto nativeBalancePost = acc1.getBalance(); + auto nonNativeBalancePost = + isNative ? 0 : acc1.loadTrustLine(asset).balance; + + int64_t feeCharged = lm.getLastTxFee() * 2; + REQUIRE(nativeBalancePost == nativeBalancePre - feeCharged); + REQUIRE(nonNativeBalancePost == nonNativeBalancePre); + }; + + SECTION("create and claim in different tx") + { + createdByIsClaimant(false); + } + SECTION("create and claim in same tx") + { + createdByIsClaimant(true); + } + } + }; + + SECTION("native") + { + int64_t const amount = lm.getLastReserve(); + balanceTestByAsset(native, amount * 2); + } + + SECTION("non-native") + { + balanceTestByAsset(usd, 100); + } + + SECTION("invalid asset") + { + usd.alphaNum4().assetCode[0] = 0; + REQUIRE_THROWS_AS( + acc1.createClaimableBalance(usd, 100, validClaimants), + ex_CREATE_CLAIMABLE_BALANCE_MALFORMED); + } + + SECTION("merge create account before claim") + { + validateBalancesOnCreateAndClaim(acc1, acc2, native, 100, + validClaimants, *app, true); + } + + SECTION("claim claimable trustline issues") + { + issuer.pay(acc2, usd, trustLineLimit); + + auto acc3 = root.create("acc3", minBalance3); + + auto balanceID = acc2.createClaimableBalance( + usd, trustLineLimit - 100, {makeClaimant(acc3, simplePred)}); + + REQUIRE_THROWS_AS(acc3.claimClaimableBalance(balanceID), + ex_CLAIM_CLAIMABLE_BALANCE_NO_TRUST); + + issuer.setOptions( + setFlags(static_cast(AUTH_REQUIRED_FLAG))); + acc3.changeTrust(usd, trustLineLimit); + + REQUIRE_THROWS_AS(acc3.claimClaimableBalance(balanceID), + ex_CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED); + + issuer.allowTrust(usd, acc3); + // claimable balance amount is limit - 100, so acc3 will not be able + // to claim it with a current balance of 101 + issuer.pay(acc3, usd, 101); + + REQUIRE_THROWS_AS(acc3.claimClaimableBalance(balanceID), + ex_CLAIM_CLAIMABLE_BALANCE_LINE_FULL); + + // bring acc3 balance down so it doesn't exceed the trustline limit + // on claim + acc3.pay(issuer, usd, 1); + acc3.claimClaimableBalance(balanceID); + } + + SECTION("native line full") + { + // issuers native line is full + issuer.manageOffer(0, usd, native, Price{1, 1}, + INT64_MAX - issuer.getBalance()); + + auto reserve = lm.getLastReserve(); + + SECTION("native claim amount results in line full") + { + auto amount = issuer.getBalance(); + root.pay(acc2, amount); + + auto balanceId = acc2.createClaimableBalance( + native, amount, {makeClaimant(issuer, simplePred)}); + + REQUIRE_THROWS_AS(issuer.claimClaimableBalance(balanceId), + ex_CLAIM_CLAIMABLE_BALANCE_LINE_FULL); + } + + SECTION("not enough space left for reserve to be paid back to " + "balance creator on claim") + { + SECTION("createdBy != claimant") + { + auto balanceId = + issuer.createClaimableBalance(usd, 100, validClaimants); + + // increase issuer native balance by reserve + root.pay(issuer, reserve); + + SECTION("reserve is sent to claiming account") + { + auto issuerBalanceBeforeClaim = issuer.getBalance(); + auto acc2BalanceBeforeClaim = acc2.getBalance(); + + acc2.claimClaimableBalance(balanceId); + + // check that the reserve was sent to acc2 (the claiming + // account) + REQUIRE(issuerBalanceBeforeClaim == + issuer.getBalance()); + REQUIRE(acc2BalanceBeforeClaim + reserve - + lm.getLastTxFee() == + acc2.getBalance()); + } + + SECTION("use create account to claim") + { + auto issuerBalanceBeforeClaim = issuer.getBalance(); + auto acc2BalanceBeforeClaim = acc2.getBalance(); + + acc2.claimClaimableBalance(balanceId); + + // check that the reserve was sent to acc2 (the claiming + // account) + REQUIRE(issuerBalanceBeforeClaim == + issuer.getBalance()); + REQUIRE(acc2BalanceBeforeClaim + reserve - + lm.getLastTxFee() == + acc2.getBalance()); + } + + SECTION("no space left in both accounts") + { + auto idr = makeAsset(acc2, "IDR"); + + // acc2 native line is full + acc2.manageOffer(0, idr, native, Price{1, 1}, + INT64_MAX - acc2.getBalance()); + + REQUIRE_THROWS_AS(acc2.claimClaimableBalance(balanceId), + ex_CLAIM_CLAIMABLE_BALANCE_LINE_FULL); + } + } + + SECTION("createdBy == claimant") + { + // issuer is the creator and claimant + + auto balanceId = issuer.createClaimableBalance( + usd, 1, {makeClaimant(issuer, simplePred)}); + + // increase issuer native balance by reserve. + root.pay(issuer, reserve); + + // reserve can't be paid back to the the createdBy account + // or the sourceAccount (both the issuer in this case) + REQUIRE_THROWS_AS(issuer.claimClaimableBalance(balanceId), + ex_CLAIM_CLAIMABLE_BALANCE_LINE_FULL); + } + } + } + + SECTION("multiple creates in tx to test index in hash") + { + root.pay(acc1, minBalance1); + auto op1 = createClaimableBalance(native, 1, validClaimants); + auto op2 = createClaimableBalance(native, 2, validClaimants); + + auto tx = acc1.tx({op1, op2}); + applyCheck(tx, *app); + + auto balanceID1 = acc1.getBalanceID(0); + auto balanceID2 = acc1.getBalanceID(1); + + LedgerTxn ltx(app->getLedgerTxnRoot()); + auto entry1 = + stellar::loadClaimableBalance(ltx, acc1.getBalanceID(0)); + auto entry2 = + stellar::loadClaimableBalance(ltx, acc1.getBalanceID(1)); + + REQUIRE((entry1 && entry2)); + + auto validateEntry = [&](LedgerTxnEntry const& entry, + ClaimableBalanceID balanceID, + int64_t amount) { + auto const& claimableBalance = + entry.current().data.claimableBalance(); + REQUIRE(claimableBalance.asset == native); + REQUIRE(claimableBalance.amount == amount); + REQUIRE(claimableBalance.balanceID == balanceID); + REQUIRE(claimableBalance.createdBy == acc1.getPublicKey()); + REQUIRE(static_cast(claimableBalance.reserve) == + validClaimants.size() * lm.getLastReserve()); + }; + + validateEntry(entry1, balanceID1, 1); + validateEntry(entry2, balanceID2, 2); + } + + SECTION("multiple claimants try to claim balance in same tx") + { + // add acc2 as signer on issuer account so acc2 can execute an op + // from issuer + auto sk1 = makeSigner(acc2, 100); + issuer.setOptions(setSigner(sk1)); + + validClaimants.emplace_back( + makeClaimant(root, makeSimplePredicate(2))); + + // acc2 and root are claimants + auto balanceID = + acc1.createClaimableBalance(native, 1, validClaimants); + auto op = claimClaimableBalance(balanceID); + + auto acc2Balance = acc2.getBalance(); + auto issuerBalance = issuer.getBalance(); + + // submit tx with 2 claims + auto tx = acc2.tx({op, issuer.op(op)}); + applyCheck(tx, *app); + + REQUIRE(tx->getResultCode() == txFAILED); + auto const& innerRes = tx->getResult().result.results(); + + REQUIRE(innerRes[0].code() == opINNER); + REQUIRE(innerRes[1].code() == opINNER); + + // first op consumed balance entry, so second op failed + REQUIRE(innerRes[0].tr().claimClaimableBalanceResult().code() == + CLAIM_CLAIMABLE_BALANCE_SUCCESS); + REQUIRE(innerRes[1].tr().claimClaimableBalanceResult().code() == + CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST); + + // only change should be the fee paid for 2 ops + REQUIRE(acc2Balance - lm.getLastTxFee() * 2 == acc2.getBalance()); + REQUIRE(issuerBalance == issuer.getBalance()); + } + + SECTION("tx account is different than op account on successful create") + { + // Allow acc1 to submit an op from acc2. + root.pay(acc2, lm.getLastMinBalance(1)); + auto sk1 = makeSigner(acc1, 100); + acc2.setOptions(setSigner(sk1)); + + applyTx(acc1.tx({acc2.op( + createClaimableBalance(native, 1, validClaimants))}), + *app); + + // second claimable balance uses acc1 for key + auto id = acc1.getBalanceID(0); + acc2.claimClaimableBalance(id); + } + + SECTION("validate tx account is used in hash") + { + // make new accounts so sequence numbers are the same + auto accA = root.create("accA", minBalance3); + auto accB = root.create("accB", lm.getLastMinBalance(4)); + + // Allow accA to submit an op from accB. This will bump accB's + // seqnum up by 1 + auto sk1 = makeSigner(accA, 100); + accB.setOptions(setSigner(sk1)); + + // Move accA seqnum up by one so accA and accB have the same seqnum + accA.bumpSequence(accB.getLastSequenceNumber()); + REQUIRE(accA.getLastSequenceNumber() == + accB.getLastSequenceNumber()); + + // accB and accA have the same seq num. Create a claimable balance + // with accB twice. Once using accB as the Tx account, and once with + // accA as the Tx account. If the claimable balance key uses the + // operation source account and the tx source account seqnum for + // it's hash, then the second create will fail because the keys will + // match. We want to make sure that the tx account and the seqnum of + // that account are used for the claimable balance key + auto id1 = accB.createClaimableBalance(native, 100, validClaimants); + applyTx(accA.tx({accB.op( + createClaimableBalance(native, 100, validClaimants))}), + *app); + + // second claimable balance uses accA for key + auto id2 = accA.getBalanceID(0); + + // claim both balances + acc2.claimClaimableBalance(id1); + acc2.claimClaimableBalance(id2); + } + }); +} From 7d93e75ab28582b7d85eb92ecec14538b8d03068 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:54:14 -0700 Subject: [PATCH 18/21] update BucketApplicator for ClaimableBalance --- src/bucket/BucketApplicator.cpp | 31 +++++++++++++++++++++++-------- src/bucket/BucketApplicator.h | 5 ++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/bucket/BucketApplicator.cpp b/src/bucket/BucketApplicator.cpp index 371fc1582c..93d4cc8ea9 100644 --- a/src/bucket/BucketApplicator.cpp +++ b/src/bucket/BucketApplicator.cpp @@ -100,6 +100,8 @@ BucketApplicator::Counters::reset(VirtualClock::time_point now) mOfferDelete = 0; mDataUpsert = 0; mDataDelete = 0; + mClaimableBalanceUpsert = 0; + mClaimableBalanceDelete = 0; } void @@ -108,6 +110,7 @@ BucketApplicator::Counters::getRates(VirtualClock::time_point now, uint64_t& tu_sec, uint64_t& td_sec, uint64_t& ou_sec, uint64_t& od_sec, uint64_t& du_sec, uint64_t& dd_sec, + uint64_t& cu_sec, uint64_t& cd_sec, uint64_t& T_sec, uint64_t& total) { VirtualClock::duration dur = now - mStarted; @@ -115,7 +118,7 @@ BucketApplicator::Counters::getRates(VirtualClock::time_point now, uint64_t usecs = usec.count() + 1; total = mAccountUpsert + mAccountDelete + mTrustLineUpsert + mTrustLineDelete + mOfferUpsert + mOfferDelete + mDataUpsert + - mDataDelete; + mDataDelete + mClaimableBalanceUpsert + mClaimableBalanceDelete; au_sec = (mAccountUpsert * 1000000) / usecs; ad_sec = (mAccountDelete * 1000000) / usecs; tu_sec = (mTrustLineUpsert * 1000000) / usecs; @@ -124,6 +127,8 @@ BucketApplicator::Counters::getRates(VirtualClock::time_point now, od_sec = (mOfferDelete * 1000000) / usecs; du_sec = (mDataUpsert * 1000000) / usecs; dd_sec = (mDataDelete * 1000000) / usecs; + cu_sec = (mClaimableBalanceUpsert * 1000000) / usecs; + cd_sec = (mClaimableBalanceDelete * 1000000) / usecs; T_sec = (total * 1000000) / usecs; } @@ -133,22 +138,25 @@ BucketApplicator::Counters::logInfo(std::string const& bucketName, VirtualClock::time_point now) { uint64_t au_sec, ad_sec, tu_sec, td_sec, ou_sec, od_sec, du_sec, dd_sec, - T_sec, total; + cu_sec, cd_sec, T_sec, total; getRates(now, au_sec, ad_sec, tu_sec, td_sec, ou_sec, od_sec, du_sec, - dd_sec, T_sec, total); + dd_sec, cu_sec, cd_sec, T_sec, total); CLOG(INFO, "Bucket") << "Apply-rates for " << total << "-entry bucket " << level << "." << bucketName << " au:" << au_sec << " ad:" << ad_sec << " tu:" << tu_sec << " td:" << td_sec << " ou:" << ou_sec << " od:" << od_sec << " du:" << du_sec - << " dd:" << dd_sec << " T:" << T_sec; + << " dd:" << dd_sec << " cu:" << cu_sec + << " cd:" << cd_sec << " T:" << T_sec; CLOG(INFO, "Bucket") << "Entry-counts for " << total << "-entry bucket " << level << "." << bucketName << " au:" << mAccountUpsert << " ad:" << mAccountDelete << " tu:" << mTrustLineUpsert << " td:" << mTrustLineDelete << " ou:" << mOfferUpsert << " od:" << mOfferDelete << " du:" << mDataUpsert - << " dd:" << mDataDelete; + << " dd:" << mDataDelete + << " cu:" << mClaimableBalanceUpsert + << " cd:" << mClaimableBalanceDelete; } void @@ -157,15 +165,16 @@ BucketApplicator::Counters::logDebug(std::string const& bucketName, VirtualClock::time_point now) { uint64_t au_sec, ad_sec, tu_sec, td_sec, ou_sec, od_sec, du_sec, dd_sec, - T_sec, total; + cu_sec, cd_sec, T_sec, total; getRates(now, au_sec, ad_sec, tu_sec, td_sec, ou_sec, od_sec, du_sec, - dd_sec, T_sec, total); + dd_sec, cu_sec, cd_sec, T_sec, total); CLOG(DEBUG, "Bucket") << "Apply-rates for " << total << "-entry bucket " << level << "." << bucketName << " au:" << au_sec << " ad:" << ad_sec << " tu:" << tu_sec << " td:" << td_sec << " ou:" << ou_sec << " od:" << od_sec << " du:" << du_sec - << " dd:" << dd_sec << " T:" << T_sec; + << " dd:" << dd_sec << " cu:" << cu_sec + << " cd:" << cd_sec << " T:" << T_sec; } void @@ -187,6 +196,9 @@ BucketApplicator::Counters::mark(BucketEntry const& e) case DATA: ++mDataUpsert; break; + case CLAIMABLE_BALANCE: + ++mClaimableBalanceUpsert; + break; } } else @@ -205,6 +217,9 @@ BucketApplicator::Counters::mark(BucketEntry const& e) case DATA: ++mDataDelete; break; + case CLAIMABLE_BALANCE: + ++mClaimableBalanceDelete; + break; } } } diff --git a/src/bucket/BucketApplicator.h b/src/bucket/BucketApplicator.h index 9b83de5b90..15b14cdff3 100644 --- a/src/bucket/BucketApplicator.h +++ b/src/bucket/BucketApplicator.h @@ -42,10 +42,13 @@ class BucketApplicator uint64_t mOfferDelete; uint64_t mDataUpsert; uint64_t mDataDelete; + uint64_t mClaimableBalanceUpsert; + uint64_t mClaimableBalanceDelete; void getRates(VirtualClock::time_point now, uint64_t& au_sec, uint64_t& ad_sec, uint64_t& tu_sec, uint64_t& td_sec, uint64_t& ou_sec, uint64_t& od_sec, uint64_t& du_sec, - uint64_t& dd_sec, uint64_t& T_sec, uint64_t& total); + uint64_t& dd_sec, uint64_t& cu_sec, uint64_t& cd_sec, + uint64_t& T_sec, uint64_t& total); public: Counters(VirtualClock::time_point now); From 0395ebe0cb6548a6b5fed7760d595134496ed714 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:54:18 -0700 Subject: [PATCH 19/21] update bucket test for ClaimableBalance --- src/bucket/test/BucketTests.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/bucket/test/BucketTests.cpp b/src/bucket/test/BucketTests.cpp index 6a32051a37..451e87f77f 100644 --- a/src/bucket/test/BucketTests.cpp +++ b/src/bucket/test/BucketTests.cpp @@ -192,6 +192,10 @@ TEST_CASE("merging bucket entries", "[bucket]") liveEntry.data.data() = LedgerTestUtils::generateValidDataEntry(10); break; + case CLAIMABLE_BALANCE: + liveEntry.data.claimableBalance() = + LedgerTestUtils::generateValidClaimableBalanceEntry(10); + break; default: abort(); } @@ -217,6 +221,7 @@ TEST_CASE("merging bucket entries", "[bucket]") checkDeadAnnihilatesLive(TRUSTLINE); checkDeadAnnihilatesLive(OFFER); checkDeadAnnihilatesLive(DATA); + checkDeadAnnihilatesLive(CLAIMABLE_BALANCE); SECTION("random dead entries annihilates live entries") { From 34aaa7e2670ac1a1a7ec99883405a8e9f2dda5cb Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:54:21 -0700 Subject: [PATCH 20/21] add claimable balance to db-schema.md --- docs/db-schema.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/db-schema.md b/docs/db-schema.md index 380fdecacb..f34c5157ac 100644 --- a/docs/db-schema.md +++ b/docs/db-schema.md @@ -113,6 +113,18 @@ extension | TEXT | Extension specific to DataEntry (XDR) ledgerext | TEXT | Extension common to all LedgerEntry types (XDR) (accountid, dataname) | PRIMARY KEY | +## claimablebalance + +Defined in [`src/ledger/LedgerTxnClaimableBalanceSQL.cpp`](/src/ledger/LedgerTxnClaimableBalanceSQL.cpp) + +Equivalent to _ClaimableBalanceEntry_ + +Field | Type | Description +------|------|--------------- +balanceid | VARCHAR(48) PRIMARY KEY | This is a ClaimableBalanceID (XDR) +ledgerentry | TEXT NOT NULL | LedgerEntry that contains a ClaimableBalanceEntry (XDR) +lastmodified | INT NOT NULL | lastModifiedLedgerSeq + ## txhistory Defined in [`src/transactions/TransactionFrame.cpp`](/src/transactions/TransactionFrame.cpp) From 29c1cab39b767d86079d19ca5b9384bd14231117 Mon Sep 17 00:00:00 2001 From: Siddharth Suresh Date: Tue, 30 Jun 2020 12:54:25 -0700 Subject: [PATCH 21/21] add claimable balance to VS project files --- Builds/VisualStudio/stellar-core.vcxproj | 6 ++++++ .../VisualStudio/stellar-core.vcxproj.filters | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Builds/VisualStudio/stellar-core.vcxproj b/Builds/VisualStudio/stellar-core.vcxproj index 4103122db1..be19e493b5 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj +++ b/Builds/VisualStudio/stellar-core.vcxproj @@ -318,6 +318,7 @@ exit /b 0 + @@ -325,6 +326,8 @@ exit /b 0 + + @@ -338,6 +341,7 @@ exit /b 0 + @@ -658,6 +662,8 @@ exit /b 0 + + diff --git a/Builds/VisualStudio/stellar-core.vcxproj.filters b/Builds/VisualStudio/stellar-core.vcxproj.filters index 9fdc49c576..0f4369f43f 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj.filters +++ b/Builds/VisualStudio/stellar-core.vcxproj.filters @@ -1074,6 +1074,18 @@ lib\tracy + + ledger + + + transactions + + + transactions + + + transactions\tests + @@ -1868,6 +1880,12 @@ lib\fmt + + transactions + + + transactions +