diff --git a/Builds/CMake/CMakeFuncs.cmake b/Builds/CMake/CMakeFuncs.cmake index fb60fd9b4eb..fe51b3e320a 100644 --- a/Builds/CMake/CMakeFuncs.cmake +++ b/Builds/CMake/CMakeFuncs.cmake @@ -18,8 +18,10 @@ macro(group_sources curdir) endmacro() macro (exclude_from_default target_) - set_target_properties (${target_} PROPERTIES EXCLUDE_FROM_ALL ON) - set_target_properties (${target_} PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD ON) + if(target_) + set_target_properties (${target_} PROPERTIES EXCLUDE_FROM_ALL ON) + set_target_properties (${target_} PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD ON) + endif() endmacro () macro (exclude_if_included target_) diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 5f5dbf5eb0a..1cc9bb6de50 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -103,7 +103,9 @@ target_sources (xrpl_core PRIVATE src/ripple/protocol/impl/STObject.cpp src/ripple/protocol/impl/STParsedJSON.cpp src/ripple/protocol/impl/STPathSet.cpp + src/ripple/protocol/impl/STXChainBridge.cpp src/ripple/protocol/impl/STTx.cpp + src/ripple/protocol/impl/XChainAttestations.cpp src/ripple/protocol/impl/STValidation.cpp src/ripple/protocol/impl/STVar.cpp src/ripple/protocol/impl/STVector256.cpp @@ -243,6 +245,7 @@ install ( src/ripple/protocol/Indexes.h src/ripple/protocol/InnerObjectFormats.h src/ripple/protocol/Issue.h + src/ripple/protocol/json_get_or_throw.h src/ripple/protocol/KeyType.h src/ripple/protocol/Keylet.h src/ripple/protocol/KnownFormats.h @@ -273,6 +276,8 @@ install ( src/ripple/protocol/STParsedJSON.h src/ripple/protocol/STPathSet.h src/ripple/protocol/STTx.h + src/ripple/protocol/XChainAttestations.h + src/ripple/protocol/STXChainBridge.h src/ripple/protocol/STValidation.h src/ripple/protocol/STVector256.h src/ripple/protocol/SecretKey.h @@ -528,6 +533,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/SetRegularKey.cpp src/ripple/app/tx/impl/SetSignerList.cpp src/ripple/app/tx/impl/SetTrust.cpp + src/ripple/app/tx/impl/XChainBridge.cpp src/ripple/app/tx/impl/SignerEntries.cpp src/ripple/app/tx/impl/Taker.cpp src/ripple/app/tx/impl/Transactor.cpp @@ -809,6 +815,7 @@ if (tests) src/test/app/ReducedOffer_test.cpp src/test/app/Regression_test.cpp src/test/app/SHAMapStore_test.cpp + src/test/app/XChain_test.cpp src/test/app/SetAuth_test.cpp src/test/app/SetRegularKey_test.cpp src/test/app/SetTrust_test.cpp @@ -927,6 +934,7 @@ if (tests) src/test/jtx/impl/acctdelete.cpp src/test/jtx/impl/account_txn_id.cpp src/test/jtx/impl/amount.cpp + src/test/jtx/impl/attester.cpp src/test/jtx/impl/balance.cpp src/test/jtx/impl/check.cpp src/test/jtx/impl/delivermin.cpp @@ -948,6 +956,7 @@ if (tests) src/test/jtx/impl/regkey.cpp src/test/jtx/impl/sendmax.cpp src/test/jtx/impl/seq.cpp + src/test/jtx/impl/xchain_bridge.cpp src/test/jtx/impl/sig.cpp src/test/jtx/impl/tag.cpp src/test/jtx/impl/ticket.cpp diff --git a/Builds/CMake/RippledRelease.cmake b/Builds/CMake/RippledRelease.cmake index a0ad3696572..0e712d8a566 100644 --- a/Builds/CMake/RippledRelease.cmake +++ b/Builds/CMake/RippledRelease.cmake @@ -2,6 +2,12 @@ package/container targets - (optional) #]===================================================================] +# Early return if the `containers` directory is missing, +# e.g. when we are building a Conan package. +if(NOT EXISTS containers) + return() +endif() + if (is_root_project) if (NOT DOCKER) find_program (DOCKER docker) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5723c831ad4..3f5e3e686b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # make GIT_COMMIT_HASH define available to all sources find_package(Git) if(Git_FOUND) - execute_process(COMMAND ${GIT_EXECUTABLE} describe --always --abbrev=40 + execute_process(COMMAND ${GIT_EXECUTABLE} --git-dir=${CMAKE_CURRENT_SOURCE_DIR}/.git describe --always --abbrev=40 OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE gch) if(gch) set(GIT_COMMIT_HASH "${gch}") diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 83cf762cfcb..14b816e4564 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -1279,6 +1279,12 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) } } + if (auto const& forcedRange = config().FORCED_LEDGER_RANGE_PRESENT) + { + m_ledgerMaster->setLedgerRangePresent( + forcedRange->first, forcedRange->second); + } + if (!config().reporting()) m_orderBookDB.setup(getLedgerMaster().getCurrentLedger()); diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 0ce8b085971..84c74b8924d 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -378,8 +378,13 @@ run(int argc, char** argv) "Override the minimum validation quorum.")( "reportingReadOnly", "Run in read-only reporting mode")( "silent", "No output to the console after startup.")( - "standalone,a", "Run with no peers.")("verbose,v", "Verbose logging.")( - "version", "Display the build version."); + "standalone,a", "Run with no peers.")("verbose,v", "Verbose logging.") + + ("force_ledger_present_range", + po::value(), + "Specify the range of present ledgers for testing purposes. Min and " + "max values are comma separated.")( + "version", "Display the build version."); po::options_description data("Ledger/Data Options"); data.add_options()("import", importText.c_str())( @@ -602,6 +607,51 @@ run(int argc, char** argv) return 0; } + if (vm.contains("force_ledger_present_range")) + { + try + { + auto const r = [&vm]() -> std::vector { + std::vector strVec; + boost::split( + strVec, + vm["force_ledger_present_range"].as(), + boost::algorithm::is_any_of(",")); + std::vector result; + for (auto& s : strVec) + { + boost::trim(s); + if (!s.empty()) + result.push_back(std::stoi(s)); + } + return result; + }(); + + if (r.size() == 2) + { + if (r[0] > r[1]) + { + throw std::runtime_error( + "Invalid force_ledger_present_range parameter"); + } + config->FORCED_LEDGER_RANGE_PRESENT.emplace(r[0], r[1]); + } + else + { + throw std::runtime_error( + "Invalid force_ledger_present_range parameter"); + } + } + catch (std::exception const& e) + { + std::cerr << "invalid 'force_ledger_present_range' parameter. The " + "parameter must be two numbers separated by a comma. " + "The first number must be <= the second." + << std::endl; + return -1; + } + } + if (vm.count("start")) { config->START_UP = Config::FRESH; diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 95ca09e0cbd..4e91a9d32f6 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -607,12 +607,14 @@ class NetworkOPsImp final : public NetworkOPs void pubValidatedTransaction( std::shared_ptr const& ledger, - AcceptedLedgerTx const& transaction); + AcceptedLedgerTx const& transaction, + bool last); void pubAccountTransaction( std::shared_ptr const& ledger, - AcceptedLedgerTx const& transaction); + AcceptedLedgerTx const& transaction, + bool last); void pubProposedAccountTransaction( @@ -3031,7 +3033,8 @@ NetworkOPsImp::pubLedger(std::shared_ptr const& lpAccepted) for (auto const& accTx : *alpAccepted) { JLOG(m_journal.trace()) << "pubAccepted: " << accTx->getJson(); - pubValidatedTransaction(lpAccepted, *accTx); + pubValidatedTransaction( + lpAccepted, *accTx, accTx == *(--alpAccepted->end())); } } @@ -3138,7 +3141,8 @@ NetworkOPsImp::transJson( void NetworkOPsImp::pubValidatedTransaction( std::shared_ptr const& ledger, - const AcceptedLedgerTx& transaction) + const AcceptedLedgerTx& transaction, + bool last) { auto const& stTxn = transaction.getTxn(); @@ -3187,13 +3191,14 @@ NetworkOPsImp::pubValidatedTransaction( if (transaction.getResult() == tesSUCCESS) app_.getOrderBookDB().processTxn(ledger, transaction, jvObj); - pubAccountTransaction(ledger, transaction); + pubAccountTransaction(ledger, transaction, last); } void NetworkOPsImp::pubAccountTransaction( std::shared_ptr const& ledger, - AcceptedLedgerTx const& transaction) + AcceptedLedgerTx const& transaction, + bool last) { hash_set notify; int iProposed = 0; @@ -3301,6 +3306,9 @@ NetworkOPsImp::pubAccountTransaction( for (InfoSub::ref isrListener : notify) isrListener->send(jvObj, true); + if (last) + jvObj[jss::account_history_boundary] = true; + assert(!jvObj.isMember(jss::account_history_tx_stream)); for (auto& info : accountHistoryNotify) { @@ -3699,8 +3707,11 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) auto const& txns = dbResult->first; marker = dbResult->second; - for (auto const& [tx, meta] : txns) + size_t num_txns = txns.size(); + for (size_t i = 0; i < num_txns; ++i) { + auto const& [tx, meta] = txns[i]; + if (!tx || !meta) { JLOG(m_journal.debug()) @@ -3735,6 +3746,11 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) *stTxn, meta->getResultTER(), true, curTxLedger); jvTx[jss::meta] = meta->getJson(JsonOptions::none); jvTx[jss::account_history_tx_index] = txHistoryIndex--; + + if (i + 1 == num_txns || + txns[i + 1].first->getLedger() != tx->getLedger()) + jvTx[jss::account_history_boundary] = true; + RPC::insertDeliveredAmount( jvTx[jss::meta], *curTxLedger, stTxn, *meta); if (isFirstTx(tx, meta)) diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 907611f1c9a..7b1ac7d08df 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include namespace ripple { @@ -387,6 +388,9 @@ LedgerEntryTypesMatch::visitEntry( case ltNFTOKEN_PAGE: case ltNFTOKEN_OFFER: case ltAMM: + case ltBRIDGE: + case ltXCHAIN_OWNED_CLAIM_ID: + case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: break; default: invalidTypeAdded_ = true; @@ -487,7 +491,9 @@ ValidNewAccountRoot::finalize( } // From this point on we know exactly one account was created. - if ((tx.getTxnType() == ttPAYMENT || tx.getTxnType() == ttAMM_CREATE) && + if ((tx.getTxnType() == ttPAYMENT || tx.getTxnType() == ttAMM_CREATE || + tx.getTxnType() == ttXCHAIN_ADD_CLAIM_ATTESTATION || + tx.getTxnType() == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) && result == tesSUCCESS) { std::uint32_t const startingSeq{ diff --git a/src/ripple/app/tx/impl/InvariantCheck.h b/src/ripple/app/tx/impl/InvariantCheck.h index 5194a9c34c6..eb606c2ed3b 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.h +++ b/src/ripple/app/tx/impl/InvariantCheck.h @@ -25,6 +25,7 @@ #include #include #include + #include #include #include @@ -300,7 +301,7 @@ class NoZeroEscrow class ValidNewAccountRoot { std::uint32_t accountsCreated_ = 0; - std::uint32_t accountSeq_ = 0; // Only meaningful if accountsCreated_ > 0 + std::uint32_t accountSeq_ = 0; public: void diff --git a/src/ripple/app/tx/impl/XChainBridge.cpp b/src/ripple/app/tx/impl/XChainBridge.cpp new file mode 100644 index 00000000000..7b924ec1636 --- /dev/null +++ b/src/ripple/app/tx/impl/XChainBridge.cpp @@ -0,0 +1,2292 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple { + +/* + Bridges connect two independent ledgers: a "locking chain" and an "issuing + chain". An asset can be moved from the locking chain to the issuing chain by + putting it into trust on the locking chain, and issuing a "wrapped asset" + that represents the locked asset on the issuing chain. + + Note that a bridge is not an exchange. There is no exchange rate: one wrapped + asset on the issuing chain always represents one asset in trust on the + locking chain. The bridge also does not exchange an asset on the locking + chain for an asset on the issuing chain. + + A good model for thinking about bridges is a box that contains an infinite + number of "wrapped tokens". When a token from the locking chain + (locking-chain-token) is put into the box, a wrapped token is taken out of + the box and put onto the issuing chain (issuing-chain-token). No one can use + the locking-chain-token while it remains in the box. When an + issuing-chain-token is returned to the box, one locking-chain-token is taken + out of the box and put back onto the locking chain. + + This requires a way to put assets into trust on one chain (put a + locking-chain-token into the box). A regular XRP account is used for this. + This account is called a "door account". Much in the same way that a door is + used to go from one room to another, a door account is used to move from one + chain to another. This account will be jointly controlled by a set of witness + servers by using the ledger's multi-signature support. The master key will be + disabled. These witness servers are trusted in the sense that if a quorum of + them collude, they can steal the funds put into trust. + + This also requires a way to prove that assets were put into the box - either + a locking-chain-token on the locking chain or returning an + issuing-chain-token on the issuing chain. A set of servers called "witness + servers" fill this role. These servers watch the ledger for these + transactions, and attest that the given events happened on the different + chains by signing messages with the event information. + + There needs to be a way to prevent the attestations from the witness + servers from being used more than once. "Claim ids" fill this role. A claim + id must be acquired on the destination chain before the asset is "put into + the box" on the source chain. This claim id has a unique id, and once it is + destroyed it can never exist again (it's a simple counter). The attestations + reference this claim id, and are accumulated on the claim id. Once a quorum + is reached, funds can move. Once the funds move, the claim id is destroyed. + + Finally, a claim id requires that the sender has an account on the + destination chain. For some chains, this can be a problem - especially if + the wrapped asset represents XRP, and XRP is needed to create an account. + There's a bootstrap problem. To address this, there is a special transaction + used to create accounts. This transaction does not require a claim id. + + See the document "docs/bridge/spec.md" for a full description of how + bridges and their transactions work. +*/ + +namespace { + +// Check that the public key is allowed to sign for the given account. If the +// account does not exist on the ledger, then the public key must be the master +// key for the given account if it existed. Otherwise the key must be an enabled +// master key or a regular key for the existing account. +TER +checkAttestationPublicKey( + ReadView const& view, + std::unordered_map const& signersList, + AccountID const& attestationSignerAccount, + PublicKey const& pk, + beast::Journal j) +{ + if (!signersList.contains(attestationSignerAccount)) + { + return tecNO_PERMISSION; + } + + AccountID const accountFromPK = calcAccountID(pk); + + if (auto const sleAttestationSigningAccount = + view.read(keylet::account(attestationSignerAccount))) + { + if (accountFromPK == attestationSignerAccount) + { + // master key + if (sleAttestationSigningAccount->getFieldU32(sfFlags) & + lsfDisableMaster) + { + JLOG(j.trace()) << "Attempt to add an attestation with " + "disabled master key."; + return tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR; + } + } + else + { + // regular key + if (std::optional regularKey = + (*sleAttestationSigningAccount)[~sfRegularKey]; + regularKey != accountFromPK) + { + if (!regularKey) + { + JLOG(j.trace()) + << "Attempt to add an attestation with " + "account present and non-present regular key."; + } + else + { + JLOG(j.trace()) << "Attempt to add an attestation with " + "account present and mismatched " + "regular key/public key."; + } + return tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR; + } + } + } + else + { + // account does not exist. + if (calcAccountID(pk) != attestationSignerAccount) + { + JLOG(j.trace()) + << "Attempt to add an attestation with non-existant account " + "and mismatched pk/account pair."; + return tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR; + } + } + + return tesSUCCESS; +} + +// If there is a quorum of attestations for the given parameters, then +// return the reward accounts, otherwise return TER for the error. +// Also removes attestations that are no longer part of the signers list. +// +// Note: the dst parameter is what the attestations are attesting to, which +// is not always used (it is used when automatically triggering a transfer +// from an `addAttestation` transaction, it is not used in a `claim` +// transaction). If the `checkDst` parameter is `check`, the attestations +// must attest to this destination, if it is `ignore` then the `dst` of the +// attestations are not checked (as for a `claim` transaction) + +enum class CheckDst { check, ignore }; +template +Expected, TER> +claimHelper( + XChainAttestationsBase& attestations, + ReadView const& view, + typename TAttestation::MatchFields const& toMatch, + CheckDst checkDst, + std::uint32_t quorum, + std::unordered_map const& signersList, + beast::Journal j) +{ + // Remove attestations that are not valid signers. They may be no longer + // part of the signers list, or their master key may have been disabled, + // or their regular key may have changed + attestations.erase_if([&](auto const& a) { + return checkAttestationPublicKey( + view, signersList, a.keyAccount, a.publicKey, j) != + tesSUCCESS; + }); + + // Check if we have quorum for the amount specified on the new claimAtt + std::vector rewardAccounts; + rewardAccounts.reserve(attestations.size()); + std::uint32_t weight = 0; + for (auto const& a : attestations) + { + auto const matchR = a.match(toMatch); + // The dest must match if claimHelper is being run as a result of an add + // attestation transaction. The dst does not need to match if the + // claimHelper is being run using an explicit claim transaction. + using enum AttestationMatch; + if (matchR == nonDstMismatch || + (checkDst == CheckDst::check && matchR != match)) + continue; + auto i = signersList.find(a.keyAccount); + if (i == signersList.end()) + { + assert(0); // should have already been checked + continue; + } + weight += i->second; + rewardAccounts.push_back(a.rewardAccount); + } + + if (weight >= quorum) + return rewardAccounts; + + return Unexpected(tecXCHAIN_CLAIM_NO_QUORUM); +} + +/** + Handle a new attestation event. + + Attempt to add the given attestation and reconcile with the current + signer's list. Attestations that are not part of the current signer's + list will be removed. + + @param claimAtt New attestation to add. It will be added if it is not + already part of the collection, or attests to a larger value. + + @param quorum Min weight required for a quorum + + @param signersList Map from signer's account id (derived from public keys) + to the weight of that key. + + @return optional reward accounts. If after handling the new attestation + there is a quorum for the amount specified on the new attestation, then + return the reward accounts for that amount, otherwise return a nullopt. + Note that if the signer's list changes and there have been `commit` + transactions of different amounts then there may be a different subset that + has reached quorum. However, to "trigger" that subset would require adding + (or re-adding) an attestation that supports that subset. + + The reason for using a nullopt instead of an empty vector when a quorum is + not reached is to allow for an interface where a quorum is reached but no + rewards are distributed. + + @note This function is not called `add` because it does more than just + add the new attestation (in fact, it may not add the attestation at + all). Instead, it handles the event of a new attestation. + */ +struct OnNewAttestationResult +{ + std::optional> rewardAccounts; + // `changed` is true if the attestation collection changed in any way + // (added/removed/changed) + bool changed{false}; +}; + +template +[[nodiscard]] OnNewAttestationResult +onNewAttestations( + XChainAttestationsBase& attestations, + ReadView const& view, + typename TAttestation::TSignedAttestation const* attBegin, + typename TAttestation::TSignedAttestation const* attEnd, + std::uint32_t quorum, + std::unordered_map const& signersList, + beast::Journal j) +{ + bool changed = false; + for (auto att = attBegin; att != attEnd; ++att) + { + if (checkAttestationPublicKey( + view, + signersList, + att->attestationSignerAccount, + att->publicKey, + j) != tesSUCCESS) + { + // The checkAttestationPublicKey is not strictly necessary here (it + // should be checked in a preclaim step), but it would be bad to let + // this slip through if that changes, and the check is relatively + // cheap, so we check again + continue; + } + + auto const& claimSigningAccount = att->attestationSignerAccount; + if (auto i = std::find_if( + attestations.begin(), + attestations.end(), + [&](auto const& a) { + return a.keyAccount == claimSigningAccount; + }); + i != attestations.end()) + { + // existing attestation + // replace old attestation with new attestation + *i = TAttestation{*att}; + changed = true; + } + else + { + attestations.emplace_back(*att); + changed = true; + } + } + + auto r = claimHelper( + attestations, + view, + typename TAttestation::MatchFields{*attBegin}, + CheckDst::check, + quorum, + signersList, + j); + + if (!r.has_value()) + return {std::nullopt, changed}; + + return {std::move(r.value()), changed}; +}; + +// Check if there is a quorurm of attestations for the given amount and +// chain. If so return the reward accounts, if not return the tec code (most +// likely tecXCHAIN_CLAIM_NO_QUORUM) +Expected, TER> +onClaim( + XChainClaimAttestations& attestations, + ReadView const& view, + STAmount const& sendingAmount, + bool wasLockingChainSend, + std::uint32_t quorum, + std::unordered_map const& signersList, + beast::Journal j) +{ + XChainClaimAttestation::MatchFields toMatch{ + sendingAmount, wasLockingChainSend, std::nullopt}; + return claimHelper( + attestations, view, toMatch, CheckDst::ignore, quorum, signersList, j); +} + +enum class CanCreateDstPolicy { no, yes }; + +enum class DepositAuthPolicy { normal, dstCanBypass }; + +// Allow the fee to dip into the reserve. To support this, information about the +// submitting account needs to be fed to the transfer helper. +struct TransferHelperSubmittingAccountInfo +{ + AccountID account; + STAmount preFeeBalance; + STAmount postFeeBalance; +}; + +/** Transfer funds from the src account to the dst account + + @param psb The payment sandbox. + @param src The source of funds. + @param dst The destination for funds. + @param dstTag Integer destination tag. Used to check if funds should be + transferred to an account with a `RequireDstTag` flag set. + @param claimOwner Owner of the claim ledger object. + @param amt Amount to transfer from the src account to the dst account. + @param canCreate Flag to determine if accounts may be created using this + transfer. + @param depositAuthPolicy Flag to determine if dst can bypass deposit auth if + it is also the claim owner. + @param submittingAccountInfo If the transaction is allowed to dip into the + reserve to pay fees, then this optional will be seated ("commit" + transactions support this, other transactions should not). + @param j Log + + @return tesSUCCESS if payment succeeds, otherwise the error code for the + failure reason. + */ + +TER +transferHelper( + PaymentSandbox& psb, + AccountID const& src, + AccountID const& dst, + std::optional const& dstTag, + std::optional const& claimOwner, + STAmount const& amt, + CanCreateDstPolicy canCreate, + DepositAuthPolicy depositAuthPolicy, + std::optional const& + submittingAccountInfo, + beast::Journal j) +{ + if (dst == src) + return tesSUCCESS; + + auto const dstK = keylet::account(dst); + if (auto sleDst = psb.read(dstK)) + { + // Check dst tag and deposit auth + + if ((sleDst->getFlags() & lsfRequireDestTag) && !dstTag) + return tecDST_TAG_NEEDED; + + // If the destination is the claim owner, and this is a claim + // transaction, that's the dst account sending funds to itself. It + // can bypass deposit auth. + bool const canBypassDepositAuth = dst == claimOwner && + depositAuthPolicy == DepositAuthPolicy::dstCanBypass; + + if (!canBypassDepositAuth && (sleDst->getFlags() & lsfDepositAuth) && + !psb.exists(keylet::depositPreauth(dst, src))) + { + return tecNO_PERMISSION; + } + } + else if (!amt.native() || canCreate == CanCreateDstPolicy::no) + { + return tecNO_DST; + } + + if (amt.native()) + { + auto const sleSrc = psb.peek(keylet::account(src)); + assert(sleSrc); + if (!sleSrc) + return tecINTERNAL; + + { + auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount); + auto const reserve = psb.fees().accountReserve(ownerCount); + + auto const availableBalance = [&]() -> STAmount { + STAmount const curBal = (*sleSrc)[sfBalance]; + // Checking that account == src and postFeeBalance == curBal is + // not strictly nessisary, but helps protect against future + // changes + if (!submittingAccountInfo || + submittingAccountInfo->account != src || + submittingAccountInfo->postFeeBalance != curBal) + return curBal; + return submittingAccountInfo->preFeeBalance; + }(); + + if (availableBalance < amt + reserve) + { + return tecUNFUNDED_PAYMENT; + } + } + + auto sleDst = psb.peek(dstK); + if (!sleDst) + { + if (canCreate == CanCreateDstPolicy::no) + { + // Already checked, but OK to check again + return tecNO_DST; + } + if (amt < psb.fees().accountReserve(0)) + { + JLOG(j.trace()) << "Insufficient payment to create account."; + return tecNO_DST_INSUF_XRP; + } + + // Create the account. + std::uint32_t const seqno{ + psb.rules().enabled(featureDeletableAccounts) ? psb.seq() : 1}; + + sleDst = std::make_shared(dstK); + sleDst->setAccountID(sfAccount, dst); + sleDst->setFieldU32(sfSequence, seqno); + + psb.insert(sleDst); + } + + (*sleSrc)[sfBalance] = (*sleSrc)[sfBalance] - amt; + (*sleDst)[sfBalance] = (*sleDst)[sfBalance] + amt; + psb.update(sleSrc); + psb.update(sleDst); + + return tesSUCCESS; + } + + auto const result = flow( + psb, + amt, + src, + dst, + STPathSet{}, + /*default path*/ true, + /*partial payment*/ false, + /*owner pays transfer fee*/ true, + /*offer crossing*/ false, + /*limit quality*/ std::nullopt, + /*sendmax*/ std::nullopt, + j); + + if (auto const r = result.result(); + isTesSuccess(r) || isTecClaim(r) || isTerRetry(r)) + return r; + return tecXCHAIN_PAYMENT_FAILED; +} + +/** Action to take when the transfer from the door account to the dst fails + + @note This is useful to prevent a failed "create account" transaction from + blocking subsequent "create account" transactions. +*/ +enum class OnTransferFail { + /** Remove the claim even if the transfer fails */ + removeClaim, + /** Keep the claim if the transfer fails */ + keepClaim +}; + +struct FinalizeClaimHelperResult +{ + /// TER for transfering the payment funds + std::optional mainFundsTer; + // TER for transfering the reward funds + std::optional rewardTer; + // TER for removing the sle (if is sle is to be removed) + std::optional rmSleTer; + + // Helper to check for overall success. If there wasn't overall success the + // individual ters can be used to decide what needs to be done. + bool + isTesSuccess() const + { + return mainFundsTer == tesSUCCESS && rewardTer == tesSUCCESS && + (!rmSleTer || *rmSleTer == tesSUCCESS); + } + + TER + ter() const + { + if ((!mainFundsTer || *mainFundsTer == tesSUCCESS) && + (!rewardTer || *rewardTer == tesSUCCESS) && + (!rmSleTer || *rmSleTer == tesSUCCESS)) + return tesSUCCESS; + + // if any phase return a tecINTERNAL or a tef, prefer returning those + // codes + if (mainFundsTer && + (isTefFailure(*mainFundsTer) || *mainFundsTer == tecINTERNAL)) + return *mainFundsTer; + if (rewardTer && + (isTefFailure(*rewardTer) || *rewardTer == tecINTERNAL)) + return *rewardTer; + if (rmSleTer && (isTefFailure(*rmSleTer) || *rmSleTer == tecINTERNAL)) + return *rmSleTer; + + // Only after the tecINTERNAL and tef are checked, return the first + // non-success error code. + if (mainFundsTer && mainFundsTer != tesSUCCESS) + return *mainFundsTer; + if (rewardTer && rewardTer != tesSUCCESS) + return *rewardTer; + if (rmSleTer && rmSleTer != tesSUCCESS) + return *rmSleTer; + return tesSUCCESS; + } +}; + +/** Transfer funds from the door account to the dst and distribute rewards + + @param psb The payment sandbox. + @param bridgeSpc Bridge + @param dst The destination for funds. + @param dstTag Integer destination tag. Used to check if funds should be + transferred to an account with a `RequireDstTag` flag set. + @param claimOwner Owner of the claim ledger object. + @param sendingAmount Amount that was committed on the source chain. + @param rewardPoolSrc Source of the funds for the reward pool (claim owner). + @param rewardPool Amount to split among the rewardAccounts. + @param rewardAccounts Account to receive the reward pool. + @param srcChain Chain where the commit event occurred. + @param sleClaimID sle for the claim id (may be NULL or XChainClaimID or + XChainCreateAccountClaimID). Don't read fields that aren't in common + with those two types and always check for NULL. Remove on success (if + not null). Remove on fail if the onTransferFail flag is removeClaim. + @param onTransferFail Flag to determine if the claim is removed on transfer + failure. This is used for create account transactions where claims + are removed so they don't block future txns. + @param j Log + + @return FinalizeClaimHelperResult. See the comments in this struct for what + the fields mean. The individual ters need to be returned instead of + an overall ter because the caller needs this information if the + attestation list changed or not. + */ + +FinalizeClaimHelperResult +finalizeClaimHelper( + PaymentSandbox& outerSb, + STXChainBridge const& bridgeSpec, + AccountID const& dst, + std::optional const& dstTag, + AccountID const& claimOwner, + STAmount const& sendingAmount, + AccountID const& rewardPoolSrc, + STAmount const& rewardPool, + std::vector const& rewardAccounts, + STXChainBridge::ChainType const srcChain, + Keylet const& claimIDKeylet, + OnTransferFail onTransferFail, + DepositAuthPolicy depositAuthPolicy, + beast::Journal j) +{ + FinalizeClaimHelperResult result; + + STXChainBridge::ChainType const dstChain = + STXChainBridge::otherChain(srcChain); + STAmount const thisChainAmount = [&] { + STAmount r = sendingAmount; + r.setIssue(bridgeSpec.issue(dstChain)); + return r; + }(); + auto const& thisDoor = bridgeSpec.door(dstChain); + + { + PaymentSandbox innerSb{&outerSb}; + // If distributing the reward pool fails, the mainFunds transfer should + // be rolled back + // + // If the claimid is removed, the rewards should be distributed + // even if the mainFunds fails. + // + // If OnTransferFail::removeClaim, the claim should be removed even if + // the rewards cannot be distributed. + + // transfer funds to the dst + result.mainFundsTer = transferHelper( + innerSb, + thisDoor, + dst, + dstTag, + claimOwner, + thisChainAmount, + CanCreateDstPolicy::yes, + depositAuthPolicy, + std::nullopt, + j); + + if (!isTesSuccess(*result.mainFundsTer) && + onTransferFail == OnTransferFail::keepClaim) + { + return result; + } + + // handle the reward pool + result.rewardTer = [&]() -> TER { + if (rewardAccounts.empty()) + return tesSUCCESS; + + // distribute the reward pool + // if the transfer failed, distribute the pool for "OnTransferFail" + // cases (the attesters did their job) + STAmount const share = [&] { + STAmount const den{rewardAccounts.size()}; + return divide(rewardPool, den, rewardPool.issue()); + }(); + STAmount distributed = rewardPool.zeroed(); + for (auto const& rewardAccount : rewardAccounts) + { + auto const thTer = transferHelper( + innerSb, + rewardPoolSrc, + rewardAccount, + /*dstTag*/ std::nullopt, + // claim owner is not relevant to distributing rewards + /*claimOwner*/ std::nullopt, + share, + CanCreateDstPolicy::no, + DepositAuthPolicy::normal, + std::nullopt, + j); + + if (thTer == tecUNFUNDED_PAYMENT || thTer == tecINTERNAL) + return thTer; + + if (isTesSuccess(thTer)) + distributed += share; + + // let txn succeed if error distributing rewards (other than + // inability to pay) + } + + if (distributed > rewardPool) + return tecINTERNAL; + + return tesSUCCESS; + }(); + + if (!isTesSuccess(*result.rewardTer) && + (onTransferFail == OnTransferFail::keepClaim || + *result.rewardTer == tecINTERNAL)) + { + return result; + } + + if (!isTesSuccess(*result.mainFundsTer) || + isTesSuccess(*result.rewardTer)) + { + // Note: if the mainFunds transfer succeeds and the result transfer + // fails, we don't apply the inner sandbox (i.e. the mainTransfer is + // rolled back) + innerSb.apply(outerSb); + } + } + + if (auto const sleClaimID = outerSb.peek(claimIDKeylet)) + { + auto const cidOwner = (*sleClaimID)[sfAccount]; + { + // Remove the claim id + auto const sleOwner = outerSb.peek(keylet::account(cidOwner)); + auto const page = (*sleClaimID)[sfOwnerNode]; + if (!outerSb.dirRemove( + keylet::ownerDir(cidOwner), page, sleClaimID->key(), true)) + { + JLOG(j.fatal()) + << "Unable to delete xchain seq number from owner."; + result.rmSleTer = tefBAD_LEDGER; + return result; + } + + // Remove the claim id from the ledger + outerSb.erase(sleClaimID); + + adjustOwnerCount(outerSb, sleOwner, -1, j); + } + } + + return result; +} + +/** Get signers list corresponding to the account that owns the bridge + + @param view View to read the signer's list from. + @param sleBridge Sle of the bridge. + @param j Log + + @return map of the signer's list (AccountIDs and weights), the quorum, and + error code +*/ +std::tuple, std::uint32_t, TER> +getSignersListAndQuorum( + ReadView const& view, + SLE const& sleBridge, + beast::Journal j) +{ + std::unordered_map r; + std::uint32_t q = std::numeric_limits::max(); + + AccountID const thisDoor = sleBridge[sfAccount]; + auto const sleDoor = [&] { return view.read(keylet::account(thisDoor)); }(); + + if (!sleDoor) + { + return {r, q, tecINTERNAL}; + } + + auto const sleS = view.read(keylet::signers(sleBridge[sfAccount])); + if (!sleS) + { + return {r, q, tecXCHAIN_NO_SIGNERS_LIST}; + } + q = (*sleS)[sfSignerQuorum]; + + auto const accountSigners = SignerEntries::deserialize(*sleS, j, "ledger"); + + if (!accountSigners) + { + return {r, q, tecINTERNAL}; + } + + for (auto const& as : *accountSigners) + { + r[as.account] = as.weight; + } + + return {std::move(r), q, tesSUCCESS}; +}; + +template +std::shared_ptr +readOrpeekBridge(F&& getter, STXChainBridge const& bridgeSpec) +{ + auto tryGet = [&](STXChainBridge::ChainType ct) -> std::shared_ptr { + if (auto r = getter(bridgeSpec, ct)) + { + if ((*r)[sfXChainBridge] == bridgeSpec) + return r; + } + return nullptr; + }; + if (auto r = tryGet(STXChainBridge::ChainType::locking)) + return r; + return tryGet(STXChainBridge::ChainType::issuing); +} + +std::shared_ptr +peekBridge(ApplyView& v, STXChainBridge const& bridgeSpec) +{ + return readOrpeekBridge( + [&v](STXChainBridge const& b, STXChainBridge::ChainType ct) + -> std::shared_ptr { return v.peek(keylet::bridge(b, ct)); }, + bridgeSpec); +} + +std::shared_ptr +readBridge(ReadView const& v, STXChainBridge const& bridgeSpec) +{ + return readOrpeekBridge( + [&v](STXChainBridge const& b, STXChainBridge::ChainType ct) + -> std::shared_ptr { + return v.read(keylet::bridge(b, ct)); + }, + bridgeSpec); +} + +// Precondition: all the claims in the range are consistent. They must sign for +// the same event (amount, sending account, claim id, etc). +template +TER +applyClaimAttestations( + ApplyView& view, + RawView& rawView, + TIter attBegin, + TIter attEnd, + STXChainBridge const& bridgeSpec, + STXChainBridge::ChainType const srcChain, + std::unordered_map const& signersList, + std::uint32_t quorum, + beast::Journal j) +{ + if (attBegin == attEnd) + return tesSUCCESS; + + PaymentSandbox psb(&view); + + auto const claimIDKeylet = + keylet::xChainClaimID(bridgeSpec, attBegin->claimID); + + struct ScopeResult + { + OnNewAttestationResult newAttResult; + STAmount rewardAmount; + AccountID cidOwner; + }; + + auto const scopeResult = [&]() -> Expected { + // This lambda is ugly - admittedly. The purpose of this lambda is to + // limit the scope of sles so they don't overlap with + // `finalizeClaimHelper`. Since `finalizeClaimHelper` can create child + // views, it's important that the sle's lifetime doesn't overlap. + auto const sleClaimID = psb.peek(claimIDKeylet); + if (!sleClaimID) + return Unexpected(tecXCHAIN_NO_CLAIM_ID); + + // Add claims that are part of the signer's list to the "claims" vector + std::vector atts; + atts.reserve(std::distance(attBegin, attEnd)); + for (auto att = attBegin; att != attEnd; ++att) + { + if (!signersList.contains(att->attestationSignerAccount)) + continue; + atts.push_back(*att); + } + + if (atts.empty()) + { + return Unexpected(tecXCHAIN_PROOF_UNKNOWN_KEY); + } + + AccountID const otherChainSource = (*sleClaimID)[sfOtherChainSource]; + if (attBegin->sendingAccount != otherChainSource) + { + return Unexpected(tecXCHAIN_SENDING_ACCOUNT_MISMATCH); + } + + { + STXChainBridge::ChainType const dstChain = + STXChainBridge::otherChain(srcChain); + + STXChainBridge::ChainType const attDstChain = + STXChainBridge::dstChain(attBegin->wasLockingChainSend); + + if (attDstChain != dstChain) + { + return Unexpected(tecXCHAIN_WRONG_CHAIN); + } + } + + XChainClaimAttestations curAtts{ + sleClaimID->getFieldArray(sfXChainClaimAttestations)}; + + auto const newAttResult = onNewAttestations( + curAtts, + view, + &atts[0], + &atts[0] + atts.size(), + quorum, + signersList, + j); + + // update the claim id + sleClaimID->setFieldArray( + sfXChainClaimAttestations, curAtts.toSTArray()); + psb.update(sleClaimID); + + return ScopeResult{ + newAttResult, + (*sleClaimID)[sfSignatureReward], + (*sleClaimID)[sfAccount]}; + }(); + + if (!scopeResult.has_value()) + return scopeResult.error(); + + auto const& [newAttResult, rewardAmount, cidOwner] = scopeResult.value(); + auto const& [rewardAccounts, attListChanged] = newAttResult; + if (rewardAccounts && attBegin->dst) + { + auto const r = finalizeClaimHelper( + psb, + bridgeSpec, + *attBegin->dst, + /*dstTag*/ std::nullopt, + cidOwner, + attBegin->sendingAmount, + cidOwner, + rewardAmount, + *rewardAccounts, + srcChain, + claimIDKeylet, + OnTransferFail::keepClaim, + DepositAuthPolicy::normal, + j); + + auto const rTer = r.ter(); + + if (!isTesSuccess(rTer) && + (!attListChanged || rTer == tecINTERNAL || rTer == tefBAD_LEDGER)) + return rTer; + } + + psb.apply(rawView); + + return tesSUCCESS; +} + +template +TER +applyCreateAccountAttestations( + ApplyView& view, + RawView& rawView, + TIter attBegin, + TIter attEnd, + AccountID const& doorAccount, + Keylet const& doorK, + STXChainBridge const& bridgeSpec, + Keylet const& bridgeK, + STXChainBridge::ChainType const srcChain, + std::unordered_map const& signersList, + std::uint32_t quorum, + beast::Journal j) +{ + if (attBegin == attEnd) + return tesSUCCESS; + + PaymentSandbox psb(&view); + + auto const claimCountResult = [&]() -> Expected { + auto const sleBridge = psb.peek(bridgeK); + if (!sleBridge) + return Unexpected(tecINTERNAL); + + return (*sleBridge)[sfXChainAccountClaimCount]; + }(); + + if (!claimCountResult.has_value()) + return claimCountResult.error(); + + std::uint64_t const claimCount = claimCountResult.value(); + + if (attBegin->createCount <= claimCount) + { + return tecXCHAIN_ACCOUNT_CREATE_PAST; + } + if (attBegin->createCount >= claimCount + xbridgeMaxAccountCreateClaims) + { + // Limit the number of claims on the account + return tecXCHAIN_ACCOUNT_CREATE_TOO_MANY; + } + + { + STXChainBridge::ChainType const dstChain = + STXChainBridge::otherChain(srcChain); + + STXChainBridge::ChainType const attDstChain = + STXChainBridge::dstChain(attBegin->wasLockingChainSend); + + if (attDstChain != dstChain) + { + return tecXCHAIN_WRONG_CHAIN; + } + } + + auto const claimIDKeylet = + keylet::xChainCreateAccountClaimID(bridgeSpec, attBegin->createCount); + + struct ScopeResult + { + OnNewAttestationResult newAttResult; + bool createCID; + XChainCreateAccountAttestations curAtts; + }; + + auto const scopeResult = [&]() -> Expected { + // This lambda is ugly - admittedly. The purpose of this lambda is to + // limit the scope of sles so they don't overlap with + // `finalizeClaimHelper`. Since `finalizeClaimHelper` can create child + // views, it's important that the sle's lifetime doesn't overlap. + + // sleClaimID may be null. If it's null it isn't created until the end + // of this function (if needed) + auto const sleClaimID = psb.peek(claimIDKeylet); + bool createCID = false; + if (!sleClaimID) + { + createCID = true; + + auto const sleDoor = psb.peek(doorK); + if (!sleDoor) + return Unexpected(tecINTERNAL); + + // Check reserve + auto const balance = (*sleDoor)[sfBalance]; + auto const reserve = + psb.fees().accountReserve((*sleDoor)[sfOwnerCount] + 1); + + if (balance < reserve) + return Unexpected(tecINSUFFICIENT_RESERVE); + } + + std::vector atts; + atts.reserve(std::distance(attBegin, attEnd)); + for (auto att = attBegin; att != attEnd; ++att) + { + if (!signersList.contains(att->attestationSignerAccount)) + continue; + atts.push_back(*att); + } + if (atts.empty()) + { + return Unexpected(tecXCHAIN_PROOF_UNKNOWN_KEY); + } + + XChainCreateAccountAttestations curAtts = [&] { + if (sleClaimID) + return XChainCreateAccountAttestations{ + sleClaimID->getFieldArray( + sfXChainCreateAccountAttestations)}; + return XChainCreateAccountAttestations{}; + }(); + + auto const newAttResult = onNewAttestations( + curAtts, + view, + &atts[0], + &atts[0] + atts.size(), + quorum, + signersList, + j); + + if (!createCID) + { + // Modify the object before it's potentially deleted, so the meta + // data will include the new attestations + if (!sleClaimID) + return Unexpected(tecINTERNAL); + sleClaimID->setFieldArray( + sfXChainCreateAccountAttestations, curAtts.toSTArray()); + psb.update(sleClaimID); + } + return ScopeResult{newAttResult, createCID, curAtts}; + }(); + + if (!scopeResult.has_value()) + return scopeResult.error(); + + auto const& [attResult, createCID, curAtts] = scopeResult.value(); + auto const& [rewardAccounts, attListChanged] = attResult; + + // Account create transactions must happen in order + if (rewardAccounts && claimCount + 1 == attBegin->createCount) + { + auto const r = finalizeClaimHelper( + psb, + bridgeSpec, + attBegin->toCreate, + /*dstTag*/ std::nullopt, + doorAccount, + attBegin->sendingAmount, + /*rewardPoolSrc*/ doorAccount, + attBegin->rewardAmount, + *rewardAccounts, + srcChain, + claimIDKeylet, + OnTransferFail::removeClaim, + DepositAuthPolicy::normal, + j); + + auto const rTer = r.ter(); + + if (!isTesSuccess(rTer)) + { + if (rTer == tecINTERNAL || rTer == tecUNFUNDED_PAYMENT || + isTefFailure(rTer)) + return rTer; + } + // Move past this claim id even if it fails, so it doesn't block + // subsequent claim ids + auto const sleBridge = psb.peek(bridgeK); + if (!sleBridge) + return tecINTERNAL; + (*sleBridge)[sfXChainAccountClaimCount] = attBegin->createCount; + psb.update(sleBridge); + } + else if (createCID) + { + auto const createdSleClaimID = std::make_shared(claimIDKeylet); + (*createdSleClaimID)[sfAccount] = doorAccount; + (*createdSleClaimID)[sfXChainBridge] = bridgeSpec; + (*createdSleClaimID)[sfXChainAccountCreateCount] = + attBegin->createCount; + createdSleClaimID->setFieldArray( + sfXChainCreateAccountAttestations, curAtts.toSTArray()); + + // Add to owner directory of the door account + auto const page = psb.dirInsert( + keylet::ownerDir(doorAccount), + claimIDKeylet, + describeOwnerDir(doorAccount)); + if (!page) + return tecDIR_FULL; + (*createdSleClaimID)[sfOwnerNode] = *page; + + auto const sleDoor = psb.peek(doorK); + if (!sleDoor) + return tecINTERNAL; + + // Reserve was already checked + adjustOwnerCount(psb, sleDoor, 1, j); + psb.insert(createdSleClaimID); + psb.update(sleDoor); + } + + psb.apply(rawView); + + return tesSUCCESS; +} + +template +std::optional +toClaim(STTx const& tx) +{ + static_assert( + std::is_same_v || + std::is_same_v); + + try + { + STObject o{tx}; + o.setAccountID(sfAccount, o[sfOtherChainSource]); + return TAttestation(o); + } + catch (...) + { + } + return std::nullopt; +} + +template +NotTEC +attestationPreflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureXChainBridge)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + auto const att = toClaim(ctx.tx); + if (!att) + return temMALFORMED; + + STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; + if (!att->verify(bridgeSpec)) + return temXCHAIN_BAD_PROOF; + if (!att->validAmounts()) + return temXCHAIN_BAD_PROOF; + + if (att->sendingAmount.signum() <= 0) + return temXCHAIN_BAD_PROOF; + auto const expectedIssue = + bridgeSpec.issue(STXChainBridge::srcChain(att->wasLockingChainSend)); + if (att->sendingAmount.issue() != expectedIssue) + return temXCHAIN_BAD_PROOF; + + return preflight2(ctx); +} + +template +TER +attestationPreclaim(PreclaimContext const& ctx) +{ + auto const att = toClaim(ctx.tx); + if (!att) + return tecINTERNAL; // checked in preflight + + STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; + auto const sleBridge = readBridge(ctx.view, bridgeSpec); + if (!sleBridge) + { + return tecNO_ENTRY; + } + + AccountID const attestationSignerAccount{ + ctx.tx[sfAttestationSignerAccount]}; + PublicKey const pk{ctx.tx[sfPublicKey]}; + + // signersList is a map from account id to weights + auto const [signersList, quorum, slTer] = + getSignersListAndQuorum(ctx.view, *sleBridge, ctx.j); + + if (!isTesSuccess(slTer)) + return slTer; + + return checkAttestationPublicKey( + ctx.view, signersList, attestationSignerAccount, pk, ctx.j); +} + +template +TER +attestationDoApply(ApplyContext& ctx) +{ + auto const att = toClaim(ctx.tx); + if (!att) + // Should already be checked in preflight + return tecINTERNAL; + + STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; + + struct ScopeResult + { + STXChainBridge::ChainType srcChain; + std::unordered_map signersList; + std::uint32_t quorum; + AccountID thisDoor; + Keylet bridgeK; + }; + + auto const scopeResult = [&]() -> Expected { + // This lambda is ugly - admittedly. The purpose of this lambda is to + // limit the scope of sles so they don't overlap with + // `finalizeClaimHelper`. Since `finalizeClaimHelper` can create child + // views, it's important that the sle's lifetime doesn't overlap. + auto sleBridge = readBridge(ctx.view(), bridgeSpec); + if (!sleBridge) + { + return Unexpected(tecNO_ENTRY); + } + Keylet const bridgeK{ltBRIDGE, sleBridge->key()}; + AccountID const thisDoor = (*sleBridge)[sfAccount]; + + STXChainBridge::ChainType dstChain = STXChainBridge::ChainType::locking; + { + if (thisDoor == bridgeSpec.lockingChainDoor()) + dstChain = STXChainBridge::ChainType::locking; + else if (thisDoor == bridgeSpec.issuingChainDoor()) + dstChain = STXChainBridge::ChainType::issuing; + else + return Unexpected(tecINTERNAL); + } + STXChainBridge::ChainType const srcChain = + STXChainBridge::otherChain(dstChain); + + // signersList is a map from account id to weights + auto [signersList, quorum, slTer] = + getSignersListAndQuorum(ctx.view(), *sleBridge, ctx.journal); + + if (!isTesSuccess(slTer)) + return Unexpected(slTer); + + return ScopeResult{ + srcChain, std::move(signersList), quorum, thisDoor, bridgeK}; + }(); + + if (!scopeResult.has_value()) + return scopeResult.error(); + + auto const& [srcChain, signersList, quorum, thisDoor, bridgeK] = + scopeResult.value(); + + static_assert( + std::is_same_v || + std::is_same_v); + + if constexpr (std::is_same_v) + { + return applyClaimAttestations( + ctx.view(), + ctx.rawView(), + &*att, + &*att + 1, + bridgeSpec, + srcChain, + signersList, + quorum, + ctx.journal); + } + else if constexpr (std::is_same_v< + TAttestation, + Attestations::AttestationCreateAccount>) + { + return applyCreateAccountAttestations( + ctx.view(), + ctx.rawView(), + &*att, + &*att + 1, + thisDoor, + keylet::account(thisDoor), + bridgeSpec, + bridgeK, + srcChain, + signersList, + quorum, + ctx.journal); + } +} + +} // namespace +//------------------------------------------------------------------------------ + +NotTEC +XChainCreateBridge::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureXChainBridge)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + auto const account = ctx.tx[sfAccount]; + auto const reward = ctx.tx[sfSignatureReward]; + auto const minAccountCreate = ctx.tx[~sfMinAccountCreateAmount]; + auto const bridgeSpec = ctx.tx[sfXChainBridge]; + // Doors must be distinct to help prevent transaction replay attacks + if (bridgeSpec.lockingChainDoor() == bridgeSpec.issuingChainDoor()) + { + return temXCHAIN_EQUAL_DOOR_ACCOUNTS; + } + + if (bridgeSpec.lockingChainDoor() != account && + bridgeSpec.issuingChainDoor() != account) + { + return temXCHAIN_BRIDGE_NONDOOR_OWNER; + } + + if (isXRP(bridgeSpec.lockingChainIssue()) != + isXRP(bridgeSpec.issuingChainIssue())) + { + // Because ious and xrp have different numeric ranges, both the src and + // dst issues must be both XRP or both IOU. + return temXCHAIN_BRIDGE_BAD_ISSUES; + } + + if (!isXRP(reward) || reward.signum() < 0) + { + return temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT; + } + + if (minAccountCreate && + ((!isXRP(*minAccountCreate) || minAccountCreate->signum() <= 0) || + !isXRP(bridgeSpec.lockingChainIssue()) || + !isXRP(bridgeSpec.issuingChainIssue()))) + { + return temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT; + } + + if (isXRP(bridgeSpec.issuingChainIssue())) + { + // Issuing account must be the root account for XRP (which presumably + // owns all the XRP). This is done so the issuing account can't "run + // out" of wrapped tokens. + static auto const rootAccount = calcAccountID( + generateKeyPair( + KeyType::secp256k1, generateSeed("masterpassphrase")) + .first); + if (bridgeSpec.issuingChainDoor() != rootAccount) + { + return temXCHAIN_BRIDGE_BAD_ISSUES; + } + } + else + { + // Issuing account must be the issuer for non-XRP. This is done so the + // issuing account can't "run out" of wrapped tokens. + if (bridgeSpec.issuingChainDoor() != + bridgeSpec.issuingChainIssue().account) + { + return temXCHAIN_BRIDGE_BAD_ISSUES; + } + } + + if (bridgeSpec.lockingChainDoor() == bridgeSpec.lockingChainIssue().account) + { + // If the locking chain door is locking their own asset, in some sense + // nothing is being locked. Disallow this. + return temXCHAIN_BRIDGE_BAD_ISSUES; + } + + return preflight2(ctx); +} + +TER +XChainCreateBridge::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + auto const bridgeSpec = ctx.tx[sfXChainBridge]; + + STXChainBridge::ChainType const chainType = + STXChainBridge::srcChain(account == bridgeSpec.lockingChainDoor()); + + if (ctx.view.read(keylet::bridge(bridgeSpec, chainType))) + { + return tecDUPLICATE; + } + + if (!isXRP(bridgeSpec.issue(chainType))) + { + auto const sleIssuer = + ctx.view.read(keylet::account(bridgeSpec.issue(chainType).account)); + + if (!sleIssuer) + return tecNO_ISSUER; + + // Allowing clawing back funds would break the bridge's invariant that + // wrapped funds are always backed by locked funds + if (sleIssuer->getFlags() & lsfAllowTrustLineClawback) + return tecNO_PERMISSION; + } + + { + // Check reserve + auto const sleAcc = ctx.view.read(keylet::account(account)); + if (!sleAcc) + return terNO_ACCOUNT; + + auto const balance = (*sleAcc)[sfBalance]; + auto const reserve = + ctx.view.fees().accountReserve((*sleAcc)[sfOwnerCount] + 1); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + return tesSUCCESS; +} + +TER +XChainCreateBridge::doApply() +{ + auto const account = ctx_.tx[sfAccount]; + auto const bridgeSpec = ctx_.tx[sfXChainBridge]; + auto const reward = ctx_.tx[sfSignatureReward]; + auto const minAccountCreate = ctx_.tx[~sfMinAccountCreateAmount]; + + auto const sleAcct = ctx_.view().peek(keylet::account(account)); + if (!sleAcct) + return tecINTERNAL; + + STXChainBridge::ChainType const chainType = + STXChainBridge::srcChain(account == bridgeSpec.lockingChainDoor()); + + Keylet const bridgeKeylet = keylet::bridge(bridgeSpec, chainType); + auto const sleBridge = std::make_shared(bridgeKeylet); + + (*sleBridge)[sfAccount] = account; + (*sleBridge)[sfSignatureReward] = reward; + if (minAccountCreate) + (*sleBridge)[sfMinAccountCreateAmount] = *minAccountCreate; + (*sleBridge)[sfXChainBridge] = bridgeSpec; + (*sleBridge)[sfXChainClaimID] = 0; + (*sleBridge)[sfXChainAccountCreateCount] = 0; + (*sleBridge)[sfXChainAccountClaimCount] = 0; + + // Add to owner directory + { + auto const page = ctx_.view().dirInsert( + keylet::ownerDir(account), bridgeKeylet, describeOwnerDir(account)); + if (!page) + return tecDIR_FULL; + (*sleBridge)[sfOwnerNode] = *page; + } + + adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + + ctx_.view().insert(sleBridge); + ctx_.view().update(sleAcct); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +NotTEC +BridgeModify::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureXChainBridge)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfBridgeModifyMask) + return temINVALID_FLAG; + + auto const account = ctx.tx[sfAccount]; + auto const reward = ctx.tx[~sfSignatureReward]; + auto const minAccountCreate = ctx.tx[~sfMinAccountCreateAmount]; + auto const bridgeSpec = ctx.tx[sfXChainBridge]; + bool const clearAccountCreate = + ctx.tx.getFlags() & tfClearAccountCreateAmount; + + if (!reward && !minAccountCreate && !clearAccountCreate) + { + // Must change something + return temMALFORMED; + } + + if (minAccountCreate && clearAccountCreate) + { + // Can't both clear and set account create in the same txn + return temMALFORMED; + } + + if (bridgeSpec.lockingChainDoor() != account && + bridgeSpec.issuingChainDoor() != account) + { + return temXCHAIN_BRIDGE_NONDOOR_OWNER; + } + + if (reward && (!isXRP(*reward) || reward->signum() < 0)) + { + return temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT; + } + + if (minAccountCreate && + ((!isXRP(*minAccountCreate) || minAccountCreate->signum() <= 0) || + !isXRP(bridgeSpec.lockingChainIssue()) || + !isXRP(bridgeSpec.issuingChainIssue()))) + { + return temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT; + } + + return preflight2(ctx); +} + +TER +BridgeModify::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + auto const bridgeSpec = ctx.tx[sfXChainBridge]; + + STXChainBridge::ChainType const chainType = + STXChainBridge::srcChain(account == bridgeSpec.lockingChainDoor()); + + if (!ctx.view.read(keylet::bridge(bridgeSpec, chainType))) + { + return tecNO_ENTRY; + } + + return tesSUCCESS; +} + +TER +BridgeModify::doApply() +{ + auto const account = ctx_.tx[sfAccount]; + auto const bridgeSpec = ctx_.tx[sfXChainBridge]; + auto const reward = ctx_.tx[~sfSignatureReward]; + auto const minAccountCreate = ctx_.tx[~sfMinAccountCreateAmount]; + bool const clearAccountCreate = + ctx_.tx.getFlags() & tfClearAccountCreateAmount; + + auto const sleAcct = ctx_.view().peek(keylet::account(account)); + if (!sleAcct) + return tecINTERNAL; + + STXChainBridge::ChainType const chainType = + STXChainBridge::srcChain(account == bridgeSpec.lockingChainDoor()); + + auto const sleBridge = + ctx_.view().peek(keylet::bridge(bridgeSpec, chainType)); + if (!sleBridge) + return tecINTERNAL; + + if (reward) + (*sleBridge)[sfSignatureReward] = *reward; + if (minAccountCreate) + { + (*sleBridge)[sfMinAccountCreateAmount] = *minAccountCreate; + } + if (clearAccountCreate && + sleBridge->isFieldPresent(sfMinAccountCreateAmount)) + { + sleBridge->makeFieldAbsent(sfMinAccountCreateAmount); + } + ctx_.view().update(sleBridge); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +NotTEC +XChainClaim::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureXChainBridge)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; + auto const amount = ctx.tx[sfAmount]; + + if (amount.signum() <= 0 || + (amount.issue() != bridgeSpec.lockingChainIssue() && + amount.issue() != bridgeSpec.issuingChainIssue())) + { + return temBAD_AMOUNT; + } + + return preflight2(ctx); +} + +TER +XChainClaim::preclaim(PreclaimContext const& ctx) +{ + AccountID const account = ctx.tx[sfAccount]; + STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; + STAmount const& thisChainAmount = ctx.tx[sfAmount]; + auto const claimID = ctx.tx[sfXChainClaimID]; + + auto const sleBridge = readBridge(ctx.view, bridgeSpec); + if (!sleBridge) + { + return tecNO_ENTRY; + } + + if (!ctx.view.read(keylet::account(ctx.tx[sfDestination]))) + { + return tecNO_DST; + } + + auto const thisDoor = (*sleBridge)[sfAccount]; + bool isLockingChain = false; + { + if (thisDoor == bridgeSpec.lockingChainDoor()) + isLockingChain = true; + else if (thisDoor == bridgeSpec.issuingChainDoor()) + isLockingChain = false; + else + return tecINTERNAL; + } + + { + // Check that the amount specified matches the expected issue + + if (isLockingChain) + { + if (bridgeSpec.lockingChainIssue() != thisChainAmount.issue()) + return tecXCHAIN_BAD_TRANSFER_ISSUE; + } + else + { + if (bridgeSpec.issuingChainIssue() != thisChainAmount.issue()) + return tecXCHAIN_BAD_TRANSFER_ISSUE; + } + } + + if (isXRP(bridgeSpec.lockingChainIssue()) != + isXRP(bridgeSpec.issuingChainIssue())) + { + // Should have been caught when creating the bridge + // Detect here so `otherChainAmount` doesn't switch from IOU -> XRP + // and the numeric issues that need to be addressed with that. + return tecINTERNAL; + } + + auto const otherChainAmount = [&]() -> STAmount { + STAmount r(thisChainAmount); + if (isLockingChain) + r.setIssue(bridgeSpec.issuingChainIssue()); + else + r.setIssue(bridgeSpec.lockingChainIssue()); + return r; + }(); + + auto const sleClaimID = + ctx.view.read(keylet::xChainClaimID(bridgeSpec, claimID)); + { + // Check that the sequence number is owned by the sender of this + // transaction + if (!sleClaimID) + { + return tecXCHAIN_NO_CLAIM_ID; + } + + if ((*sleClaimID)[sfAccount] != account) + { + // Sequence number isn't owned by the sender of this transaction + return tecXCHAIN_BAD_CLAIM_ID; + } + } + + // quorum is checked in `doApply` + return tesSUCCESS; +} + +TER +XChainClaim::doApply() +{ + PaymentSandbox psb(&ctx_.view()); + + AccountID const account = ctx_.tx[sfAccount]; + auto const dst = ctx_.tx[sfDestination]; + STXChainBridge const bridgeSpec = ctx_.tx[sfXChainBridge]; + STAmount const& thisChainAmount = ctx_.tx[sfAmount]; + auto const claimID = ctx_.tx[sfXChainClaimID]; + auto const claimIDKeylet = keylet::xChainClaimID(bridgeSpec, claimID); + + struct ScopeResult + { + std::vector rewardAccounts; + AccountID rewardPoolSrc; + STAmount sendingAmount; + STXChainBridge::ChainType srcChain; + STAmount signatureReward; + }; + + auto const scopeResult = [&]() -> Expected { + // This lambda is ugly - admittedly. The purpose of this lambda is to + // limit the scope of sles so they don't overlap with + // `finalizeClaimHelper`. Since `finalizeClaimHelper` can create child + // views, it's important that the sle's lifetime doesn't overlap. + + auto const sleAcct = psb.peek(keylet::account(account)); + auto const sleBridge = peekBridge(psb, bridgeSpec); + auto const sleClaimID = psb.peek(claimIDKeylet); + + if (!(sleBridge && sleClaimID && sleAcct)) + return Unexpected(tecINTERNAL); + + AccountID const thisDoor = (*sleBridge)[sfAccount]; + + STXChainBridge::ChainType dstChain = STXChainBridge::ChainType::locking; + { + if (thisDoor == bridgeSpec.lockingChainDoor()) + dstChain = STXChainBridge::ChainType::locking; + else if (thisDoor == bridgeSpec.issuingChainDoor()) + dstChain = STXChainBridge::ChainType::issuing; + else + return Unexpected(tecINTERNAL); + } + STXChainBridge::ChainType const srcChain = + STXChainBridge::otherChain(dstChain); + + auto const sendingAmount = [&]() -> STAmount { + STAmount r(thisChainAmount); + r.setIssue(bridgeSpec.issue(srcChain)); + return r; + }(); + + auto const [signersList, quorum, slTer] = + getSignersListAndQuorum(ctx_.view(), *sleBridge, ctx_.journal); + + if (!isTesSuccess(slTer)) + return Unexpected(slTer); + + XChainClaimAttestations curAtts{ + sleClaimID->getFieldArray(sfXChainClaimAttestations)}; + + auto const claimR = onClaim( + curAtts, + psb, + sendingAmount, + /*wasLockingChainSend*/ srcChain == + STXChainBridge::ChainType::locking, + quorum, + signersList, + ctx_.journal); + if (!claimR.has_value()) + return Unexpected(claimR.error()); + + return ScopeResult{ + claimR.value(), + (*sleClaimID)[sfAccount], + sendingAmount, + srcChain, + (*sleClaimID)[sfSignatureReward], + }; + }(); + + if (!scopeResult.has_value()) + return scopeResult.error(); + + auto const& [rewardAccounts, rewardPoolSrc, sendingAmount, srcChain, signatureReward] = + scopeResult.value(); + std::optional const dstTag = ctx_.tx[~sfDestinationTag]; + + auto const r = finalizeClaimHelper( + psb, + bridgeSpec, + dst, + dstTag, + /*claimOwner*/ account, + sendingAmount, + rewardPoolSrc, + signatureReward, + rewardAccounts, + srcChain, + claimIDKeylet, + OnTransferFail::keepClaim, + DepositAuthPolicy::dstCanBypass, + ctx_.journal); + if (!r.isTesSuccess()) + return r.ter(); + + psb.apply(ctx_.rawView()); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +TxConsequences +XChainCommit::makeTxConsequences(PreflightContext const& ctx) +{ + auto const maxSpend = [&] { + auto const amount = ctx.tx[sfAmount]; + if (amount.native() && amount.signum() > 0) + return amount.xrp(); + return XRPAmount{beast::zero}; + }(); + + return TxConsequences{ctx.tx, maxSpend}; +} + +NotTEC +XChainCommit::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureXChainBridge)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + auto const amount = ctx.tx[sfAmount]; + auto const bridgeSpec = ctx.tx[sfXChainBridge]; + + if (amount.signum() <= 0 || !isLegalNet(amount)) + return temBAD_AMOUNT; + + if (amount.issue() != bridgeSpec.lockingChainIssue() && + amount.issue() != bridgeSpec.issuingChainIssue()) + return temBAD_ISSUER; + + return preflight2(ctx); +} + +TER +XChainCommit::preclaim(PreclaimContext const& ctx) +{ + auto const bridgeSpec = ctx.tx[sfXChainBridge]; + auto const amount = ctx.tx[sfAmount]; + + auto const sleBridge = readBridge(ctx.view, bridgeSpec); + if (!sleBridge) + { + return tecNO_ENTRY; + } + + AccountID const thisDoor = (*sleBridge)[sfAccount]; + AccountID const account = ctx.tx[sfAccount]; + + if (thisDoor == account) + { + // Door account can't lock funds onto itself + return tecXCHAIN_SELF_COMMIT; + } + + bool isLockingChain = false; + { + if (thisDoor == bridgeSpec.lockingChainDoor()) + isLockingChain = true; + else if (thisDoor == bridgeSpec.issuingChainDoor()) + isLockingChain = false; + else + return tecINTERNAL; + } + + if (isLockingChain) + { + if (bridgeSpec.lockingChainIssue() != ctx.tx[sfAmount].issue()) + return tecXCHAIN_BAD_TRANSFER_ISSUE; + } + else + { + if (bridgeSpec.issuingChainIssue() != ctx.tx[sfAmount].issue()) + return tecXCHAIN_BAD_TRANSFER_ISSUE; + } + + return tesSUCCESS; +} + +TER +XChainCommit::doApply() +{ + PaymentSandbox psb(&ctx_.view()); + + auto const account = ctx_.tx[sfAccount]; + auto const amount = ctx_.tx[sfAmount]; + auto const bridgeSpec = ctx_.tx[sfXChainBridge]; + + if (!psb.read(keylet::account(account))) + return tecINTERNAL; + + auto const sleBridge = readBridge(psb, bridgeSpec); + if (!sleBridge) + return tecINTERNAL; + + auto const dst = (*sleBridge)[sfAccount]; + + // Support dipping into reserves to pay the fee + TransferHelperSubmittingAccountInfo submittingAccountInfo{ + account_, mPriorBalance, mSourceBalance}; + + auto const thTer = transferHelper( + psb, + account, + dst, + /*dstTag*/ std::nullopt, + /*claimOwner*/ std::nullopt, + amount, + CanCreateDstPolicy::no, + DepositAuthPolicy::normal, + submittingAccountInfo, + ctx_.journal); + + if (!isTesSuccess(thTer)) + return thTer; + + psb.apply(ctx_.rawView()); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +NotTEC +XChainCreateClaimID::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureXChainBridge)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + auto const reward = ctx.tx[sfSignatureReward]; + + if (!isXRP(reward) || reward.signum() < 0 || !isLegalNet(reward)) + return temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT; + + return preflight2(ctx); +} + +TER +XChainCreateClaimID::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + auto const bridgeSpec = ctx.tx[sfXChainBridge]; + auto const sleBridge = readBridge(ctx.view, bridgeSpec); + + if (!sleBridge) + { + return tecNO_ENTRY; + } + + // Check that the reward matches + auto const reward = ctx.tx[sfSignatureReward]; + + if (reward != (*sleBridge)[sfSignatureReward]) + { + return tecXCHAIN_REWARD_MISMATCH; + } + + { + // Check reserve + auto const sleAcc = ctx.view.read(keylet::account(account)); + if (!sleAcc) + return terNO_ACCOUNT; + + auto const balance = (*sleAcc)[sfBalance]; + auto const reserve = + ctx.view.fees().accountReserve((*sleAcc)[sfOwnerCount] + 1); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + return tesSUCCESS; +} + +TER +XChainCreateClaimID::doApply() +{ + auto const account = ctx_.tx[sfAccount]; + auto const bridgeSpec = ctx_.tx[sfXChainBridge]; + auto const reward = ctx_.tx[sfSignatureReward]; + auto const otherChainSrc = ctx_.tx[sfOtherChainSource]; + + auto const sleAcct = ctx_.view().peek(keylet::account(account)); + if (!sleAcct) + return tecINTERNAL; + + auto const sleBridge = peekBridge(ctx_.view(), bridgeSpec); + if (!sleBridge) + return tecINTERNAL; + + std::uint32_t const claimID = (*sleBridge)[sfXChainClaimID] + 1; + if (claimID == 0) + return tecINTERNAL; // overflow + + (*sleBridge)[sfXChainClaimID] = claimID; + + Keylet const claimIDKeylet = keylet::xChainClaimID(bridgeSpec, claimID); + if (ctx_.view().exists(claimIDKeylet)) + return tecINTERNAL; // already checked out!?! + + auto const sleClaimID = std::make_shared(claimIDKeylet); + + (*sleClaimID)[sfAccount] = account; + (*sleClaimID)[sfXChainBridge] = bridgeSpec; + (*sleClaimID)[sfXChainClaimID] = claimID; + (*sleClaimID)[sfOtherChainSource] = otherChainSrc; + (*sleClaimID)[sfSignatureReward] = reward; + sleClaimID->setFieldArray( + sfXChainClaimAttestations, STArray{sfXChainClaimAttestations}); + + // Add to owner directory + { + auto const page = ctx_.view().dirInsert( + keylet::ownerDir(account), + claimIDKeylet, + describeOwnerDir(account)); + if (!page) + return tecDIR_FULL; + (*sleClaimID)[sfOwnerNode] = *page; + } + + adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal); + + ctx_.view().insert(sleClaimID); + ctx_.view().update(sleBridge); + ctx_.view().update(sleAcct); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +NotTEC +XChainAddClaimAttestation::preflight(PreflightContext const& ctx) +{ + return attestationPreflight(ctx); +} + +TER +XChainAddClaimAttestation::preclaim(PreclaimContext const& ctx) +{ + return attestationPreclaim(ctx); +} + +TER +XChainAddClaimAttestation::doApply() +{ + return attestationDoApply(ctx_); +} + +//------------------------------------------------------------------------------ + +NotTEC +XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx) +{ + return attestationPreflight(ctx); +} + +TER +XChainAddAccountCreateAttestation::preclaim(PreclaimContext const& ctx) +{ + return attestationPreclaim(ctx); +} + +TER +XChainAddAccountCreateAttestation::doApply() +{ + return attestationDoApply(ctx_); +} + +//------------------------------------------------------------------------------ + +NotTEC +XChainCreateAccountCommit::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureXChainBridge)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + auto const amount = ctx.tx[sfAmount]; + + if (amount.signum() <= 0 || !amount.native()) + return temBAD_AMOUNT; + + auto const reward = ctx.tx[sfSignatureReward]; + if (reward.signum() < 0 || !reward.native()) + return temBAD_AMOUNT; + + if (reward.issue() != amount.issue()) + return temBAD_AMOUNT; + + return preflight2(ctx); +} + +TER +XChainCreateAccountCommit::preclaim(PreclaimContext const& ctx) +{ + STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; + STAmount const amount = ctx.tx[sfAmount]; + STAmount const reward = ctx.tx[sfSignatureReward]; + + auto const sleBridge = readBridge(ctx.view, bridgeSpec); + if (!sleBridge) + { + return tecNO_ENTRY; + } + + if (reward != (*sleBridge)[sfSignatureReward]) + { + return tecXCHAIN_REWARD_MISMATCH; + } + + std::optional const minCreateAmount = + (*sleBridge)[~sfMinAccountCreateAmount]; + + if (!minCreateAmount) + return tecXCHAIN_CREATE_ACCOUNT_DISABLED; + + if (amount < *minCreateAmount) + return tecXCHAIN_INSUFF_CREATE_AMOUNT; + + if (minCreateAmount->issue() != amount.issue()) + return tecXCHAIN_BAD_TRANSFER_ISSUE; + + AccountID const thisDoor = (*sleBridge)[sfAccount]; + AccountID const account = ctx.tx[sfAccount]; + if (thisDoor == account) + { + // Door account can't lock funds onto itself + return tecXCHAIN_SELF_COMMIT; + } + + STXChainBridge::ChainType srcChain = STXChainBridge::ChainType::locking; + { + if (thisDoor == bridgeSpec.lockingChainDoor()) + srcChain = STXChainBridge::ChainType::locking; + else if (thisDoor == bridgeSpec.issuingChainDoor()) + srcChain = STXChainBridge::ChainType::issuing; + else + return tecINTERNAL; + } + STXChainBridge::ChainType const dstChain = + STXChainBridge::otherChain(srcChain); + + if (bridgeSpec.issue(srcChain) != ctx.tx[sfAmount].issue()) + return tecXCHAIN_BAD_TRANSFER_ISSUE; + + if (!isXRP(bridgeSpec.issue(dstChain))) + return tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE; + + return tesSUCCESS; +} + +TER +XChainCreateAccountCommit::doApply() +{ + PaymentSandbox psb(&ctx_.view()); + + AccountID const account = ctx_.tx[sfAccount]; + STAmount const amount = ctx_.tx[sfAmount]; + STAmount const reward = ctx_.tx[sfSignatureReward]; + STXChainBridge const bridge = ctx_.tx[sfXChainBridge]; + + auto const sle = psb.peek(keylet::account(account)); + if (!sle) + return tecINTERNAL; + + auto const sleBridge = peekBridge(psb, bridge); + if (!sleBridge) + return tecINTERNAL; + + auto const dst = (*sleBridge)[sfAccount]; + + // Support dipping into reserves to pay the fee + TransferHelperSubmittingAccountInfo submittingAccountInfo{ + account_, mPriorBalance, mSourceBalance}; + STAmount const toTransfer = amount + reward; + auto const thTer = transferHelper( + psb, + account, + dst, + /*dstTag*/ std::nullopt, + /*claimOwner*/ std::nullopt, + toTransfer, + CanCreateDstPolicy::yes, + DepositAuthPolicy::normal, + submittingAccountInfo, + ctx_.journal); + + if (!isTesSuccess(thTer)) + return thTer; + + (*sleBridge)[sfXChainAccountCreateCount] = + (*sleBridge)[sfXChainAccountCreateCount] + 1; + psb.update(sleBridge); + + psb.apply(ctx_.rawView()); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/XChainBridge.h b/src/ripple/app/tx/impl/XChainBridge.h new file mode 100644 index 00000000000..4d41c7d1c21 --- /dev/null +++ b/src/ripple/app/tx/impl/XChainBridge.h @@ -0,0 +1,255 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_XCHAINBRIDGE_H_INCLUDED +#define RIPPLE_TX_XCHAINBRIDGE_H_INCLUDED + +#include +#include +#include + +namespace ripple { + +constexpr size_t xbridgeMaxAccountCreateClaims = 128; + +// Attach a new bridge to a door account. Once this is done, the cross-chain +// transfer transactions may be used to transfer funds from this account. +class XChainCreateBridge : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit XChainCreateBridge(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +class BridgeModify : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit BridgeModify(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; +//------------------------------------------------------------------------------ + +// Claim funds from a `XChainCommit` transaction. This is normally not needed, +// but may be used to handle transaction failures or if the destination account +// was not specified in the `XChainCommit` transaction. It may only be used +// after a quorum of signatures have been sent from the witness servers. +// +// If the transaction succeeds in moving funds, the referenced `XChainClaimID` +// ledger object will be destroyed. This prevents transaction replay. If the +// transaction fails, the `XChainClaimID` will not be destroyed and the +// transaction may be re-run with different parameters. +class XChainClaim : public Transactor +{ +public: + // Blocker since we cannot accurately calculate the consequences + static constexpr ConsequencesFactoryType ConsequencesFactory{Blocker}; + + explicit XChainClaim(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +// Put assets into trust on the locking-chain so they may be wrapped on the +// issuing-chain, or return wrapped assets on the issuing-chain so they can be +// unlocked on the locking-chain. The second step in a cross-chain transfer. +class XChainCommit : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + explicit XChainCommit(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +// Create a new claim id owned by the account. This is the first step in a +// cross-chain transfer. The claim id must be created on the destination chain +// before the `XChainCommit` transaction (which must reference this number) can +// be sent on the source chain. The account that will send the `XChainCommit` on +// the source chain must be specified in this transaction (see note on the +// `SourceAccount` field in the `XChainClaimID` ledger object for +// justification). The actual sequence number must be retrieved from a validated +// ledger. +class XChainCreateClaimID : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit XChainCreateClaimID(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +// Provide attestations from a witness server attesting to events on +// the other chain. The signatures must be from one of the keys on the door's +// signer's list at the time the signature was provided. However, if the +// signature list changes between the time the signature was submitted and the +// quorum is reached, the new signature set is used and some of the currently +// collected signatures may be removed. Also note the reward is only sent to +// accounts that have keys on the current list. +class XChainAddClaimAttestation : public Transactor +{ +public: + // Blocker since we cannot accurately calculate the consequences + static constexpr ConsequencesFactoryType ConsequencesFactory{Blocker}; + + explicit XChainAddClaimAttestation(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +class XChainAddAccountCreateAttestation : public Transactor +{ +public: + // Blocker since we cannot accurately calculate the consequences + static constexpr ConsequencesFactoryType ConsequencesFactory{Blocker}; + + explicit XChainAddAccountCreateAttestation(ApplyContext& ctx) + : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +// This is a special transaction used for creating accounts through a +// cross-chain transfer. A normal cross-chain transfer requires a "chain claim +// id" (which requires an existing account on the destination chain). One +// purpose of the "chain claim id" is to prevent transaction replay. For this +// transaction, we use a different mechanism: the accounts must be claimed on +// the destination chain in the same order that the `XChainCreateAccountCommit` +// transactions occurred on the source chain. +// +// This transaction can only be used for XRP to XRP bridges. +// +// IMPORTANT: This transaction should only be enabled if the witness +// attestations will be reliably delivered to the destination chain. If the +// signatures are not delivered (for example, the chain relies on user wallets +// to collect signatures) then account creation would be blocked for all +// transactions that happened after the one waiting on attestations. This could +// be used maliciously. To disable this transaction on XRP to XRP bridges, the +// bridge's `MinAccountCreateAmount` should not be present. +// +// Note: If this account already exists, the XRP is transferred to the existing +// account. However, note that unlike the `XChainCommit` transaction, there is +// no error handling mechanism. If the claim transaction fails, there is no +// mechanism for refunds. The funds are permanently lost. This transaction +// should still only be used for account creation. +class XChainCreateAccountCommit : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit XChainCreateAccountCommit(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index fdb84c271a0..d4d9e72a830 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -47,6 +47,8 @@ #include #include #include +#include +#include namespace ripple { @@ -168,6 +170,23 @@ invoke_preflight(PreflightContext const& ctx) return invoke_preflight_helper(ctx); case ttAMM_DELETE: return invoke_preflight_helper(ctx); + case ttXCHAIN_CREATE_BRIDGE: + return invoke_preflight_helper(ctx); + case ttXCHAIN_MODIFY_BRIDGE: + return invoke_preflight_helper(ctx); + case ttXCHAIN_CREATE_CLAIM_ID: + return invoke_preflight_helper(ctx); + case ttXCHAIN_COMMIT: + return invoke_preflight_helper(ctx); + case ttXCHAIN_CLAIM: + return invoke_preflight_helper(ctx); + case ttXCHAIN_ADD_CLAIM_ATTESTATION: + return invoke_preflight_helper(ctx); + case ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION: + return invoke_preflight_helper( + ctx); + case ttXCHAIN_ACCOUNT_CREATE_COMMIT: + return invoke_preflight_helper(ctx); default: assert(false); return {temUNKNOWN, TxConsequences{temUNKNOWN}}; @@ -283,6 +302,22 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttAMM_DELETE: return invoke_preclaim(ctx); + case ttXCHAIN_CREATE_BRIDGE: + return invoke_preclaim(ctx); + case ttXCHAIN_MODIFY_BRIDGE: + return invoke_preclaim(ctx); + case ttXCHAIN_CREATE_CLAIM_ID: + return invoke_preclaim(ctx); + case ttXCHAIN_COMMIT: + return invoke_preclaim(ctx); + case ttXCHAIN_CLAIM: + return invoke_preclaim(ctx); + case ttXCHAIN_ACCOUNT_CREATE_COMMIT: + return invoke_preclaim(ctx); + case ttXCHAIN_ADD_CLAIM_ATTESTATION: + return invoke_preclaim(ctx); + case ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION: + return invoke_preclaim(ctx); default: assert(false); return temUNKNOWN; @@ -360,6 +395,23 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return AMMBid::calculateBaseFee(view, tx); case ttAMM_DELETE: return AMMDelete::calculateBaseFee(view, tx); + case ttXCHAIN_CREATE_BRIDGE: + return XChainCreateBridge::calculateBaseFee(view, tx); + case ttXCHAIN_MODIFY_BRIDGE: + return BridgeModify::calculateBaseFee(view, tx); + case ttXCHAIN_CREATE_CLAIM_ID: + return XChainCreateClaimID::calculateBaseFee(view, tx); + case ttXCHAIN_COMMIT: + return XChainCommit::calculateBaseFee(view, tx); + case ttXCHAIN_CLAIM: + return XChainClaim::calculateBaseFee(view, tx); + case ttXCHAIN_ADD_CLAIM_ATTESTATION: + return XChainAddClaimAttestation::calculateBaseFee(view, tx); + case ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION: + return XChainAddAccountCreateAttestation::calculateBaseFee( + view, tx); + case ttXCHAIN_ACCOUNT_CREATE_COMMIT: + return XChainCreateAccountCommit::calculateBaseFee(view, tx); default: assert(false); return XRPAmount{0}; @@ -540,6 +592,38 @@ invoke_apply(ApplyContext& ctx) AMMDelete p(ctx); return p(); } + case ttXCHAIN_CREATE_BRIDGE: { + XChainCreateBridge p(ctx); + return p(); + } + case ttXCHAIN_MODIFY_BRIDGE: { + BridgeModify p(ctx); + return p(); + } + case ttXCHAIN_CREATE_CLAIM_ID: { + XChainCreateClaimID p(ctx); + return p(); + } + case ttXCHAIN_COMMIT: { + XChainCommit p(ctx); + return p(); + } + case ttXCHAIN_CLAIM: { + XChainClaim p(ctx); + return p(); + } + case ttXCHAIN_ADD_CLAIM_ATTESTATION: { + XChainAddClaimAttestation p(ctx); + return p(); + } + case ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION: { + XChainAddAccountCreateAttestation p(ctx); + return p(); + } + case ttXCHAIN_ACCOUNT_CREATE_COMMIT: { + XChainCreateAccountCommit p(ctx); + return p(); + } default: assert(false); return {temUNKNOWN, false}; diff --git a/src/ripple/beast/utility/Journal.h b/src/ripple/beast/utility/Journal.h index 333a743a658..0738748b6c5 100644 --- a/src/ripple/beast/utility/Journal.h +++ b/src/ripple/beast/utility/Journal.h @@ -134,7 +134,6 @@ class Journal class Stream; -private: /* Scoped ostream-based container for writing messages to a Journal. */ class ScopedStream { diff --git a/src/ripple/core/Config.h b/src/ripple/core/Config.h index 6236f89fc52..48bc9681e46 100644 --- a/src/ripple/core/Config.h +++ b/src/ripple/core/Config.h @@ -26,9 +26,11 @@ #include #include #include // VFALCO Breaks levelization + #include #include // VFALCO FIX: This include should not be here #include + #include #include #include @@ -37,6 +39,7 @@ #include #include #include +#include #include namespace ripple { @@ -295,6 +298,14 @@ class Config : public BasicConfig // First, attempt to load the latest ledger directly from disk. bool FAST_LOAD = false; + // When starting rippled with existing database it do not know it has those + // ledgers locally until the server naturally tries to backfill. This makes + // is difficult to test some functionality (in particular performance + // testing sidechains). With this variable the user is able to force rippled + // to consider the ledger range to be present. It should be used for testing + // only. + std::optional> + FORCED_LEDGER_RANGE_PRESENT; public: Config(); diff --git a/src/ripple/overlay/PeerReservationTable.h b/src/ripple/overlay/PeerReservationTable.h index 3242ee68a8d..e8fd4a29437 100644 --- a/src/ripple/overlay/PeerReservationTable.h +++ b/src/ripple/overlay/PeerReservationTable.h @@ -26,9 +26,6 @@ #include #include -#define SOCI_USE_BOOST -#include - #include #include #include diff --git a/src/ripple/protocol/AccountID.h b/src/ripple/protocol/AccountID.h index 79768eefd7d..27e1f452293 100644 --- a/src/ripple/protocol/AccountID.h +++ b/src/ripple/protocol/AccountID.h @@ -26,6 +26,8 @@ #include #include #include +#include + #include #include #include @@ -123,6 +125,21 @@ initAccountIdCache(std::size_t count); } // namespace ripple +//------------------------------------------------------------------------------ +namespace Json { +template <> +inline ripple::AccountID +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + using namespace ripple; + + std::string const b58 = getOrThrow(v, field); + if (auto const r = parseBase58(b58)) + return *r; + Throw(field.getJsonName(), "AccountID"); +} +} // namespace Json + //------------------------------------------------------------------------------ namespace std { diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 626b99b8cdb..df0570d3a52 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 61; +static constexpr std::size_t numFeatures = 62; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -348,6 +348,7 @@ extern uint256 const fixNonFungibleTokensV1_2; extern uint256 const fixNFTokenRemint; extern uint256 const fixReducedOffersV1; extern uint256 const featureClawback; +extern uint256 const featureXChainBridge; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 014ff82ef1b..0c83f7765a4 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -270,6 +271,15 @@ amm(Issue const& issue1, Issue const& issue2) noexcept; Keylet amm(uint256 const& amm) noexcept; +Keylet +bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType); + +Keylet +xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq); + +Keylet +xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index b9205e7888a..e907e299f52 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -91,6 +91,13 @@ enum LedgerEntryType : std::uint16_t */ ltOFFER = 0x006f, + + /** The ledger object which lists details about sidechains. + + \sa keylet::bridge + */ + ltBRIDGE = 0x0069, + /** A ledger object that contains a list of ledger hashes. This type is used to store the ledger hashes which the protocol uses @@ -109,6 +116,18 @@ enum LedgerEntryType : std::uint16_t */ ltAMENDMENTS = 0x0066, + /** A claim id for a cross chain transaction. + + \sa keylet::xChainClaimID + */ + ltXCHAIN_OWNED_CLAIM_ID = 0x0071, + + /** A claim id for a cross chain create account transaction. + + \sa keylet::xChainCreateAccountClaimID + */ + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID = 0x0074, + /** The ledger object which lists the network's fee settings. \note This is a singleton: only one such object exists in the ledger. diff --git a/src/ripple/protocol/PublicKey.h b/src/ripple/protocol/PublicKey.h index 406c6c58feb..58394cd82d4 100644 --- a/src/ripple/protocol/PublicKey.h +++ b/src/ripple/protocol/PublicKey.h @@ -24,7 +24,9 @@ #include #include #include +#include #include + #include #include #include @@ -268,4 +270,27 @@ calcAccountID(PublicKey const& pk); } // namespace ripple +//------------------------------------------------------------------------------ + +namespace Json { +template <> +inline ripple::PublicKey +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + using namespace ripple; + std::string const b58 = getOrThrow(v, field); + if (auto pubKeyBlob = strUnHex(b58); publicKeyType(makeSlice(*pubKeyBlob))) + { + return PublicKey{makeSlice(*pubKeyBlob)}; + } + for (auto const tokenType : + {TokenType::NodePublic, TokenType::AccountPublic}) + { + if (auto const pk = parseBase58(tokenType, b58)) + return *pk; + } + Throw(field.getJsonName(), "PublicKey"); +} +} // namespace Json + #endif diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index e1180bc1c93..7c802a64e1d 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -49,6 +49,7 @@ template class STBitString; template class STInteger; +class STXChainBridge; class STVector256; enum SerializedTypeID { @@ -79,6 +80,7 @@ enum SerializedTypeID { STI_UINT384 = 22, STI_UINT512 = 23, STI_ISSUE = 24, + STI_XCHAIN_BRIDGE = 25, // high level types // cannot be serialized inside other types @@ -318,6 +320,7 @@ using SF_AMOUNT = TypedField; using SF_ISSUE = TypedField; using SF_VL = TypedField; using SF_VECTOR256 = TypedField; +using SF_XCHAIN_BRIDGE = TypedField; //------------------------------------------------------------------------------ @@ -332,6 +335,7 @@ extern SField const sfMetadata; extern SF_UINT8 const sfCloseResolution; extern SF_UINT8 const sfMethod; extern SF_UINT8 const sfTransactionResult; +extern SF_UINT8 const sfWasLockingChainSend; // 8-bit integers (uncommon) extern SF_UINT8 const sfTickSize; @@ -424,6 +428,9 @@ extern SF_UINT64 const sfHookOn; extern SF_UINT64 const sfHookInstructionCount; extern SF_UINT64 const sfHookReturnCode; extern SF_UINT64 const sfReferenceCount; +extern SF_UINT64 const sfXChainClaimID; +extern SF_UINT64 const sfXChainAccountCreateCount; +extern SF_UINT64 const sfXChainAccountClaimCount; // 128-bit extern SF_UINT128 const sfEmailHash; @@ -499,6 +506,8 @@ extern SF_AMOUNT const sfLPTokenIn; extern SF_AMOUNT const sfBaseFeeDrops; extern SF_AMOUNT const sfReserveBaseDrops; extern SF_AMOUNT const sfReserveIncrementDrops; +extern SF_AMOUNT const sfSignatureReward; +extern SF_AMOUNT const sfMinAccountCreateAmount; // variable length (common) extern SF_VL const sfPublicKey; @@ -541,6 +550,12 @@ extern SF_ACCOUNT const sfEmitCallback; // account (uncommon) extern SF_ACCOUNT const sfHookAccount; +extern SF_ACCOUNT const sfOtherChainSource; +extern SF_ACCOUNT const sfOtherChainDestination; +extern SF_ACCOUNT const sfAttestationSignerAccount; +extern SF_ACCOUNT const sfAttestationRewardAccount; +extern SF_ACCOUNT const sfLockingChainDoor; +extern SF_ACCOUNT const sfIssuingChainDoor; // path set extern SField const sfPaths; @@ -548,6 +563,11 @@ extern SField const sfPaths; // issue extern SF_ISSUE const sfAsset; extern SF_ISSUE const sfAsset2; +extern SF_ISSUE const sfLockingChainIssue; +extern SF_ISSUE const sfIssuingChainIssue; + +// bridge +extern SF_XCHAIN_BRIDGE const sfXChainBridge; // vector of 256-bit extern SF_VECTOR256 const sfIndexes; @@ -582,6 +602,10 @@ extern SField const sfHookExecution; extern SField const sfHookDefinition; extern SField const sfHookParameter; extern SField const sfHookGrant; +extern SField const sfXChainClaimProofSig; +extern SField const sfXChainCreateAccountProofSig; +extern SField const sfXChainClaimAttestationCollectionElement; +extern SField const sfXChainCreateAccountAttestationCollectionElement; // array of objects (common) // ARRAY/1 is reserved for end of array @@ -604,6 +628,8 @@ extern SField const sfDisabledValidators; extern SField const sfHookExecutions; extern SField const sfHookParameters; extern SField const sfHookGrants; +extern SField const sfXChainClaimAttestations; +extern SField const sfXChainCreateAccountAttestations; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/STAccount.h b/src/ripple/protocol/STAccount.h index fb6d1c81bfe..c622a7c5eef 100644 --- a/src/ripple/protocol/STAccount.h +++ b/src/ripple/protocol/STAccount.h @@ -64,7 +64,7 @@ class STAccount final : public STBase STAccount& operator=(AccountID const& value); - AccountID + AccountID const& value() const noexcept; void @@ -86,7 +86,7 @@ STAccount::operator=(AccountID const& value) return *this; } -inline AccountID +inline AccountID const& STAccount::value() const noexcept { return value_; @@ -99,6 +99,36 @@ STAccount::setValue(AccountID const& v) default_ = false; } +inline bool +operator==(STAccount const& lhs, STAccount const& rhs) +{ + return lhs.value() == rhs.value(); +} + +inline auto +operator<(STAccount const& lhs, STAccount const& rhs) +{ + return lhs.value() < rhs.value(); +} + +inline bool +operator==(STAccount const& lhs, AccountID const& rhs) +{ + return lhs.value() == rhs; +} + +inline auto +operator<(STAccount const& lhs, AccountID const& rhs) +{ + return lhs.value() < rhs; +} + +inline auto +operator<(AccountID const& lhs, STAccount const& rhs) +{ + return lhs < rhs.value(); +} + } // namespace ripple #endif diff --git a/src/ripple/protocol/STAmount.h b/src/ripple/protocol/STAmount.h index 63f97bb48fe..1de1568ae03 100644 --- a/src/ripple/protocol/STAmount.h +++ b/src/ripple/protocol/STAmount.h @@ -29,6 +29,7 @@ #include #include #include +#include namespace ripple { @@ -125,6 +126,8 @@ class STAmount final : public STBase, public CountedObject explicit STAmount(std::uint64_t mantissa = 0, bool negative = false); + explicit STAmount(SField const& name, STAmount const& amt); + STAmount( Issue const& issue, std::uint64_t mantissa = 0, @@ -582,4 +585,18 @@ class STAmountSO } // namespace ripple +//------------------------------------------------------------------------------ +namespace Json { +template <> +inline ripple::STAmount +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + using namespace ripple; + Json::StaticString const& key = field.getJsonName(); + if (!v.isMember(key)) + Throw(key); + Json::Value const& inner = v[key]; + return amountFromJson(field, inner); +} +} // namespace Json #endif diff --git a/src/ripple/protocol/STArray.h b/src/ripple/protocol/STArray.h index 9501307c2c9..8c3833d3fc8 100644 --- a/src/ripple/protocol/STArray.h +++ b/src/ripple/protocol/STArray.h @@ -61,7 +61,7 @@ class STArray final : public STBase, public CountedObject STArray& operator=(STArray&&); - STArray(SField const& f, int n); + STArray(SField const& f, std::size_t n); STArray(SerialIter& sit, SField const& f, int depth = 0); explicit STArray(int n); explicit STArray(SField const& f); diff --git a/src/ripple/protocol/STIssue.h b/src/ripple/protocol/STIssue.h index c24cf6a30b5..80a37b305fc 100644 --- a/src/ripple/protocol/STIssue.h +++ b/src/ripple/protocol/STIssue.h @@ -100,6 +100,10 @@ STIssue::value() const noexcept inline void STIssue::setIssue(Issue const& issue) { + if (isXRP(issue_.currency) != isXRP(issue_.account)) + Throw( + "invalid issue: currency and account native mismatch"); + issue_ = issue; } diff --git a/src/ripple/protocol/STXChainBridge.h b/src/ripple/protocol/STXChainBridge.h new file mode 100644 index 00000000000..44cd6a480f7 --- /dev/null +++ b/src/ripple/protocol/STXChainBridge.h @@ -0,0 +1,235 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_PROTOCOL_STXCHAINBRIDGE_H_INCLUDED +#define RIPPLE_PROTOCOL_STXCHAINBRIDGE_H_INCLUDED + +#include +#include +#include + +namespace ripple { + +class Serializer; +class STObject; + +class STXChainBridge final : public STBase +{ + STAccount lockingChainDoor_{sfLockingChainDoor}; + STIssue lockingChainIssue_{sfLockingChainIssue}; + STAccount issuingChainDoor_{sfIssuingChainDoor}; + STIssue issuingChainIssue_{sfIssuingChainIssue}; + +public: + using value_type = STXChainBridge; + + enum class ChainType { locking, issuing }; + + static ChainType + otherChain(ChainType ct); + + static ChainType + srcChain(bool wasLockingChainSend); + + static ChainType + dstChain(bool wasLockingChainSend); + + STXChainBridge(); + + explicit STXChainBridge(SField const& name); + + STXChainBridge(STXChainBridge const& rhs) = default; + + STXChainBridge(STObject const& o); + + STXChainBridge( + AccountID const& srcChainDoor, + Issue const& srcChainIssue, + AccountID const& dstChainDoor, + Issue const& dstChainIssue); + + explicit STXChainBridge(Json::Value const& v); + + explicit STXChainBridge(SField const& name, Json::Value const& v); + + explicit STXChainBridge(SerialIter& sit, SField const& name); + + STXChainBridge& + operator=(STXChainBridge const& rhs) = default; + + std::string + getText() const override; + + STObject + toSTObject() const; + + AccountID const& + lockingChainDoor() const; + + Issue const& + lockingChainIssue() const; + + AccountID const& + issuingChainDoor() const; + + Issue const& + issuingChainIssue() const; + + AccountID const& + door(ChainType ct) const; + + Issue const& + issue(ChainType ct) const; + + SerializedTypeID + getSType() const override; + + Json::Value getJson(JsonOptions) const override; + + void + add(Serializer& s) const override; + + bool + isEquivalent(const STBase& t) const override; + + bool + isDefault() const override; + + value_type const& + value() const noexcept; + +private: + static std::unique_ptr + construct(SerialIter&, SField const& name); + + STBase* + copy(std::size_t n, void* buf) const override; + STBase* + move(std::size_t n, void* buf) override; + + friend bool + operator==(STXChainBridge const& lhs, STXChainBridge const& rhs); + + friend bool + operator<(STXChainBridge const& lhs, STXChainBridge const& rhs); +}; + +inline bool +operator==(STXChainBridge const& lhs, STXChainBridge const& rhs) +{ + return std::tie( + lhs.lockingChainDoor_, + lhs.lockingChainIssue_, + lhs.issuingChainDoor_, + lhs.issuingChainIssue_) == + std::tie( + rhs.lockingChainDoor_, + rhs.lockingChainIssue_, + rhs.issuingChainDoor_, + rhs.issuingChainIssue_); +} + +inline bool +operator<(STXChainBridge const& lhs, STXChainBridge const& rhs) +{ + return std::tie( + lhs.lockingChainDoor_, + lhs.lockingChainIssue_, + lhs.issuingChainDoor_, + lhs.issuingChainIssue_) < + std::tie( + rhs.lockingChainDoor_, + rhs.lockingChainIssue_, + rhs.issuingChainDoor_, + rhs.issuingChainIssue_); +} + +inline AccountID const& +STXChainBridge::lockingChainDoor() const +{ + return lockingChainDoor_.value(); +}; + +inline Issue const& +STXChainBridge::lockingChainIssue() const +{ + return lockingChainIssue_.value(); +}; + +inline AccountID const& +STXChainBridge::issuingChainDoor() const +{ + return issuingChainDoor_.value(); +}; + +inline Issue const& +STXChainBridge::issuingChainIssue() const +{ + return issuingChainIssue_.value(); +}; + +inline STXChainBridge::value_type const& +STXChainBridge::value() const noexcept +{ + return *this; +} + +inline AccountID const& +STXChainBridge::door(ChainType ct) const +{ + if (ct == ChainType::locking) + return lockingChainDoor(); + return issuingChainDoor(); +} + +inline Issue const& +STXChainBridge::issue(ChainType ct) const +{ + if (ct == ChainType::locking) + return lockingChainIssue(); + return issuingChainIssue(); +} + +inline STXChainBridge::ChainType +STXChainBridge::otherChain(ChainType ct) +{ + if (ct == ChainType::locking) + return ChainType::issuing; + return ChainType::locking; +} + +inline STXChainBridge::ChainType +STXChainBridge::srcChain(bool wasLockingChainSend) +{ + if (wasLockingChainSend) + return ChainType::locking; + return ChainType::issuing; +} + +inline STXChainBridge::ChainType +STXChainBridge::dstChain(bool wasLockingChainSend) +{ + if (wasLockingChainSend) + return ChainType::issuing; + return ChainType::locking; +} + +} // namespace ripple + +#endif diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index edae58d83c9..4cabac1dd13 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -125,6 +125,13 @@ enum TEMcodes : TERUnderlyingType { temBAD_NFTOKEN_TRANSFER_FEE, temBAD_AMM_TOKENS, + + temXCHAIN_EQUAL_DOOR_ACCOUNTS, + temXCHAIN_BAD_PROOF, + temXCHAIN_BRIDGE_BAD_ISSUES, + temXCHAIN_BRIDGE_NONDOOR_OWNER, + temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT, + temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, }; //------------------------------------------------------------------------------ @@ -303,7 +310,24 @@ enum TECcodes : TERUnderlyingType { tecAMM_EMPTY = 166, tecAMM_NOT_EMPTY = 167, tecAMM_ACCOUNT = 168, - tecINCOMPLETE = 169 + tecINCOMPLETE = 169, + tecXCHAIN_BAD_TRANSFER_ISSUE = 170, + tecXCHAIN_NO_CLAIM_ID = 171, + tecXCHAIN_BAD_CLAIM_ID = 172, + tecXCHAIN_CLAIM_NO_QUORUM = 173, + tecXCHAIN_PROOF_UNKNOWN_KEY = 174, + tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE = 175, + tecXCHAIN_WRONG_CHAIN = 176, + tecXCHAIN_REWARD_MISMATCH = 177, + tecXCHAIN_NO_SIGNERS_LIST = 178, + tecXCHAIN_SENDING_ACCOUNT_MISMATCH = 179, + tecXCHAIN_INSUFF_CREATE_AMOUNT = 180, + tecXCHAIN_ACCOUNT_CREATE_PAST = 181, + tecXCHAIN_ACCOUNT_CREATE_TOO_MANY = 182, + tecXCHAIN_PAYMENT_FAILED = 183, + tecXCHAIN_SELF_COMMIT = 184, + tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 185, + tecXCHAIN_CREATE_ACCOUNT_DISABLED = 186, }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index 39680e41d95..ba2b97562db 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/protocol/TxFlags.h @@ -181,6 +181,10 @@ constexpr std::uint32_t tfDepositSubTx = constexpr std::uint32_t tfWithdrawMask = ~(tfUniversal | tfWithdrawSubTx); constexpr std::uint32_t tfDepositMask = ~(tfUniversal | tfDepositSubTx); +// BridgeModify flags: +constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000; +constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount); + // clang-format on } // namespace ripple diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 2d7ba40c44c..d8785f3ea1d 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -160,6 +160,31 @@ enum TxType : std::uint16_t /** This transaction type deletes AMM in the empty state */ ttAMM_DELETE = 40, + /** This transactions creates a crosschain sequence number */ + ttXCHAIN_CREATE_CLAIM_ID = 41, + + /** This transactions initiates a crosschain transaction */ + ttXCHAIN_COMMIT = 42, + + /** This transaction completes a crosschain transaction */ + ttXCHAIN_CLAIM = 43, + + /** This transaction initiates a crosschain account create transaction */ + ttXCHAIN_ACCOUNT_CREATE_COMMIT = 44, + + /** This transaction adds an attestation to a claimid*/ + ttXCHAIN_ADD_CLAIM_ATTESTATION = 45, + + /** This transaction adds an attestation to a claimid*/ + ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION = 46, + + /** This transaction modifies a sidechain */ + ttXCHAIN_MODIFY_BRIDGE = 47, + + /** This transactions creates a sidechain */ + ttXCHAIN_CREATE_BRIDGE = 48, + + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/src/ripple/protocol/XChainAttestations.h b/src/ripple/protocol/XChainAttestations.h new file mode 100644 index 00000000000..8fa1a03118a --- /dev/null +++ b/src/ripple/protocol/XChainAttestations.h @@ -0,0 +1,514 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_PROTOCOL_STXATTESTATIONS_H_INCLUDED +#define RIPPLE_PROTOCOL_STXATTESTATIONS_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace ripple { + +namespace Attestations { + +struct AttestationBase +{ + // Account associated with the public key + AccountID attestationSignerAccount; + // Public key from the witness server attesting to the event + PublicKey publicKey; + // Signature from the witness server attesting to the event + Buffer signature; + // Account on the sending chain that triggered the event (sent the + // transaction) + AccountID sendingAccount; + // Amount transfered on the sending chain + STAmount sendingAmount; + // Account on the destination chain that collects a share of the attestation + // reward + AccountID rewardAccount; + // Amount was transfered on the locking chain + bool wasLockingChainSend; + + explicit AttestationBase( + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + Buffer signature_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_); + + AttestationBase(AttestationBase const&) = default; + + virtual ~AttestationBase() = default; + + AttestationBase& + operator=(AttestationBase const&) = default; + + // verify that the signature attests to the data. + bool + verify(STXChainBridge const& bridge) const; + +protected: + explicit AttestationBase(STObject const& o); + explicit AttestationBase(Json::Value const& v); + + [[nodiscard]] static bool + equalHelper(AttestationBase const& lhs, AttestationBase const& rhs); + + [[nodiscard]] static bool + sameEventHelper(AttestationBase const& lhs, AttestationBase const& rhs); + + void + addHelper(STObject& o) const; + +private: + [[nodiscard]] virtual std::vector + message(STXChainBridge const& bridge) const = 0; +}; + +// Attest to a regular cross-chain transfer +struct AttestationClaim : AttestationBase +{ + std::uint64_t claimID; + std::optional dst; + + explicit AttestationClaim( + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + Buffer signature_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::uint64_t claimID_, + std::optional const& dst_); + + explicit AttestationClaim( + STXChainBridge const& bridge, + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + SecretKey const& secretKey_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::uint64_t claimID_, + std::optional const& dst_); + + explicit AttestationClaim(STObject const& o); + explicit AttestationClaim(Json::Value const& v); + + [[nodiscard]] STObject + toSTObject() const; + + // return true if the two attestations attest to the same thing + [[nodiscard]] bool + sameEvent(AttestationClaim const& rhs) const; + + [[nodiscard]] static std::vector + message( + STXChainBridge const& bridge, + AccountID const& sendingAccount, + STAmount const& sendingAmount, + AccountID const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t claimID, + std::optional const& dst); + + [[nodiscard]] bool + validAmounts() const; + +private: + [[nodiscard]] std::vector + message(STXChainBridge const& bridge) const override; + + friend bool + operator==(AttestationClaim const& lhs, AttestationClaim const& rhs); +}; + +struct CmpByClaimID +{ + bool + operator()(AttestationClaim const& lhs, AttestationClaim const& rhs) const + { + return lhs.claimID < rhs.claimID; + } +}; + +// Attest to a cross-chain transfer that creates an account +struct AttestationCreateAccount : AttestationBase +{ + // createCount on the sending chain. This is the value of the `CreateCount` + // field of the bridge on the sending chain when the transaction was + // executed. + std::uint64_t createCount; + // Account to create on the destination chain + AccountID toCreate; + // Total amount of the reward pool + STAmount rewardAmount; + + explicit AttestationCreateAccount(STObject const& o); + + explicit AttestationCreateAccount(Json::Value const& v); + + explicit AttestationCreateAccount( + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + Buffer signature_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + STAmount const& rewardAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::uint64_t createCount_, + AccountID const& toCreate_); + + explicit AttestationCreateAccount( + STXChainBridge const& bridge, + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + SecretKey const& secretKey_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + STAmount const& rewardAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::uint64_t createCount_, + AccountID const& toCreate_); + + [[nodiscard]] STObject + toSTObject() const; + + // return true if the two attestations attest to the same thing + [[nodiscard]] bool + sameEvent(AttestationCreateAccount const& rhs) const; + + friend bool + operator==( + AttestationCreateAccount const& lhs, + AttestationCreateAccount const& rhs); + + [[nodiscard]] static std::vector + message( + STXChainBridge const& bridge, + AccountID const& sendingAccount, + STAmount const& sendingAmount, + STAmount const& rewardAmount, + AccountID const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t createCount, + AccountID const& dst); + + [[nodiscard]] bool + validAmounts() const; + +private: + [[nodiscard]] std::vector + message(STXChainBridge const& bridge) const override; +}; + +struct CmpByCreateCount +{ + bool + operator()( + AttestationCreateAccount const& lhs, + AttestationCreateAccount const& rhs) const + { + return lhs.createCount < rhs.createCount; + } +}; + +}; // namespace Attestations + +// Result when checking when two attestation match. +enum class AttestationMatch { + // One of the fields doesn't match, and it isn't the dst field + nonDstMismatch, + // all of the fields match, except the dst field + matchExceptDst, + // all of the fields match + match +}; + +struct XChainClaimAttestation +{ + using TSignedAttestation = Attestations::AttestationClaim; + static SField const& ArrayFieldName; + + AccountID keyAccount; + PublicKey publicKey; + STAmount amount; + AccountID rewardAccount; + bool wasLockingChainSend; + std::optional dst; + + struct MatchFields + { + STAmount amount; + bool wasLockingChainSend; + std::optional dst; + MatchFields(TSignedAttestation const& att); + MatchFields( + STAmount const& a, + bool b, + std::optional const& d) + : amount{a}, wasLockingChainSend{b}, dst{d} + { + } + }; + + explicit XChainClaimAttestation( + AccountID const& keyAccount_, + PublicKey const& publicKey_, + STAmount const& amount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::optional const& dst); + + explicit XChainClaimAttestation( + STAccount const& keyAccount_, + PublicKey const& publicKey_, + STAmount const& amount_, + STAccount const& rewardAccount_, + bool wasLockingChainSend_, + std::optional const& dst); + + explicit XChainClaimAttestation(TSignedAttestation const& claimAtt); + + explicit XChainClaimAttestation(STObject const& o); + + explicit XChainClaimAttestation(Json::Value const& v); + + AttestationMatch + match(MatchFields const& rhs) const; + + [[nodiscard]] STObject + toSTObject() const; + + friend bool + operator==( + XChainClaimAttestation const& lhs, + XChainClaimAttestation const& rhs); +}; + +struct XChainCreateAccountAttestation +{ + using TSignedAttestation = Attestations::AttestationCreateAccount; + static SField const& ArrayFieldName; + + AccountID keyAccount; + PublicKey publicKey; + STAmount amount; + STAmount rewardAmount; + AccountID rewardAccount; + bool wasLockingChainSend; + AccountID dst; + + struct MatchFields + { + STAmount amount; + STAmount rewardAmount; + bool wasLockingChainSend; + AccountID dst; + + MatchFields(TSignedAttestation const& att); + }; + + explicit XChainCreateAccountAttestation( + AccountID const& keyAccount_, + PublicKey const& publicKey_, + STAmount const& amount_, + STAmount const& rewardAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + AccountID const& dst_); + + explicit XChainCreateAccountAttestation(TSignedAttestation const& claimAtt); + + explicit XChainCreateAccountAttestation(STObject const& o); + + explicit XChainCreateAccountAttestation(Json::Value const& v); + + [[nodiscard]] STObject + toSTObject() const; + + AttestationMatch + match(MatchFields const& rhs) const; + + friend bool + operator==( + XChainCreateAccountAttestation const& lhs, + XChainCreateAccountAttestation const& rhs); +}; + +// Attestations from witness servers for a particular claimid and bridge. +// Only one attestation per signature is allowed. +template +class XChainAttestationsBase +{ +public: + using AttCollection = std::vector; + +private: + // Set a max number of allowed attestations to limit the amount of memory + // allocated and processing time. This number is much larger than the actual + // number of attestation a server would ever expect. + static constexpr std::uint32_t maxAttestations = 256; + AttCollection attestations_; + +protected: + // Prevent slicing to the base class + ~XChainAttestationsBase() = default; + +public: + XChainAttestationsBase() = default; + XChainAttestationsBase(XChainAttestationsBase const& rhs) = default; + XChainAttestationsBase& + operator=(XChainAttestationsBase const& rhs) = default; + + explicit XChainAttestationsBase(AttCollection&& sigs); + + explicit XChainAttestationsBase(Json::Value const& v); + + explicit XChainAttestationsBase(STArray const& arr); + + [[nodiscard]] STArray + toSTArray() const; + + typename AttCollection::const_iterator + begin() const; + + typename AttCollection::const_iterator + end() const; + + typename AttCollection::iterator + begin(); + + typename AttCollection::iterator + end(); + + template + std::size_t + erase_if(F&& f); + + std::size_t + size() const; + + bool + empty() const; + + AttCollection const& + attestations() const; + + template + void + emplace_back(T&& att); + + // verify that all the signatures attest to transaction data. + [[nodiscard]] bool + verify() const; + +protected: + // Return the message that was expected to be signed by the attesters given + // the data to be proved. + [[nodiscard]] std::vector + message() const; +}; + +template +[[nodiscard]] inline bool +operator==( + XChainAttestationsBase const& lhs, + XChainAttestationsBase const& rhs) +{ + return lhs.attestations() == rhs.attestations(); +} + +template +inline typename XChainAttestationsBase::AttCollection const& +XChainAttestationsBase::attestations() const +{ + return attestations_; +}; + +template +template +inline void +XChainAttestationsBase::emplace_back(T&& att) +{ + attestations_.emplace_back(std::forward(att)); +}; + +template +template +inline std::size_t +XChainAttestationsBase::erase_if(F&& f) +{ + return std::erase_if(attestations_, std::forward(f)); +} + +template +inline std::size_t +XChainAttestationsBase::size() const +{ + return attestations_.size(); +} + +template +inline bool +XChainAttestationsBase::empty() const +{ + return attestations_.empty(); +} + +class XChainClaimAttestations final + : public XChainAttestationsBase +{ + using TBase = XChainAttestationsBase; + using TBase::TBase; +}; + +class XChainCreateAccountAttestations final + : public XChainAttestationsBase +{ + using TBase = XChainAttestationsBase; + using TBase::TBase; +}; + +} // namespace ripple + +#endif // STXCHAINATTESTATIONS_H_ diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index b9710ebbc69..dcb35641360 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -455,6 +455,7 @@ REGISTER_FIX (fixNFTokenRemint, Supported::yes, VoteBehavior::De REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FEATURE(XChainBridge, Supported::no, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 1140c42d3ef..3fef856b365 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -18,9 +18,13 @@ //============================================================================== #include +#include +#include +#include #include #include #include + #include #include @@ -64,6 +68,9 @@ enum class LedgerNameSpace : std::uint16_t { NFTOKEN_BUY_OFFERS = 'h', NFTOKEN_SELL_OFFERS = 'i', AMM = 'A', + BRIDGE = 'H', + XCHAIN_CLAIM_ID = 'Q', + XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -389,6 +396,47 @@ amm(uint256 const& id) noexcept return {ltAMM, id}; } +Keylet +bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType) +{ + // A door account can support multiple bridges. On the locking chain + // there can only be one bridge per lockingChainCurrency. On the issuing + // chain there can only be one bridge per issuingChainCurrency. + auto const& issue = bridge.issue(chainType); + return { + ltBRIDGE, + indexHash( + LedgerNameSpace::BRIDGE, bridge.door(chainType), issue.currency)}; +} + +Keylet +xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq) +{ + return { + ltXCHAIN_OWNED_CLAIM_ID, + indexHash( + LedgerNameSpace::XCHAIN_CLAIM_ID, + bridge.lockingChainDoor(), + bridge.lockingChainIssue(), + bridge.issuingChainDoor(), + bridge.issuingChainIssue(), + seq)}; +} + +Keylet +xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq) +{ + return { + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID, + indexHash( + LedgerNameSpace::XCHAIN_CREATE_ACCOUNT_CLAIM_ID, + bridge.lockingChainDoor(), + bridge.lockingChainIssue(), + bridge.issuingChainDoor(), + bridge.issuingChainIssue(), + seq)}; +} + } // namespace keylet } // namespace ripple diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index ba1a40a87ee..58f4392f536 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -18,6 +18,8 @@ //============================================================================== #include +#include +#include namespace ripple { @@ -70,12 +72,62 @@ InnerObjectFormats::InnerObjectFormats() add(sfAuctionSlot.jsonName.c_str(), sfAuctionSlot.getCode(), + {{sfAccount, soeREQUIRED}, + {sfExpiration, soeREQUIRED}, + {sfDiscountedFee, soeDEFAULT}, + {sfPrice, soeREQUIRED}, + {sfAuthAccounts, soeOPTIONAL}}); + + add(sfXChainClaimAttestationCollectionElement.jsonName.c_str(), + sfXChainClaimAttestationCollectionElement.getCode(), { + {sfAttestationSignerAccount, soeREQUIRED}, + {sfPublicKey, soeREQUIRED}, + {sfSignature, soeREQUIRED}, + {sfAmount, soeREQUIRED}, {sfAccount, soeREQUIRED}, - {sfExpiration, soeREQUIRED}, - {sfDiscountedFee, soeDEFAULT}, - {sfPrice, soeREQUIRED}, - {sfAuthAccounts, soeOPTIONAL}, + {sfAttestationRewardAccount, soeREQUIRED}, + {sfWasLockingChainSend, soeREQUIRED}, + {sfXChainClaimID, soeREQUIRED}, + {sfDestination, soeOPTIONAL}, + }); + + add(sfXChainCreateAccountAttestationCollectionElement.jsonName.c_str(), + sfXChainCreateAccountAttestationCollectionElement.getCode(), + { + {sfAttestationSignerAccount, soeREQUIRED}, + {sfPublicKey, soeREQUIRED}, + {sfSignature, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfAccount, soeREQUIRED}, + {sfAttestationRewardAccount, soeREQUIRED}, + {sfWasLockingChainSend, soeREQUIRED}, + {sfXChainAccountCreateCount, soeREQUIRED}, + {sfDestination, soeREQUIRED}, + {sfSignatureReward, soeREQUIRED}, + }); + + add(sfXChainClaimProofSig.jsonName.c_str(), + sfXChainClaimProofSig.getCode(), + { + {sfAttestationSignerAccount, soeREQUIRED}, + {sfPublicKey, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfAttestationRewardAccount, soeREQUIRED}, + {sfWasLockingChainSend, soeREQUIRED}, + {sfDestination, soeOPTIONAL}, + }); + + add(sfXChainCreateAccountProofSig.jsonName.c_str(), + sfXChainCreateAccountProofSig.getCode(), + { + {sfAttestationSignerAccount, soeREQUIRED}, + {sfPublicKey, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfSignatureReward, soeREQUIRED}, + {sfAttestationRewardAccount, soeREQUIRED}, + {sfWasLockingChainSend, soeREQUIRED}, + {sfDestination, soeREQUIRED}, }); add(sfAuthAccount.jsonName.c_str(), diff --git a/src/ripple/protocol/impl/Issue.cpp b/src/ripple/protocol/impl/Issue.cpp index 6d0be069a8a..623ce24bb15 100644 --- a/src/ripple/protocol/impl/Issue.cpp +++ b/src/ripple/protocol/impl/Issue.cpp @@ -79,8 +79,8 @@ issueFromJson(Json::Value const& v) { if (!v.isObject()) { - Throw( - "issueFromJson can only be specified with a 'object' Json value"); + Throw( + "issueFromJson can only be specified with an 'object' Json value"); } Json::Value const curStr = v[jss::currency]; diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index d9e7ca178c0..e5313a8c1f9 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -283,6 +283,49 @@ LedgerFormats::LedgerFormats() }, commonFields); + add(jss::Bridge, + ltBRIDGE, + { + {sfAccount, soeREQUIRED}, + {sfSignatureReward, soeREQUIRED}, + {sfMinAccountCreateAmount, soeOPTIONAL}, + {sfXChainBridge, soeREQUIRED}, + {sfXChainClaimID, soeREQUIRED}, + {sfXChainAccountCreateCount, soeREQUIRED}, + {sfXChainAccountClaimCount, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); + + add(jss::XChainOwnedClaimID, + ltXCHAIN_OWNED_CLAIM_ID, + { + {sfAccount, soeREQUIRED}, + {sfXChainBridge, soeREQUIRED}, + {sfXChainClaimID, soeREQUIRED}, + {sfOtherChainSource, soeREQUIRED}, + {sfXChainClaimAttestations, soeREQUIRED}, + {sfSignatureReward, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); + + add(jss::XChainOwnedCreateAccountClaimID, + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID, + { + {sfAccount, soeREQUIRED}, + {sfXChainBridge, soeREQUIRED}, + {sfXChainAccountCreateCount, soeREQUIRED}, + {sfXChainCreateAccountAttestations, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); // clang-format on } diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 95b6d123941..517971dbf07 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -96,6 +96,7 @@ CONSTRUCT_TYPED_SFIELD(sfTransactionResult, "TransactionResult", UINT8, CONSTRUCT_TYPED_SFIELD(sfTickSize, "TickSize", UINT8, 16); CONSTRUCT_TYPED_SFIELD(sfUNLModifyDisabling, "UNLModifyDisabling", UINT8, 17); CONSTRUCT_TYPED_SFIELD(sfHookResult, "HookResult", UINT8, 18); +CONSTRUCT_TYPED_SFIELD(sfWasLockingChainSend, "WasLockingChainSend", UINT8, 19); // 16-bit integers CONSTRUCT_TYPED_SFIELD(sfLedgerEntryType, "LedgerEntryType", UINT16, 1, SField::sMD_Never); @@ -180,10 +181,13 @@ CONSTRUCT_TYPED_SFIELD(sfNFTokenOfferNode, "NFTokenOfferNode", UINT64, CONSTRUCT_TYPED_SFIELD(sfEmitBurden, "EmitBurden", UINT64, 13); // 64-bit integers (uncommon) -CONSTRUCT_TYPED_SFIELD(sfHookOn, "HookOn", UINT64, 16); -CONSTRUCT_TYPED_SFIELD(sfHookInstructionCount, "HookInstructionCount", UINT64, 17); -CONSTRUCT_TYPED_SFIELD(sfHookReturnCode, "HookReturnCode", UINT64, 18); -CONSTRUCT_TYPED_SFIELD(sfReferenceCount, "ReferenceCount", UINT64, 19); +CONSTRUCT_TYPED_SFIELD(sfHookOn, "HookOn", UINT64, 16); +CONSTRUCT_TYPED_SFIELD(sfHookInstructionCount, "HookInstructionCount", UINT64, 17); +CONSTRUCT_TYPED_SFIELD(sfHookReturnCode, "HookReturnCode", UINT64, 18); +CONSTRUCT_TYPED_SFIELD(sfReferenceCount, "ReferenceCount", UINT64, 19); +CONSTRUCT_TYPED_SFIELD(sfXChainClaimID, "XChainClaimID", UINT64, 20); +CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21); +CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); @@ -263,7 +267,8 @@ CONSTRUCT_TYPED_SFIELD(sfLPTokenOut, "LPTokenOut", AMOUNT, CONSTRUCT_TYPED_SFIELD(sfLPTokenIn, "LPTokenIn", AMOUNT, 26); CONSTRUCT_TYPED_SFIELD(sfEPrice, "EPrice", AMOUNT, 27); CONSTRUCT_TYPED_SFIELD(sfPrice, "Price", AMOUNT, 28); -// 29 and 30 are reserved for side-chains +CONSTRUCT_TYPED_SFIELD(sfSignatureReward, "SignatureReward", AMOUNT, 29); +CONSTRUCT_TYPED_SFIELD(sfMinAccountCreateAmount, "MinAccountCreateAmount", AMOUNT, 30); CONSTRUCT_TYPED_SFIELD(sfLPTokenBalance, "LPTokenBalance", AMOUNT, 31); // variable length (common) @@ -308,6 +313,12 @@ CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT, // account (uncommon) CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16); +CONSTRUCT_TYPED_SFIELD(sfOtherChainSource, "OtherChainSource", ACCOUNT, 18); +CONSTRUCT_TYPED_SFIELD(sfOtherChainDestination, "OtherChainDestination",ACCOUNT, 19); +CONSTRUCT_TYPED_SFIELD(sfAttestationSignerAccount, "AttestationSignerAccount", ACCOUNT, 20); +CONSTRUCT_TYPED_SFIELD(sfAttestationRewardAccount, "AttestationRewardAccount", ACCOUNT, 21); +CONSTRUCT_TYPED_SFIELD(sfLockingChainDoor, "LockingChainDoor", ACCOUNT, 22); +CONSTRUCT_TYPED_SFIELD(sfIssuingChainDoor, "IssuingChainDoor", ACCOUNT, 23); // vector of 256-bit CONSTRUCT_TYPED_SFIELD(sfIndexes, "Indexes", VECTOR256, 1, SField::sMD_Never); @@ -319,9 +330,14 @@ CONSTRUCT_TYPED_SFIELD(sfNFTokenOffers, "NFTokenOffers", VECTOR25 CONSTRUCT_UNTYPED_SFIELD(sfPaths, "Paths", PATHSET, 1); // issue +CONSTRUCT_TYPED_SFIELD(sfLockingChainIssue, "LockingChainIssue", ISSUE, 1); +CONSTRUCT_TYPED_SFIELD(sfIssuingChainIssue, "IssuingChainIssue", ISSUE, 2); CONSTRUCT_TYPED_SFIELD(sfAsset, "Asset", ISSUE, 3); CONSTRUCT_TYPED_SFIELD(sfAsset2, "Asset2", ISSUE, 4); +// Bridge +CONSTRUCT_TYPED_SFIELD(sfXChainBridge, "XChainBridge", XCHAIN_BRIDGE, + 1); // inner object // OBJECT/1 is reserved for end of object CONSTRUCT_UNTYPED_SFIELD(sfTransactionMetaData, "TransactionMetaData", OBJECT, 2); @@ -351,6 +367,16 @@ CONSTRUCT_UNTYPED_SFIELD(sfHookGrant, "HookGrant", OBJECT, CONSTRUCT_UNTYPED_SFIELD(sfVoteEntry, "VoteEntry", OBJECT, 25); CONSTRUCT_UNTYPED_SFIELD(sfAuctionSlot, "AuctionSlot", OBJECT, 26); CONSTRUCT_UNTYPED_SFIELD(sfAuthAccount, "AuthAccount", OBJECT, 27); +CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimProofSig, "XChainClaimProofSig", OBJECT, 28); +CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountProofSig, + "XChainCreateAccountProofSig", + OBJECT, 29); +CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, + "XChainClaimAttestationCollectionElement", + OBJECT, 30); +CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, + "XChainCreateAccountAttestationCollectionElement", + OBJECT, 31); // array of objects // ARRAY/1 is reserved for end of array @@ -372,7 +398,13 @@ CONSTRUCT_UNTYPED_SFIELD(sfDisabledValidators, "DisabledValidators", ARRAY, CONSTRUCT_UNTYPED_SFIELD(sfHookExecutions, "HookExecutions", ARRAY, 18); CONSTRUCT_UNTYPED_SFIELD(sfHookParameters, "HookParameters", ARRAY, 19); CONSTRUCT_UNTYPED_SFIELD(sfHookGrants, "HookGrants", ARRAY, 20); -// 21-24 is reserved for side-chains +CONSTRUCT_UNTYPED_SFIELD(sfXChainClaimAttestations, + "XChainClaimAttestations", + ARRAY, 21); +CONSTRUCT_UNTYPED_SFIELD(sfXChainCreateAccountAttestations, + "XChainCreateAccountAttestations", + ARRAY, 22); +// 23 and 24 are unused and available for use CONSTRUCT_UNTYPED_SFIELD(sfAuthAccounts, "AuthAccounts", ARRAY, 25); // clang-format on diff --git a/src/ripple/protocol/impl/STAmount.cpp b/src/ripple/protocol/impl/STAmount.cpp index 877a19813cf..201bcb6b681 100644 --- a/src/ripple/protocol/impl/STAmount.cpp +++ b/src/ripple/protocol/impl/STAmount.cpp @@ -239,6 +239,17 @@ STAmount::STAmount( canonicalize(); } +STAmount::STAmount(SField const& name, STAmount const& from) + : STBase(name) + , mIssue(from.mIssue) + , mValue(from.mValue) + , mOffset(from.mOffset) + , mIsNegative(from.mIsNegative) +{ + assert(mValue <= std::numeric_limits::max()); + canonicalize(); +} + //------------------------------------------------------------------------------ STAmount::STAmount(std::uint64_t mantissa, bool negative) diff --git a/src/ripple/protocol/impl/STArray.cpp b/src/ripple/protocol/impl/STArray.cpp index 7b1e5d9f249..7ee3da1ff1a 100644 --- a/src/ripple/protocol/impl/STArray.cpp +++ b/src/ripple/protocol/impl/STArray.cpp @@ -46,7 +46,7 @@ STArray::STArray(SField const& f) : STBase(f) { } -STArray::STArray(SField const& f, int n) : STBase(f) +STArray::STArray(SField const& f, std::size_t n) : STBase(f) { v_.reserve(n); } diff --git a/src/ripple/protocol/impl/STIssue.cpp b/src/ripple/protocol/impl/STIssue.cpp index ffe18f7c5b7..878a5b4c71b 100644 --- a/src/ripple/protocol/impl/STIssue.cpp +++ b/src/ripple/protocol/impl/STIssue.cpp @@ -19,7 +19,18 @@ #include +#include #include +#include +#include + +#include +#include +#include + +#include +#include +#include namespace ripple { diff --git a/src/ripple/protocol/impl/STParsedJSON.cpp b/src/ripple/protocol/impl/STParsedJSON.cpp index 6c38e377c8f..fb960e6f11e 100644 --- a/src/ripple/protocol/impl/STParsedJSON.cpp +++ b/src/ripple/protocol/impl/STParsedJSON.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -33,10 +34,13 @@ #include #include #include +#include #include #include #include +#include #include + #include #include #include @@ -742,6 +746,20 @@ parseLeaf( return ret; } break; + + case STI_XCHAIN_BRIDGE: + try + { + ret = detail::make_stvar( + STXChainBridge(field, value)); + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + break; + default: error = bad_type(json_name, fieldName); return ret; @@ -951,6 +969,7 @@ parseArray( if (ret->getFName().fieldType != STI_OBJECT) { + ss << "Field type: " << ret->getFName().fieldType << " "; error = non_object_in_array(ss.str(), i); return std::nullopt; } diff --git a/src/ripple/protocol/impl/STVar.cpp b/src/ripple/protocol/impl/STVar.cpp index ee0a6cb17a4..2ec55ccaf03 100644 --- a/src/ripple/protocol/impl/STVar.cpp +++ b/src/ripple/protocol/impl/STVar.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include + #include #include #include @@ -29,7 +31,8 @@ #include #include #include -#include +#include +#include namespace ripple { namespace detail { @@ -161,6 +164,9 @@ STVar::STVar(SerialIter& sit, SField const& name, int depth) case STI_ISSUE: construct(sit, name); return; + case STI_XCHAIN_BRIDGE: + construct(sit, name); + return; default: Throw("Unknown object type"); } @@ -219,6 +225,9 @@ STVar::STVar(SerializedTypeID id, SField const& name) case STI_ISSUE: construct(name); return; + case STI_XCHAIN_BRIDGE: + construct(name); + return; default: Throw("Unknown object type"); } diff --git a/src/ripple/protocol/impl/STVar.h b/src/ripple/protocol/impl/STVar.h index b156534f827..73863edbbe0 100644 --- a/src/ripple/protocol/impl/STVar.h +++ b/src/ripple/protocol/impl/STVar.h @@ -125,7 +125,7 @@ class STVar void construct(Args&&... args) { - if (sizeof(T) > max_size) + if constexpr (sizeof(T) > max_size) p_ = new T(std::forward(args)...); else p_ = new (&d_) T(std::forward(args)...); diff --git a/src/ripple/protocol/impl/STXChainBridge.cpp b/src/ripple/protocol/impl/STXChainBridge.cpp new file mode 100644 index 00000000000..8ff19ca7e3b --- /dev/null +++ b/src/ripple/protocol/impl/STXChainBridge.cpp @@ -0,0 +1,227 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +STXChainBridge::STXChainBridge() : STBase{sfXChainBridge} +{ +} + +STXChainBridge::STXChainBridge(SField const& name) : STBase{name} +{ +} + +STXChainBridge::STXChainBridge( + AccountID const& srcChainDoor, + Issue const& srcChainIssue, + AccountID const& dstChainDoor, + Issue const& dstChainIssue) + : STBase{sfXChainBridge} + , lockingChainDoor_{sfLockingChainDoor, srcChainDoor} + , lockingChainIssue_{sfLockingChainIssue, srcChainIssue} + , issuingChainDoor_{sfIssuingChainDoor, dstChainDoor} + , issuingChainIssue_{sfIssuingChainIssue, dstChainIssue} +{ +} + +STXChainBridge::STXChainBridge(STObject const& o) + : STBase{sfXChainBridge} + , lockingChainDoor_{sfLockingChainDoor, o[sfLockingChainDoor]} + , lockingChainIssue_{sfLockingChainIssue, o[sfLockingChainIssue]} + , issuingChainDoor_{sfIssuingChainDoor, o[sfIssuingChainDoor]} + , issuingChainIssue_{sfIssuingChainIssue, o[sfIssuingChainIssue]} +{ +} + +STXChainBridge::STXChainBridge(Json::Value const& v) + : STXChainBridge{sfXChainBridge, v} +{ +} + +STXChainBridge::STXChainBridge(SField const& name, Json::Value const& v) + : STBase{name} +{ + if (!v.isObject()) + { + Throw( + "STXChainBridge can only be specified with a 'object' Json value"); + } + + auto checkExtra = [](Json::Value const& v) { + static auto const jbridge = + ripple::STXChainBridge().getJson(ripple::JsonOptions::none); + for (auto it = v.begin(); it != v.end(); ++it) + { + std::string const name = it.memberName(); + if (!jbridge.isMember(name)) + { + Throw( + "STXChainBridge extra field detected: " + name); + } + } + return true; + }; + checkExtra(v); + + Json::Value const& lockingChainDoorStr = + v[sfLockingChainDoor.getJsonName()]; + Json::Value const& lockingChainIssue = v[sfLockingChainIssue.getJsonName()]; + Json::Value const& issuingChainDoorStr = + v[sfIssuingChainDoor.getJsonName()]; + Json::Value const& issuingChainIssue = v[sfIssuingChainIssue.getJsonName()]; + + if (!lockingChainDoorStr.isString()) + { + Throw( + "STXChainBridge LockingChainDoor must be a string Json value"); + } + if (!issuingChainDoorStr.isString()) + { + Throw( + "STXChainBridge IssuingChainDoor must be a string Json value"); + } + + auto const lockingChainDoor = + parseBase58(lockingChainDoorStr.asString()); + auto const issuingChainDoor = + parseBase58(issuingChainDoorStr.asString()); + if (!lockingChainDoor) + { + Throw( + "STXChainBridge LockingChainDoor must be a valid account"); + } + if (!issuingChainDoor) + { + Throw( + "STXChainBridge IssuingChainDoor must be a valid account"); + } + + lockingChainDoor_ = STAccount{sfLockingChainDoor, *lockingChainDoor}; + lockingChainIssue_ = + STIssue{sfLockingChainIssue, issueFromJson(lockingChainIssue)}; + issuingChainDoor_ = STAccount{sfIssuingChainDoor, *issuingChainDoor}; + issuingChainIssue_ = + STIssue{sfIssuingChainIssue, issueFromJson(issuingChainIssue)}; +} + +STXChainBridge::STXChainBridge(SerialIter& sit, SField const& name) + : STBase{name} + , lockingChainDoor_{sit, sfLockingChainDoor} + , lockingChainIssue_{sit, sfLockingChainIssue} + , issuingChainDoor_{sit, sfIssuingChainDoor} + , issuingChainIssue_{sit, sfIssuingChainIssue} +{ +} + +void +STXChainBridge::add(Serializer& s) const +{ + lockingChainDoor_.add(s); + lockingChainIssue_.add(s); + issuingChainDoor_.add(s); + issuingChainIssue_.add(s); +} + +Json::Value +STXChainBridge::getJson(JsonOptions jo) const +{ + Json::Value v; + v[sfLockingChainDoor.getJsonName()] = lockingChainDoor_.getJson(jo); + v[sfLockingChainIssue.getJsonName()] = lockingChainIssue_.getJson(jo); + v[sfIssuingChainDoor.getJsonName()] = issuingChainDoor_.getJson(jo); + v[sfIssuingChainIssue.getJsonName()] = issuingChainIssue_.getJson(jo); + return v; +} + +std::string +STXChainBridge::getText() const +{ + return str( + boost::format("{ %s = %s, %s = %s, %s = %s, %s = %s }") % + sfLockingChainDoor.getName() % lockingChainDoor_.getText() % + sfLockingChainIssue.getName() % lockingChainIssue_.getText() % + sfIssuingChainDoor.getName() % issuingChainDoor_.getText() % + sfIssuingChainIssue.getName() % issuingChainIssue_.getText()); +} + +STObject +STXChainBridge::toSTObject() const +{ + STObject o{sfXChainBridge}; + o[sfLockingChainDoor] = lockingChainDoor_; + o[sfLockingChainIssue] = lockingChainIssue_; + o[sfIssuingChainDoor] = issuingChainDoor_; + o[sfIssuingChainIssue] = issuingChainIssue_; + return o; +} + +SerializedTypeID +STXChainBridge::getSType() const +{ + return STI_XCHAIN_BRIDGE; +} + +bool +STXChainBridge::isEquivalent(const STBase& t) const +{ + const STXChainBridge* v = dynamic_cast(&t); + return v && (*v == *this); +} + +bool +STXChainBridge::isDefault() const +{ + return lockingChainDoor_.isDefault() && lockingChainIssue_.isDefault() && + issuingChainDoor_.isDefault() && issuingChainIssue_.isDefault(); +} + +std::unique_ptr +STXChainBridge::construct(SerialIter& sit, SField const& name) +{ + return std::make_unique(sit, name); +} + +STBase* +STXChainBridge::copy(std::size_t n, void* buf) const +{ + return emplace(n, buf, *this); +} + +STBase* +STXChainBridge::move(std::size_t n, void* buf) +{ + return emplace(n, buf, std::move(*this)); +} +} // namespace ripple diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 9da1bc70757..87dae362598 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -96,6 +96,23 @@ transResults() MAKE_ERROR(tecOBJECT_NOT_FOUND, "A requested object could not be located."), MAKE_ERROR(tecINSUFFICIENT_PAYMENT, "The payment is not sufficient."), MAKE_ERROR(tecINCOMPLETE, "Some work was completed, but more submissions required to finish."), + MAKE_ERROR(tecXCHAIN_BAD_TRANSFER_ISSUE, "Bad xchain transfer issue."), + MAKE_ERROR(tecXCHAIN_NO_CLAIM_ID, "No such xchain claim id."), + MAKE_ERROR(tecXCHAIN_BAD_CLAIM_ID, "Bad xchain claim id."), + MAKE_ERROR(tecXCHAIN_CLAIM_NO_QUORUM, "Quorum was not reached on the xchain claim."), + MAKE_ERROR(tecXCHAIN_PROOF_UNKNOWN_KEY, "Unknown key for the xchain proof."), + MAKE_ERROR(tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE, "Only XRP may be used for xchain create account."), + MAKE_ERROR(tecXCHAIN_WRONG_CHAIN, "XChain Transaction was submitted to the wrong chain."), + MAKE_ERROR(tecXCHAIN_REWARD_MISMATCH, "The reward amount must match the reward specified in the xchain bridge."), + MAKE_ERROR(tecXCHAIN_NO_SIGNERS_LIST, "The account did not have a signers list."), + MAKE_ERROR(tecXCHAIN_SENDING_ACCOUNT_MISMATCH,"The sending account did not match the expected sending account."), + MAKE_ERROR(tecXCHAIN_INSUFF_CREATE_AMOUNT, "Insufficient amount to create an account."), + MAKE_ERROR(tecXCHAIN_ACCOUNT_CREATE_PAST, "The account create count has already passed."), + MAKE_ERROR(tecXCHAIN_ACCOUNT_CREATE_TOO_MANY, "There are too many pending account create transactions to submit a new one."), + MAKE_ERROR(tecXCHAIN_PAYMENT_FAILED, "Failed to transfer funds in a xchain transaction."), + MAKE_ERROR(tecXCHAIN_SELF_COMMIT, "Account cannot commit funds to itself."), + MAKE_ERROR(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR, "Bad public key account pair in an xchain transaction."), + MAKE_ERROR(tecXCHAIN_CREATE_ACCOUNT_DISABLED, "This bridge does not support account creation."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -175,6 +192,12 @@ transResults() MAKE_ERROR(temINVALID_COUNT, "Malformed: Count field outside valid range."), MAKE_ERROR(temSEQ_AND_TICKET, "Transaction contains a TicketSequence and a non-zero Sequence."), MAKE_ERROR(temBAD_NFTOKEN_TRANSFER_FEE, "Malformed: The NFToken transfer fee must be between 1 and 5000, inclusive."), + MAKE_ERROR(temXCHAIN_EQUAL_DOOR_ACCOUNTS, "Malformed: Bridge must have unique door accounts."), + MAKE_ERROR(temXCHAIN_BAD_PROOF, "Malformed: Bad cross-chain claim proof."), + MAKE_ERROR(temXCHAIN_BRIDGE_BAD_ISSUES, "Malformed: Bad bridge issues."), + MAKE_ERROR(temXCHAIN_BRIDGE_NONDOOR_OWNER, "Malformed: Bridge owner must be one of the door accounts."), + MAKE_ERROR(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT, "Malformed: Bad min account create amount."), + MAKE_ERROR(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT, "Malformed: Bad reward amount."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 58e25f0b8f9..720f9deb399 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -18,6 +18,9 @@ //============================================================================== #include + +#include +#include #include namespace ripple { @@ -390,6 +393,101 @@ TxFormats::TxFormats() {sfAmount, soeREQUIRED}, }, commonFields); + + add(jss::XChainCreateBridge, + ttXCHAIN_CREATE_BRIDGE, + { + {sfXChainBridge, soeREQUIRED}, + {sfSignatureReward, soeREQUIRED}, + {sfMinAccountCreateAmount, soeOPTIONAL}, + }, + commonFields); + + add(jss::XChainModifyBridge, + ttXCHAIN_MODIFY_BRIDGE, + { + {sfXChainBridge, soeREQUIRED}, + {sfSignatureReward, soeOPTIONAL}, + {sfMinAccountCreateAmount, soeOPTIONAL}, + }, + commonFields); + + add(jss::XChainCreateClaimID, + ttXCHAIN_CREATE_CLAIM_ID, + { + {sfXChainBridge, soeREQUIRED}, + {sfSignatureReward, soeREQUIRED}, + {sfOtherChainSource, soeREQUIRED}, + }, + commonFields); + + add(jss::XChainCommit, + ttXCHAIN_COMMIT, + { + {sfXChainBridge, soeREQUIRED}, + {sfXChainClaimID, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfOtherChainDestination, soeOPTIONAL}, + }, + commonFields); + + add(jss::XChainClaim, + ttXCHAIN_CLAIM, + { + {sfXChainBridge, soeREQUIRED}, + {sfXChainClaimID, soeREQUIRED}, + {sfDestination, soeREQUIRED}, + {sfDestinationTag, soeOPTIONAL}, + {sfAmount, soeREQUIRED}, + }, + commonFields); + + add(jss::XChainAddClaimAttestation, + ttXCHAIN_ADD_CLAIM_ATTESTATION, + { + {sfXChainBridge, soeREQUIRED}, + + {sfAttestationSignerAccount, soeREQUIRED}, + {sfPublicKey, soeREQUIRED}, + {sfSignature, soeREQUIRED}, + {sfOtherChainSource, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfAttestationRewardAccount, soeREQUIRED}, + {sfWasLockingChainSend, soeREQUIRED}, + + {sfXChainClaimID, soeREQUIRED}, + {sfDestination, soeOPTIONAL}, + }, + commonFields); + + add(jss::XChainAddAccountCreateAttestation, + ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, + { + {sfXChainBridge, soeREQUIRED}, + + {sfAttestationSignerAccount, soeREQUIRED}, + {sfPublicKey, soeREQUIRED}, + {sfSignature, soeREQUIRED}, + {sfOtherChainSource, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfAttestationRewardAccount, soeREQUIRED}, + {sfWasLockingChainSend, soeREQUIRED}, + + {sfXChainAccountCreateCount, soeREQUIRED}, + {sfDestination, soeREQUIRED}, + {sfSignatureReward, soeREQUIRED}, + }, + commonFields); + + add(jss::XChainAccountCreateCommit, + ttXCHAIN_ACCOUNT_CREATE_COMMIT, + { + {sfXChainBridge, soeREQUIRED}, + {sfDestination, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfSignatureReward, soeREQUIRED}, + }, + commonFields); } TxFormats const& diff --git a/src/ripple/protocol/impl/XChainAttestations.cpp b/src/ripple/protocol/impl/XChainAttestations.cpp new file mode 100644 index 00000000000..591b20ad5a0 --- /dev/null +++ b/src/ripple/protocol/impl/XChainAttestations.cpp @@ -0,0 +1,759 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace Attestations { + +AttestationBase::AttestationBase( + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + Buffer signature_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_) + : attestationSignerAccount{attestationSignerAccount_} + , publicKey{publicKey_} + , signature{std::move(signature_)} + , sendingAccount{sendingAccount_} + , sendingAmount{sendingAmount_} + , rewardAccount{rewardAccount_} + , wasLockingChainSend{wasLockingChainSend_} +{ +} + +bool +AttestationBase::equalHelper( + AttestationBase const& lhs, + AttestationBase const& rhs) +{ + return std::tie( + lhs.attestationSignerAccount, + lhs.publicKey, + lhs.signature, + lhs.sendingAccount, + lhs.sendingAmount, + lhs.rewardAccount, + lhs.wasLockingChainSend) == + std::tie( + rhs.attestationSignerAccount, + rhs.publicKey, + rhs.signature, + rhs.sendingAccount, + rhs.sendingAmount, + rhs.rewardAccount, + rhs.wasLockingChainSend); +} + +bool +AttestationBase::sameEventHelper( + AttestationBase const& lhs, + AttestationBase const& rhs) +{ + return std::tie( + lhs.sendingAccount, + lhs.sendingAmount, + lhs.wasLockingChainSend) == + std::tie( + rhs.sendingAccount, rhs.sendingAmount, rhs.wasLockingChainSend); +} + +bool +AttestationBase::verify(STXChainBridge const& bridge) const +{ + std::vector msg = message(bridge); + return ripple::verify(publicKey, makeSlice(msg), signature); +} + +AttestationBase::AttestationBase(STObject const& o) + : attestationSignerAccount{o[sfAttestationSignerAccount]} + , publicKey{o[sfPublicKey]} + , signature{o[sfSignature]} + , sendingAccount{o[sfAccount]} + , sendingAmount{o[sfAmount]} + , rewardAccount{o[sfAttestationRewardAccount]} + , wasLockingChainSend{bool(o[sfWasLockingChainSend])} +{ +} + +AttestationBase::AttestationBase(Json::Value const& v) + : attestationSignerAccount{Json::getOrThrow( + v, + sfAttestationSignerAccount)} + , publicKey{Json::getOrThrow(v, sfPublicKey)} + , signature{Json::getOrThrow(v, sfSignature)} + , sendingAccount{Json::getOrThrow(v, sfAccount)} + , sendingAmount{Json::getOrThrow(v, sfAmount)} + , rewardAccount{Json::getOrThrow(v, sfAttestationRewardAccount)} + , wasLockingChainSend{Json::getOrThrow(v, sfWasLockingChainSend)} +{ +} + +void +AttestationBase::addHelper(STObject& o) const +{ + o[sfAttestationSignerAccount] = attestationSignerAccount; + o[sfPublicKey] = publicKey; + o[sfSignature] = signature; + o[sfAmount] = sendingAmount; + o[sfAccount] = sendingAccount; + o[sfAttestationRewardAccount] = rewardAccount; + o[sfWasLockingChainSend] = wasLockingChainSend; +} + +AttestationClaim::AttestationClaim( + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + Buffer signature_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::uint64_t claimID_, + std::optional const& dst_) + : AttestationBase( + attestationSignerAccount_, + publicKey_, + std::move(signature_), + sendingAccount_, + sendingAmount_, + rewardAccount_, + wasLockingChainSend_) + , claimID{claimID_} + , dst{dst_} +{ +} + +AttestationClaim::AttestationClaim( + STXChainBridge const& bridge, + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + SecretKey const& secretKey_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::uint64_t claimID_, + std::optional const& dst_) + : AttestationClaim{ + attestationSignerAccount_, + publicKey_, + Buffer{}, + sendingAccount_, + sendingAmount_, + rewardAccount_, + wasLockingChainSend_, + claimID_, + dst_} +{ + auto const toSign = message(bridge); + signature = sign(publicKey_, secretKey_, makeSlice(toSign)); +} + +AttestationClaim::AttestationClaim(STObject const& o) + : AttestationBase(o), claimID{o[sfXChainClaimID]}, dst{o[~sfDestination]} +{ +} + +AttestationClaim::AttestationClaim(Json::Value const& v) + : AttestationBase{v} + , claimID{Json::getOrThrow(v, sfXChainClaimID)} +{ + if (v.isMember(sfDestination.getJsonName())) + dst = Json::getOrThrow(v, sfDestination); +} + +STObject +AttestationClaim::toSTObject() const +{ + STObject o{sfXChainClaimAttestationCollectionElement}; + addHelper(o); + o[sfXChainClaimID] = claimID; + if (dst) + o[sfDestination] = *dst; + return o; +} + +std::vector +AttestationClaim::message( + STXChainBridge const& bridge, + AccountID const& sendingAccount, + STAmount const& sendingAmount, + AccountID const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t claimID, + std::optional const& dst) +{ + STObject o{sfGeneric}; + // Serialize in SField order to make python serializers easier to write + o[sfXChainClaimID] = claimID; + o[sfAmount] = sendingAmount; + if (dst) + o[sfDestination] = *dst; + o[sfOtherChainSource] = sendingAccount; + o[sfAttestationRewardAccount] = rewardAccount; + o[sfWasLockingChainSend] = wasLockingChainSend ? 1 : 0; + o[sfXChainBridge] = bridge; + + Serializer s; + o.add(s); + + return std::move(s.modData()); +} + +std::vector +AttestationClaim::message(STXChainBridge const& bridge) const +{ + return AttestationClaim::message( + bridge, + sendingAccount, + sendingAmount, + rewardAccount, + wasLockingChainSend, + claimID, + dst); +} + +bool +AttestationClaim::validAmounts() const +{ + return isLegalNet(sendingAmount); +} + +bool +AttestationClaim::sameEvent(AttestationClaim const& rhs) const +{ + return AttestationClaim::sameEventHelper(*this, rhs) && + tie(claimID, dst) == tie(rhs.claimID, rhs.dst); +} + +bool +operator==(AttestationClaim const& lhs, AttestationClaim const& rhs) +{ + return AttestationClaim::equalHelper(lhs, rhs) && + tie(lhs.claimID, lhs.dst) == tie(rhs.claimID, rhs.dst); +} + +AttestationCreateAccount::AttestationCreateAccount(STObject const& o) + : AttestationBase(o) + , createCount{o[sfXChainAccountCreateCount]} + , toCreate{o[sfDestination]} + , rewardAmount{o[sfSignatureReward]} +{ +} + +AttestationCreateAccount::AttestationCreateAccount(Json::Value const& v) + : AttestationBase{v} + , createCount{Json::getOrThrow( + v, + sfXChainAccountCreateCount)} + , toCreate{Json::getOrThrow(v, sfDestination)} + , rewardAmount{Json::getOrThrow(v, sfSignatureReward)} +{ +} + +AttestationCreateAccount::AttestationCreateAccount( + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + Buffer signature_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + STAmount const& rewardAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::uint64_t createCount_, + AccountID const& toCreate_) + : AttestationBase( + attestationSignerAccount_, + publicKey_, + std::move(signature_), + sendingAccount_, + sendingAmount_, + rewardAccount_, + wasLockingChainSend_) + , createCount{createCount_} + , toCreate{toCreate_} + , rewardAmount{rewardAmount_} +{ +} + +AttestationCreateAccount::AttestationCreateAccount( + STXChainBridge const& bridge, + AccountID attestationSignerAccount_, + PublicKey const& publicKey_, + SecretKey const& secretKey_, + AccountID const& sendingAccount_, + STAmount const& sendingAmount_, + STAmount const& rewardAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::uint64_t createCount_, + AccountID const& toCreate_) + : AttestationCreateAccount{ + attestationSignerAccount_, + publicKey_, + Buffer{}, + sendingAccount_, + sendingAmount_, + rewardAmount_, + rewardAccount_, + wasLockingChainSend_, + createCount_, + toCreate_} +{ + auto const toSign = message(bridge); + signature = sign(publicKey_, secretKey_, makeSlice(toSign)); +} + +STObject +AttestationCreateAccount::toSTObject() const +{ + STObject o{sfXChainCreateAccountAttestationCollectionElement}; + addHelper(o); + + o[sfXChainAccountCreateCount] = createCount; + o[sfDestination] = toCreate; + o[sfSignatureReward] = rewardAmount; + + return o; +} + +std::vector +AttestationCreateAccount::message( + STXChainBridge const& bridge, + AccountID const& sendingAccount, + STAmount const& sendingAmount, + STAmount const& rewardAmount, + AccountID const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t createCount, + AccountID const& dst) +{ + STObject o{sfGeneric}; + // Serialize in SField order to make python serializers easier to write + o[sfXChainAccountCreateCount] = createCount; + o[sfAmount] = sendingAmount; + o[sfSignatureReward] = rewardAmount; + o[sfDestination] = dst; + o[sfOtherChainSource] = sendingAccount; + o[sfAttestationRewardAccount] = rewardAccount; + o[sfWasLockingChainSend] = wasLockingChainSend ? 1 : 0; + o[sfXChainBridge] = bridge; + + Serializer s; + o.add(s); + + return std::move(s.modData()); +} + +std::vector +AttestationCreateAccount::message(STXChainBridge const& bridge) const +{ + return AttestationCreateAccount::message( + bridge, + sendingAccount, + sendingAmount, + rewardAmount, + rewardAccount, + wasLockingChainSend, + createCount, + toCreate); +} + +bool +AttestationCreateAccount::validAmounts() const +{ + return isLegalNet(rewardAmount) && isLegalNet(sendingAmount); +} + +bool +AttestationCreateAccount::sameEvent(AttestationCreateAccount const& rhs) const +{ + return AttestationCreateAccount::sameEventHelper(*this, rhs) && + std::tie(createCount, toCreate, rewardAmount) == + std::tie(rhs.createCount, rhs.toCreate, rhs.rewardAmount); +} + +bool +operator==( + AttestationCreateAccount const& lhs, + AttestationCreateAccount const& rhs) +{ + return AttestationCreateAccount::equalHelper(lhs, rhs) && + std::tie(lhs.createCount, lhs.toCreate, lhs.rewardAmount) == + std::tie(rhs.createCount, rhs.toCreate, rhs.rewardAmount); +} + +} // namespace Attestations + +SField const& XChainClaimAttestation::ArrayFieldName{sfXChainClaimAttestations}; +SField const& XChainCreateAccountAttestation::ArrayFieldName{ + sfXChainCreateAccountAttestations}; + +XChainClaimAttestation::XChainClaimAttestation( + AccountID const& keyAccount_, + PublicKey const& publicKey_, + STAmount const& amount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + std::optional const& dst_) + : keyAccount(keyAccount_) + , publicKey(publicKey_) + , amount(sfAmount, amount_) + , rewardAccount(rewardAccount_) + , wasLockingChainSend(wasLockingChainSend_) + , dst(dst_) +{ +} + +XChainClaimAttestation::XChainClaimAttestation( + STAccount const& keyAccount_, + PublicKey const& publicKey_, + STAmount const& amount_, + STAccount const& rewardAccount_, + bool wasLockingChainSend_, + std::optional const& dst_) + : XChainClaimAttestation{ + keyAccount_.value(), + publicKey_, + amount_, + rewardAccount_.value(), + wasLockingChainSend_, + dst_ ? std::optional{dst_->value()} : std::nullopt} +{ +} + +XChainClaimAttestation::XChainClaimAttestation(STObject const& o) + : XChainClaimAttestation{ + o[sfAttestationSignerAccount], + PublicKey{o[sfPublicKey]}, + o[sfAmount], + o[sfAttestationRewardAccount], + o[sfWasLockingChainSend] != 0, + o[~sfDestination]} {}; + +XChainClaimAttestation::XChainClaimAttestation(Json::Value const& v) + : XChainClaimAttestation{ + Json::getOrThrow(v, sfAttestationSignerAccount), + Json::getOrThrow(v, sfPublicKey), + Json::getOrThrow(v, sfAmount), + Json::getOrThrow(v, sfAttestationRewardAccount), + Json::getOrThrow(v, sfWasLockingChainSend), + std::nullopt} +{ + if (v.isMember(sfDestination.getJsonName())) + dst = Json::getOrThrow(v, sfDestination); +}; + +XChainClaimAttestation::XChainClaimAttestation( + XChainClaimAttestation::TSignedAttestation const& claimAtt) + : XChainClaimAttestation{ + claimAtt.attestationSignerAccount, + claimAtt.publicKey, + claimAtt.sendingAmount, + claimAtt.rewardAccount, + claimAtt.wasLockingChainSend, + claimAtt.dst} +{ +} + +STObject +XChainClaimAttestation::toSTObject() const +{ + STObject o{sfXChainClaimProofSig}; + o[sfAttestationSignerAccount] = + STAccount{sfAttestationSignerAccount, keyAccount}; + o[sfPublicKey] = publicKey; + o[sfAmount] = STAmount{sfAmount, amount}; + o[sfAttestationRewardAccount] = + STAccount{sfAttestationRewardAccount, rewardAccount}; + o[sfWasLockingChainSend] = wasLockingChainSend; + if (dst) + o[sfDestination] = STAccount{sfDestination, *dst}; + return o; +} + +bool +operator==(XChainClaimAttestation const& lhs, XChainClaimAttestation const& rhs) +{ + return std::tie( + lhs.keyAccount, + lhs.publicKey, + lhs.amount, + lhs.rewardAccount, + lhs.wasLockingChainSend, + lhs.dst) == + std::tie( + rhs.keyAccount, + rhs.publicKey, + rhs.amount, + rhs.rewardAccount, + rhs.wasLockingChainSend, + rhs.dst); +} + +XChainClaimAttestation::MatchFields::MatchFields( + XChainClaimAttestation::TSignedAttestation const& att) + : amount{att.sendingAmount} + , wasLockingChainSend{att.wasLockingChainSend} + , dst{att.dst} +{ +} + +AttestationMatch +XChainClaimAttestation::match( + XChainClaimAttestation::MatchFields const& rhs) const +{ + if (std::tie(amount, wasLockingChainSend) != + std::tie(rhs.amount, rhs.wasLockingChainSend)) + return AttestationMatch::nonDstMismatch; + if (dst != rhs.dst) + return AttestationMatch::matchExceptDst; + return AttestationMatch::match; +} + +//------------------------------------------------------------------------------ + +XChainCreateAccountAttestation::XChainCreateAccountAttestation( + AccountID const& keyAccount_, + PublicKey const& publicKey_, + STAmount const& amount_, + STAmount const& rewardAmount_, + AccountID const& rewardAccount_, + bool wasLockingChainSend_, + AccountID const& dst_) + : keyAccount(keyAccount_) + , publicKey(publicKey_) + , amount(sfAmount, amount_) + , rewardAmount(sfSignatureReward, rewardAmount_) + , rewardAccount(rewardAccount_) + , wasLockingChainSend(wasLockingChainSend_) + , dst(dst_) +{ +} + +XChainCreateAccountAttestation::XChainCreateAccountAttestation( + STObject const& o) + : XChainCreateAccountAttestation{ + o[sfAttestationSignerAccount], + PublicKey{o[sfPublicKey]}, + o[sfAmount], + o[sfSignatureReward], + o[sfAttestationRewardAccount], + o[sfWasLockingChainSend] != 0, + o[sfDestination]} {}; + +XChainCreateAccountAttestation ::XChainCreateAccountAttestation( + Json::Value const& v) + : XChainCreateAccountAttestation{ + Json::getOrThrow(v, sfAttestationSignerAccount), + Json::getOrThrow(v, sfPublicKey), + Json::getOrThrow(v, sfAmount), + Json::getOrThrow(v, sfSignatureReward), + Json::getOrThrow(v, sfAttestationRewardAccount), + Json::getOrThrow(v, sfWasLockingChainSend), + Json::getOrThrow(v, sfDestination)} +{ +} + +XChainCreateAccountAttestation::XChainCreateAccountAttestation( + XChainCreateAccountAttestation::TSignedAttestation const& createAtt) + : XChainCreateAccountAttestation{ + createAtt.attestationSignerAccount, + createAtt.publicKey, + createAtt.sendingAmount, + createAtt.rewardAmount, + createAtt.rewardAccount, + createAtt.wasLockingChainSend, + createAtt.toCreate} +{ +} + +STObject +XChainCreateAccountAttestation::toSTObject() const +{ + STObject o{sfXChainCreateAccountProofSig}; + + o[sfAttestationSignerAccount] = + STAccount{sfAttestationSignerAccount, keyAccount}; + o[sfPublicKey] = publicKey; + o[sfAmount] = STAmount{sfAmount, amount}; + o[sfSignatureReward] = STAmount{sfSignatureReward, rewardAmount}; + o[sfAttestationRewardAccount] = + STAccount{sfAttestationRewardAccount, rewardAccount}; + o[sfWasLockingChainSend] = wasLockingChainSend; + o[sfDestination] = STAccount{sfDestination, dst}; + + return o; +} + +XChainCreateAccountAttestation::MatchFields::MatchFields( + XChainCreateAccountAttestation::TSignedAttestation const& att) + : amount{att.sendingAmount} + , rewardAmount(att.rewardAmount) + , wasLockingChainSend{att.wasLockingChainSend} + , dst{att.toCreate} +{ +} + +AttestationMatch +XChainCreateAccountAttestation::match( + XChainCreateAccountAttestation::MatchFields const& rhs) const +{ + if (std::tie(amount, rewardAmount, wasLockingChainSend) != + std::tie(rhs.amount, rhs.rewardAmount, rhs.wasLockingChainSend)) + return AttestationMatch::nonDstMismatch; + if (dst != rhs.dst) + return AttestationMatch::matchExceptDst; + return AttestationMatch::match; +} + +bool +operator==( + XChainCreateAccountAttestation const& lhs, + XChainCreateAccountAttestation const& rhs) +{ + return std::tie( + lhs.keyAccount, + lhs.publicKey, + lhs.amount, + lhs.rewardAmount, + lhs.rewardAccount, + lhs.wasLockingChainSend, + lhs.dst) == + std::tie( + rhs.keyAccount, + rhs.publicKey, + rhs.amount, + rhs.rewardAmount, + rhs.rewardAccount, + rhs.wasLockingChainSend, + rhs.dst); +} + +//------------------------------------------------------------------------------ +// +template +XChainAttestationsBase::XChainAttestationsBase( + XChainAttestationsBase::AttCollection&& atts) + : attestations_{std::move(atts)} +{ +} + +template +typename XChainAttestationsBase::AttCollection::const_iterator +XChainAttestationsBase::begin() const +{ + return attestations_.begin(); +} + +template +typename XChainAttestationsBase::AttCollection::const_iterator +XChainAttestationsBase::end() const +{ + return attestations_.end(); +} + +template +typename XChainAttestationsBase::AttCollection::iterator +XChainAttestationsBase::begin() +{ + return attestations_.begin(); +} + +template +typename XChainAttestationsBase::AttCollection::iterator +XChainAttestationsBase::end() +{ + return attestations_.end(); +} + +template +XChainAttestationsBase::XChainAttestationsBase( + Json::Value const& v) +{ + if (!v.isObject()) + { + Throw( + "XChainAttestationsBase can only be specified with an 'object' " + "Json value"); + } + + attestations_ = [&] { + auto const jAtts = v[jss::attestations]; + + if (jAtts.size() > maxAttestations) + Throw( + "XChainAttestationsBase exceeded max number of attestations"); + + std::vector r; + r.reserve(jAtts.size()); + for (auto const& a : jAtts) + r.emplace_back(a); + return r; + }(); +} + +template +XChainAttestationsBase::XChainAttestationsBase(STArray const& arr) +{ + if (arr.size() > maxAttestations) + Throw( + "XChainAttestationsBase exceeded max number of attestations"); + + attestations_.reserve(arr.size()); + for (auto const& o : arr) + attestations_.emplace_back(o); +} + +template +STArray +XChainAttestationsBase::toSTArray() const +{ + STArray r{TAttestation::ArrayFieldName, attestations_.size()}; + for (auto const& e : attestations_) + r.emplace_back(e.toSTObject()); + return r; +} + +template class XChainAttestationsBase; +template class XChainAttestationsBase; + +} // namespace ripple diff --git a/src/ripple/protocol/json_get_or_throw.h b/src/ripple/protocol/json_get_or_throw.h new file mode 100644 index 00000000000..86bd5924d3e --- /dev/null +++ b/src/ripple/protocol/json_get_or_throw.h @@ -0,0 +1,159 @@ +#ifndef PROTOCOL_GET_OR_THROW_H_ +#define PROTOCOL_GET_OR_THROW_H_ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace Json { +struct JsonMissingKeyError : std::exception +{ + char const* const key; + mutable std::string msg; + JsonMissingKeyError(Json::StaticString const& k) : key{k.c_str()} + { + } + const char* + what() const noexcept override + { + if (msg.empty()) + { + msg = std::string("Missing json key: ") + key; + } + return msg.c_str(); + } +}; + +struct JsonTypeMismatchError : std::exception +{ + char const* const key; + std::string const expectedType; + mutable std::string msg; + JsonTypeMismatchError(Json::StaticString const& k, std::string et) + : key{k.c_str()}, expectedType{std::move(et)} + { + } + const char* + what() const noexcept override + { + if (msg.empty()) + { + msg = std::string("Type mismatch on json key: ") + key + + "; expected type: " + expectedType; + } + return msg.c_str(); + } +}; + +template +T +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + static_assert(sizeof(T) == -1, "This function must be specialized"); +} + +template <> +inline std::string +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + using namespace ripple; + Json::StaticString const& key = field.getJsonName(); + if (!v.isMember(key)) + Throw(key); + + Json::Value const& inner = v[key]; + if (!inner.isString()) + Throw(key, "string"); + return inner.asString(); +} + +// Note, this allows integer numeric fields to act as bools +template <> +inline bool +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + using namespace ripple; + Json::StaticString const& key = field.getJsonName(); + if (!v.isMember(key)) + Throw(key); + Json::Value const& inner = v[key]; + if (inner.isBool()) + return inner.asBool(); + if (!inner.isIntegral()) + Throw(key, "bool"); + + return inner.asInt() != 0; +} + +template <> +inline std::uint64_t +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + using namespace ripple; + Json::StaticString const& key = field.getJsonName(); + if (!v.isMember(key)) + Throw(key); + Json::Value const& inner = v[key]; + if (inner.isUInt()) + return inner.asUInt(); + if (inner.isInt()) + { + auto const r = inner.asInt(); + if (r < 0) + Throw(key, "uint64"); + return r; + } + if (inner.isString()) + { + auto const s = inner.asString(); + // parse as hex + std::uint64_t val; + + auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), val, 16); + + if (ec != std::errc() || (p != s.data() + s.size())) + Throw(key, "uint64"); + return val; + } + Throw(key, "uint64"); +} + +template <> +inline ripple::Buffer +getOrThrow(Json::Value const& v, ripple::SField const& field) +{ + using namespace ripple; + std::string const hex = getOrThrow(v, field); + if (auto const r = strUnHex(hex)) + { + // TODO: mismatch between a buffer and a blob + return Buffer{r->data(), r->size()}; + } + Throw(field.getJsonName(), "Buffer"); +} + +// This function may be used by external projects (like the witness server). +template +std::optional +getOptional(Json::Value const& v, ripple::SField const& field) +{ + try + { + return getOrThrow(v, field); + } + catch (...) + { + } + return {}; +} + +} // namespace Json + +#endif // PROTOCOL_GET_OR_THROW_H_ diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index eaf0ffa74c3..9a3e315dd8e 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -62,6 +62,7 @@ JSS(Asset); // in: AMM Asset1 JSS(Asset2); // in: AMM Asset2 JSS(AuthAccount); // in: AMM Auction Slot JSS(AuthAccounts); // in: AMM Auction Slot +JSS(Bridge); // ledger type. JSS(Check); // ledger type. JSS(CheckCancel); // transaction type. JSS(CheckCash); // transaction type. @@ -107,31 +108,41 @@ JSS(Paths); // in/out: TransactionSign JSS(PayChannel); // ledger type. JSS(Payment); // transaction type. JSS(PaymentChannelClaim); // transaction type. -JSS(PaymentChannelCreate); // transaction type. -JSS(PaymentChannelFund); // transaction type. -JSS(RippleState); // ledger type. -JSS(SLE_hit_rate); // out: GetCounts. -JSS(SetFee); // transaction type. -JSS(UNLModify); // transaction type. -JSS(SettleDelay); // in: TransactionSign -JSS(SendMax); // in: TransactionSign -JSS(Sequence); // in/out: TransactionSign; field. -JSS(SetFlag); // field. -JSS(SetRegularKey); // transaction type. -JSS(SignerList); // ledger type. -JSS(SignerListSet); // transaction type. -JSS(SigningPubKey); // field. -JSS(TakerGets); // field. -JSS(TakerPays); // field. -JSS(Ticket); // ledger type. -JSS(TicketCreate); // transaction type. -JSS(TxnSignature); // field. -JSS(TradingFee); // in/out: AMM trading fee -JSS(TransactionType); // in: TransactionSign. -JSS(TransferRate); // in: TransferRate. -JSS(TrustSet); // transaction type. -JSS(VoteSlots); // out: AMM Vote -JSS(aborted); // out: InboundLedger +JSS(PaymentChannelCreate); // transaction type. +JSS(PaymentChannelFund); // transaction type. +JSS(RippleState); // ledger type. +JSS(SLE_hit_rate); // out: GetCounts. +JSS(SetFee); // transaction type. +JSS(UNLModify); // transaction type. +JSS(SettleDelay); // in: TransactionSign +JSS(SendMax); // in: TransactionSign +JSS(Sequence); // in/out: TransactionSign; field. +JSS(SetFlag); // field. +JSS(SetRegularKey); // transaction type. +JSS(SignerList); // ledger type. +JSS(SignerListSet); // transaction type. +JSS(SigningPubKey); // field. +JSS(TakerGets); // field. +JSS(TakerPays); // field. +JSS(Ticket); // ledger type. +JSS(TicketCreate); // transaction type. +JSS(TxnSignature); // field. +JSS(TradingFee); // in/out: AMM trading fee +JSS(TransactionType); // in: TransactionSign. +JSS(TransferRate); // in: TransferRate. +JSS(TrustSet); // transaction type. +JSS(VoteSlots); // out: AMM Vote +JSS(XChainAddAccountCreateAttestation); // transaction type. +JSS(XChainAddClaimAttestation); // transaction type. +JSS(XChainAccountCreateCommit); // transaction type. +JSS(XChainClaim); // transaction type. +JSS(XChainCommit); // transaction type. +JSS(XChainCreateBridge); // transaction type. +JSS(XChainCreateClaimID); // transaction type. +JSS(XChainModifyBridge); // transaction type. +JSS(XChainOwnedClaimID); // ledger type. +JSS(XChainOwnedCreateAccountClaimID); // ledger type. +JSS(aborted); // out: InboundLedger JSS(accepted); // out: LedgerToJson, OwnerInfo, SubmitTransaction JSS(account); // in/out: many JSS(accountState); // out: LedgerToJson @@ -147,96 +158,102 @@ JSS(account_sequence_next); // out: SubmitTransaction JSS(account_sequence_available); // out: SubmitTransaction JSS(account_history_tx_stream); // in: Subscribe, Unsubscribe JSS(account_history_tx_index); // out: Account txn history subscribe -JSS(account_history_tx_first); // out: Account txn history subscribe -JSS(accounts); // in: LedgerEntry, Subscribe, - // handlers/Ledger, Unsubscribe -JSS(accounts_proposed); // in: Subscribe, Unsubscribe + +JSS(account_history_tx_first); // out: Account txn history subscribe +JSS(account_history_boundary); // out: Account txn history subscribe +JSS(accounts); // in: LedgerEntry, Subscribe, + // handlers/Ledger, Unsubscribe +JSS(accounts_proposed); // in: Subscribe, Unsubscribe JSS(action); -JSS(acquiring); // out: LedgerRequest -JSS(address); // out: PeerImp -JSS(affected); // out: AcceptedLedgerTx -JSS(age); // out: NetworkOPs, Peers -JSS(alternatives); // out: PathRequest, RipplePathFind -JSS(amendment_blocked); // out: NetworkOPs -JSS(amendments); // in: AccountObjects, out: NetworkOPs -JSS(amm); // out: amm_info -JSS(amm_account); // in: amm_info -JSS(amount); // out: AccountChannels, amm_info -JSS(amount2); // out: amm_info -JSS(api_version); // in: many, out: Version -JSS(api_version_low); // out: Version -JSS(applied); // out: SubmitTransaction -JSS(asks); // out: Subscribe -JSS(asset); // in: amm_info -JSS(asset2); // in: amm_info -JSS(assets); // out: GatewayBalances -JSS(asset_frozen); // out: amm_info -JSS(asset2_frozen); // out: amm_info -JSS(auction_slot); // out: amm_info -JSS(authorized); // out: AccountLines -JSS(auth_accounts); // out: amm_info -JSS(auth_change); // out: AccountInfo -JSS(auth_change_queued); // out: AccountInfo -JSS(available); // out: ValidatorList -JSS(avg_bps_recv); // out: Peers -JSS(avg_bps_sent); // out: Peers -JSS(balance); // out: AccountLines -JSS(balances); // out: GatewayBalances -JSS(base); // out: LogLevel -JSS(base_fee); // out: NetworkOPs -JSS(base_fee_xrp); // out: NetworkOPs -JSS(bids); // out: Subscribe -JSS(binary); // in: AccountTX, LedgerEntry, - // AccountTxOld, Tx LedgerData -JSS(blob); // out: ValidatorList -JSS(blobs_v2); // out: ValidatorList - // in: UNL -JSS(books); // in: Subscribe, Unsubscribe -JSS(both); // in: Subscribe, Unsubscribe -JSS(both_sides); // in: Subscribe, Unsubscribe -JSS(broadcast); // out: SubmitTransaction -JSS(build_path); // in: TransactionSign -JSS(build_version); // out: NetworkOPs -JSS(cancel_after); // out: AccountChannels -JSS(can_delete); // out: CanDelete -JSS(changes); // out: BookChanges -JSS(channel_id); // out: AccountChannels -JSS(channels); // out: AccountChannels -JSS(check); // in: AccountObjects -JSS(check_nodes); // in: LedgerCleaner -JSS(clear); // in/out: FetchInfo -JSS(close); // out: BookChanges -JSS(close_flags); // out: LedgerToJson -JSS(close_time); // in: Application, out: NetworkOPs, - // RCLCxPeerPos, LedgerToJson -JSS(close_time_estimated); // in: Application, out: LedgerToJson -JSS(close_time_human); // out: LedgerToJson -JSS(close_time_offset); // out: NetworkOPs -JSS(close_time_resolution); // in: Application; out: LedgerToJson -JSS(closed); // out: NetworkOPs, LedgerToJson, - // handlers/Ledger -JSS(closed_ledger); // out: NetworkOPs -JSS(cluster); // out: PeerImp -JSS(code); // out: errors -JSS(command); // in: RPCHandler -JSS(complete); // out: NetworkOPs, InboundLedger -JSS(complete_ledgers); // out: NetworkOPs, PeerImp -JSS(complete_shards); // out: OverlayImpl, PeerImp -JSS(consensus); // out: NetworkOPs, LedgerConsensus -JSS(converge_time); // out: NetworkOPs -JSS(converge_time_s); // out: NetworkOPs -JSS(cookie); // out: NetworkOPs -JSS(count); // in: AccountTx*, ValidatorList -JSS(counters); // in/out: retrieve counters -JSS(ctid); // in/out: Tx RPC -JSS(currency_a); // out: BookChanges -JSS(currency_b); // out: BookChanges -JSS(currentShard); // out: NodeToShardStatus -JSS(currentShardIndex); // out: NodeToShardStatus -JSS(currency); // in: paths/PathRequest, STAmount - // out: STPathSet, STAmount, - // AccountLines -JSS(current); // out: OwnerInfo +JSS(acquiring); // out: LedgerRequest +JSS(address); // out: PeerImp +JSS(affected); // out: AcceptedLedgerTx +JSS(age); // out: NetworkOPs, Peers +JSS(alternatives); // out: PathRequest, RipplePathFind +JSS(amendment_blocked); // out: NetworkOPs +JSS(amendments); // in: AccountObjects, out: NetworkOPs +JSS(amm); // out: amm_info +JSS(amm_account); // in: amm_info +JSS(amount); // out: AccountChannels, amm_info +JSS(amount2); // out: amm_info +JSS(api_version); // in: many, out: Version +JSS(api_version_low); // out: Version +JSS(applied); // out: SubmitTransaction +JSS(asks); // out: Subscribe +JSS(asset); // in: amm_info +JSS(asset2); // in: amm_info +JSS(assets); // out: GatewayBalances +JSS(asset_frozen); // out: amm_info +JSS(asset2_frozen); // out: amm_info +JSS(attestations); // +JSS(attestation_reward_account); // +JSS(auction_slot); // out: amm_info +JSS(authorized); // out: AccountLines +JSS(auth_accounts); // out: amm_info +JSS(auth_change); // out: AccountInfo +JSS(auth_change_queued); // out: AccountInfo +JSS(available); // out: ValidatorList +JSS(avg_bps_recv); // out: Peers +JSS(avg_bps_sent); // out: Peers +JSS(balance); // out: AccountLines +JSS(balances); // out: GatewayBalances +JSS(base); // out: LogLevel +JSS(base_fee); // out: NetworkOPs +JSS(base_fee_xrp); // out: NetworkOPs +JSS(bids); // out: Subscribe +JSS(binary); // in: AccountTX, LedgerEntry, + // AccountTxOld, Tx LedgerData +JSS(blob); // out: ValidatorList +JSS(blobs_v2); // out: ValidatorList + // in: UNL +JSS(books); // in: Subscribe, Unsubscribe +JSS(both); // in: Subscribe, Unsubscribe +JSS(both_sides); // in: Subscribe, Unsubscribe +JSS(broadcast); // out: SubmitTransaction +JSS(bridge); // in: LedgerEntry +JSS(bridge_account); // in: LedgerEntry +JSS(build_path); // in: TransactionSign +JSS(build_version); // out: NetworkOPs +JSS(cancel_after); // out: AccountChannels +JSS(can_delete); // out: CanDelete +JSS(changes); // out: BookChanges +JSS(channel_id); // out: AccountChannels +JSS(channels); // out: AccountChannels +JSS(check); // in: AccountObjects +JSS(check_nodes); // in: LedgerCleaner +JSS(clear); // in/out: FetchInfo +JSS(close); // out: BookChanges +JSS(close_flags); // out: LedgerToJson +JSS(close_time); // in: Application, out: NetworkOPs, + // RCLCxPeerPos, LedgerToJson +JSS(close_time_estimated); // in: Application, out: LedgerToJson +JSS(close_time_human); // out: LedgerToJson +JSS(close_time_offset); // out: NetworkOPs +JSS(close_time_resolution); // in: Application; out: LedgerToJson +JSS(closed); // out: NetworkOPs, LedgerToJson, + // handlers/Ledger +JSS(closed_ledger); // out: NetworkOPs +JSS(cluster); // out: PeerImp +JSS(code); // out: errors +JSS(command); // in: RPCHandler +JSS(complete); // out: NetworkOPs, InboundLedger +JSS(complete_ledgers); // out: NetworkOPs, PeerImp +JSS(complete_shards); // out: OverlayImpl, PeerImp +JSS(consensus); // out: NetworkOPs, LedgerConsensus +JSS(converge_time); // out: NetworkOPs +JSS(converge_time_s); // out: NetworkOPs +JSS(cookie); // out: NetworkOPs +JSS(count); // in: AccountTx*, ValidatorList +JSS(counters); // in/out: retrieve counters +JSS(ctid); // in/out: Tx RPC +JSS(currency_a); // out: BookChanges +JSS(currency_b); // out: BookChanges +JSS(currentShard); // out: NodeToShardStatus +JSS(currentShardIndex); // out: NodeToShardStatus +JSS(currency); // in: paths/PathRequest, STAmount + // out: STPathSet, STAmount, + // AccountLines +JSS(current); // out: OwnerInfo JSS(current_activities); JSS(current_ledger_size); // out: TxQ JSS(current_queue_size); // out: TxQ @@ -698,8 +715,10 @@ JSS(vote_weight); // out: amm_info JSS(warning); // rpc: JSS(warnings); // out: server_info, server_state JSS(workers); -JSS(write_load); // out: GetCounts -JSS(NegativeUNL); // out: ValidatorList; ledger type +JSS(write_load); // out: GetCounts +JSS(xchain_owned_claim_id); // in: LedgerEntry, AccountObjects +JSS(xchain_owned_create_account_claim_id); // in: LedgerEntry +JSS(NegativeUNL); // out: ValidatorList; ledger type #undef JSS } // namespace jss diff --git a/src/ripple/rpc/handlers/AccountObjects.cpp b/src/ripple/rpc/handlers/AccountObjects.cpp index 65cd12f2d41..bbf5b6e126a 100644 --- a/src/ripple/rpc/handlers/AccountObjects.cpp +++ b/src/ripple/rpc/handlers/AccountObjects.cpp @@ -197,7 +197,11 @@ doAccountObjects(RPC::JsonContext& context) {jss::escrow, ltESCROW}, {jss::nft_page, ltNFTOKEN_PAGE}, {jss::payment_channel, ltPAYCHAN}, - {jss::state, ltRIPPLE_STATE}}; + {jss::state, ltRIPPLE_STATE}, + {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, + {jss::xchain_owned_create_account_claim_id, + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, + {jss::bridge, ltBRIDGE}}; typeFilter.emplace(); typeFilter->reserve(std::size(deletionBlockers)); diff --git a/src/ripple/rpc/handlers/LedgerEntry.cpp b/src/ripple/rpc/handlers/LedgerEntry.cpp index 44bf1c1ab45..7f40d3ee3be 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -390,6 +391,203 @@ doLedgerEntry(RPC::JsonContext& context) } } } + else if (context.params.isMember(jss::bridge)) + { + expectedType = ltBRIDGE; + + // return the keylet for the specified bridge or nullopt if the + // request is malformed + auto const maybeKeylet = [&]() -> std::optional { + try + { + if (!context.params.isMember(jss::bridge_account)) + return std::nullopt; + + auto const& jsBridgeAccount = + context.params[jss::bridge_account]; + if (!jsBridgeAccount.isString()) + { + return std::nullopt; + } + auto const account = + parseBase58(jsBridgeAccount.asString()); + if (!account || account->isZero()) + { + return std::nullopt; + } + + // This may throw and is the reason for the `try` block. The + // try block has a larger scope so the `bridge` variable + // doesn't need to be an optional. + STXChainBridge const bridge(context.params[jss::bridge]); + STXChainBridge::ChainType const chainType = + STXChainBridge::srcChain( + account == bridge.lockingChainDoor()); + if (account != bridge.door(chainType)) + return std::nullopt; + + return keylet::bridge(bridge, chainType); + } + catch (...) + { + return std::nullopt; + } + }(); + + if (maybeKeylet) + { + uNodeIndex = maybeKeylet->key; + } + else + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + } + else if (context.params.isMember(jss::xchain_owned_claim_id)) + { + expectedType = ltXCHAIN_OWNED_CLAIM_ID; + auto& claim_id = context.params[jss::xchain_owned_claim_id]; + if (claim_id.isString()) + { + // we accept a node id as specifier of a xchain claim id + if (!uNodeIndex.parseHex(claim_id.asString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + } + else if ( + !claim_id.isObject() || + !(claim_id.isMember(sfIssuingChainDoor.getJsonName()) && + claim_id[sfIssuingChainDoor.getJsonName()].isString()) || + !(claim_id.isMember(sfLockingChainDoor.getJsonName()) && + claim_id[sfLockingChainDoor.getJsonName()].isString()) || + !claim_id.isMember(sfIssuingChainIssue.getJsonName()) || + !claim_id.isMember(sfLockingChainIssue.getJsonName()) || + !claim_id.isMember(jss::xchain_owned_claim_id)) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + // if not specified with a node id, a claim_id is specified by + // four strings defining the bridge (locking_chain_door, + // locking_chain_issue, issuing_chain_door, issuing_chain_issue) + // and the claim id sequence number. + auto lockingChainDoor = parseBase58( + claim_id[sfLockingChainDoor.getJsonName()].asString()); + auto issuingChainDoor = parseBase58( + claim_id[sfIssuingChainDoor.getJsonName()].asString()); + Issue lockingChainIssue, issuingChainIssue; + bool valid = lockingChainDoor && issuingChainDoor; + if (valid) + { + try + { + lockingChainIssue = issueFromJson( + claim_id[sfLockingChainIssue.getJsonName()]); + issuingChainIssue = issueFromJson( + claim_id[sfIssuingChainIssue.getJsonName()]); + } + catch (std::runtime_error const& ex) + { + valid = false; + jvResult[jss::error] = "malformedRequest"; + } + } + + if (valid && claim_id[jss::xchain_owned_claim_id].isIntegral()) + { + auto seq = claim_id[jss::xchain_owned_claim_id].asUInt(); + + STXChainBridge bridge_spec( + *lockingChainDoor, + lockingChainIssue, + *issuingChainDoor, + issuingChainIssue); + Keylet keylet = keylet::xChainClaimID(bridge_spec, seq); + uNodeIndex = keylet.key; + } + } + } + else if (context.params.isMember( + jss::xchain_owned_create_account_claim_id)) + { + // see object definition in LedgerFormats.cpp + expectedType = ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID; + auto& claim_id = + context.params[jss::xchain_owned_create_account_claim_id]; + if (claim_id.isString()) + { + // we accept a node id as specifier of a xchain create account + // claim_id + if (!uNodeIndex.parseHex(claim_id.asString())) + { + uNodeIndex = beast::zero; + jvResult[jss::error] = "malformedRequest"; + } + } + else if ( + !claim_id.isObject() || + !(claim_id.isMember(sfIssuingChainDoor.getJsonName()) && + claim_id[sfIssuingChainDoor.getJsonName()].isString()) || + !(claim_id.isMember(sfLockingChainDoor.getJsonName()) && + claim_id[sfLockingChainDoor.getJsonName()].isString()) || + !claim_id.isMember(sfIssuingChainIssue.getJsonName()) || + !claim_id.isMember(sfLockingChainIssue.getJsonName()) || + !claim_id.isMember(jss::xchain_owned_create_account_claim_id)) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + // if not specified with a node id, a create account claim_id is + // specified by four strings defining the bridge + // (locking_chain_door, locking_chain_issue, issuing_chain_door, + // issuing_chain_issue) and the create account claim id sequence + // number. + auto lockingChainDoor = parseBase58( + claim_id[sfLockingChainDoor.getJsonName()].asString()); + auto issuingChainDoor = parseBase58( + claim_id[sfIssuingChainDoor.getJsonName()].asString()); + Issue lockingChainIssue, issuingChainIssue; + bool valid = lockingChainDoor && issuingChainDoor; + if (valid) + { + try + { + lockingChainIssue = issueFromJson( + claim_id[sfLockingChainIssue.getJsonName()]); + issuingChainIssue = issueFromJson( + claim_id[sfIssuingChainIssue.getJsonName()]); + } + catch (std::runtime_error const& ex) + { + valid = false; + jvResult[jss::error] = "malformedRequest"; + } + } + + if (valid && + claim_id[jss::xchain_owned_create_account_claim_id] + .isIntegral()) + { + auto seq = + claim_id[jss::xchain_owned_create_account_claim_id] + .asUInt(); + + STXChainBridge bridge_spec( + *lockingChainDoor, + lockingChainIssue, + *issuingChainDoor, + issuingChainIssue); + Keylet keylet = + keylet::xChainCreateAccountClaimID(bridge_spec, seq); + uNodeIndex = keylet.key; + } + } + } else { if (context.params.isMember("params") && diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index 4a517733637..898755bd5ab 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -982,7 +982,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 16> + static constexpr std::array, 19> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -999,7 +999,11 @@ chooseLedgerEntryType(Json::Value const& params) {jss::ticket, ltTICKET}, {jss::nft_offer, ltNFTOKEN_OFFER}, {jss::nft_page, ltNFTOKEN_PAGE}, - {jss::amm, ltAMM}}}; + {jss::amm, ltAMM}, + {jss::bridge, ltBRIDGE}, + {jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID}, + {jss::xchain_owned_create_account_claim_id, + ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}}}; auto const& p = params[jss::type]; if (!p.isString()) @@ -1103,11 +1107,13 @@ getLedgerByContext(RPC::JsonContext& context) return RPC::make_param_error("Ledger index too small"); auto const j = context.app.journal("RPCHandler"); - // Try to get the hash of the desired ledger from the validated ledger + // Try to get the hash of the desired ledger from the validated + // ledger auto neededHash = hashOfSeq(*ledger, ledgerIndex, j); if (!neededHash) { - // Find a ledger more likely to have the hash of the desired ledger + // Find a ledger more likely to have the hash of the desired + // ledger auto const refIndex = getCandidateLedger(ledgerIndex); auto refHash = hashOfSeq(*ledger, refIndex, j); assert(refHash); @@ -1115,8 +1121,8 @@ getLedgerByContext(RPC::JsonContext& context) ledger = ledgerMaster.getLedgerByHash(*refHash); if (!ledger) { - // We don't have the ledger we need to figure out which ledger - // they want. Try to get it. + // We don't have the ledger we need to figure out which + // ledger they want. Try to get it. if (auto il = context.app.getInboundLedgers().acquire( *refHash, refIndex, InboundLedger::Reason::GENERIC)) diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp new file mode 100644 index 00000000000..14fcf8bf3cd --- /dev/null +++ b/src/test/app/XChain_test.cpp @@ -0,0 +1,5121 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple::test { + +// SEnv class - encapsulate jtx::Env to make it more user-friendly, +// for example having APIs that return a *this reference so that calls can be +// chained (fluent interface) allowing to create an environment and use it +// without encapsulating it in a curly brace block. +// --------------------------------------------------------------------------- +template +struct SEnv +{ + jtx::Env env_; + + SEnv( + T& s, + std::unique_ptr config, + FeatureBitset features, + std::unique_ptr logs = nullptr, + beast::severities::Severity thresh = beast::severities::kError) + : env_(s, std::move(config), features, std::move(logs), thresh) + { + } + + SEnv& + close() + { + env_.close(); + return *this; + } + + SEnv& + enableFeature(uint256 const feature) + { + env_.enableFeature(feature); + return *this; + } + + SEnv& + disableFeature(uint256 const feature) + { + env_.app().config().features.erase(feature); + return *this; + } + + template + SEnv& + fund(STAmount const& amount, Arg const& arg, Args const&... args) + { + env_.fund(amount, arg, args...); + return *this; + } + + template + SEnv& + tx(JsonValue&& jv, FN const&... fN) + { + env_(std::forward(jv), fN...); + return *this; + } + + template + SEnv& + multiTx(jtx::JValueVec&& jvv, FN const&... fN) + { + for (auto const& jv : jvv) + env_(jv, fN...); + return *this; + } + + TER + ter() const + { + return env_.ter(); + } + + STAmount + balance(jtx::Account const& account) const + { + return env_.balance(account).value(); + } + + STAmount + balance(jtx::Account const& account, Issue const& issue) const + { + return env_.balance(account, issue).value(); + } + + XRPAmount + reserve(std::uint32_t count) + { + return env_.current()->fees().accountReserve(count); + } + + XRPAmount + txFee() + { + return env_.current()->fees().base; + } + + std::shared_ptr + account(jtx::Account const& account) + { + return env_.le(account); + } + + std::shared_ptr + bridge(Json::Value const& jvb) + { + STXChainBridge b(jvb); + + auto tryGet = + [&](STXChainBridge::ChainType ct) -> std::shared_ptr { + if (auto r = env_.le(keylet::bridge(b, ct))) + { + if ((*r)[sfXChainBridge] == b) + return r; + } + return nullptr; + }; + if (auto r = tryGet(STXChainBridge::ChainType::locking)) + return r; + return tryGet(STXChainBridge::ChainType::issuing); + } + + std::uint64_t + claimCount(Json::Value const& jvb) + { + return (*bridge(jvb))[sfXChainAccountClaimCount]; + } + + std::uint64_t + claimID(Json::Value const& jvb) + { + return (*bridge(jvb))[sfXChainClaimID]; + } + + std::shared_ptr + claimID(Json::Value const& jvb, std::uint64_t seq) + { + return env_.le(keylet::xChainClaimID(STXChainBridge(jvb), seq)); + } + + std::shared_ptr + caClaimID(Json::Value const& jvb, std::uint64_t seq) + { + return env_.le( + keylet::xChainCreateAccountClaimID(STXChainBridge(jvb), seq)); + } +}; + +// XEnv class used for XChain tests. The only difference with SEnv is that it +// funds some default accounts, and that it enables `supported_amendments() | +// FeatureBitset{featureXChainBridge}` by default. +// ----------------------------------------------------------------------------- +template +struct XEnv : public jtx::XChainBridgeObjects, public SEnv +{ + XEnv(T& s, bool side = false) + : SEnv( + s, + jtx::envconfig(jtx::port_increment, side ? 3 : 0), + features) + { + using namespace jtx; + STAmount xrp_funds{XRP(10000)}; + + if (!side) + { + this->fund(xrp_funds, mcDoor, mcAlice, mcBob, mcCarol, mcGw); + + // Signer's list must match the attestation signers + // env_(jtx::signers(mcDoor, quorum, signers)); + for (auto& s : signers) + this->fund(xrp_funds, s.account); + } + else + { + this->fund( + xrp_funds, + scDoor, + scAlice, + scBob, + scCarol, + scGw, + scAttester, + scReward); + + for (auto& ra : payees) + this->fund(xrp_funds, ra); + + for (auto& s : signers) + this->fund(xrp_funds, s.account); + + // Signer's list must match the attestation signers + // env_(jtx::signers(Account::master, quorum, signers)); + } + this->close(); + } +}; + +// Tracks the xrp balance for one account +template +struct Balance +{ + jtx::Account const& account_; + T& env_; + STAmount startAmount; + + Balance(T& env, jtx::Account const& account) : account_(account), env_(env) + { + startAmount = env_.balance(account_); + } + + STAmount + diff() const + { + return env_.balance(account_) - startAmount; + } +}; + +// Tracks the xrp balance for multiple accounts involved in a crosss-chain +// transfer +template +struct BalanceTransfer +{ + using balance = Balance; + + balance from_; + balance to_; + balance payor_; // pays the rewards + std::vector reward_accounts; // receives the reward + XRPAmount txFees_; + + BalanceTransfer( + T& env, + jtx::Account const& from_acct, + jtx::Account const& to_acct, + jtx::Account const& payor, + jtx::Account const* payees, + size_t num_payees, + bool withClaim) + : from_(env, from_acct) + , to_(env, to_acct) + , payor_(env, payor) + , reward_accounts([&]() { + std::vector r; + r.reserve(num_payees); + for (size_t i = 0; i < num_payees; ++i) + r.emplace_back(env, payees[i]); + return r; + }()) + , txFees_(withClaim ? env.env_.current()->fees().base : XRPAmount(0)) + { + } + + BalanceTransfer( + T& env, + jtx::Account const& from_acct, + jtx::Account const& to_acct, + jtx::Account const& payor, + std::vector const& payees, + bool withClaim) + : BalanceTransfer( + env, + from_acct, + to_acct, + payor, + &payees[0], + payees.size(), + withClaim) + { + } + + bool + payees_received(STAmount const& reward) const + { + return std::all_of( + reward_accounts.begin(), + reward_accounts.end(), + [&](const balance& b) { return b.diff() == reward; }); + } + + bool + check_most_balances(STAmount const& amt, STAmount const& reward) + { + return from_.diff() == -amt && to_.diff() == amt && + payees_received(reward); + } + + bool + has_happened( + STAmount const& amt, + STAmount const& reward, + bool check_payer = true) + { + auto reward_cost = + multiply(reward, STAmount(reward_accounts.size()), reward.issue()); + return check_most_balances(amt, reward) && + (!check_payer || payor_.diff() == -(reward_cost + txFees_)); + } + + bool + has_not_happened() + { + return check_most_balances(STAmount(0), STAmount(0)) && + payor_.diff() <= txFees_; // could have paid fee for failed claim + } +}; + +struct BridgeDef +{ + jtx::Account doorA; + Issue issueA; + jtx::Account doorB; + Issue issueB; + STAmount reward; + STAmount minAccountCreate; + uint32_t quorum; + std::vector const& signers; + Json::Value jvb; + + template + void + initBridge(ENV& mcEnv, ENV& scEnv) + { + jvb = bridge(doorA, issueA, doorB, issueB); + + auto const optAccountCreate = [&]() -> std::optional { + if (issueA != xrpIssue() || issueB != xrpIssue()) + return {}; + return minAccountCreate; + }(); + mcEnv.tx(bridge_create(doorA, jvb, reward, optAccountCreate)) + .tx(jtx::signers(doorA, quorum, signers)) + .close(); + + scEnv.tx(bridge_create(doorB, jvb, reward, optAccountCreate)) + .tx(jtx::signers(doorB, quorum, signers)) + .close(); + } +}; + +struct XChain_test : public beast::unit_test::suite, + public jtx::XChainBridgeObjects +{ + XRPAmount + reserve(std::uint32_t count) + { + return XEnv(*this).env_.current()->fees().accountReserve(count); + } + + XRPAmount + txFee() + { + return XEnv(*this).env_.current()->fees().base; + } + + void + testXChainBridgeExtraFields() + { + auto jBridge = create_bridge(mcDoor)[sfXChainBridge.jsonName]; + bool exceptionPresent = false; + try + { + exceptionPresent = false; + [[maybe_unused]] STXChainBridge testBridge1(jBridge); + } + catch (std::exception& ec) + { + exceptionPresent = true; + } + + BEAST_EXPECT(!exceptionPresent); + + try + { + exceptionPresent = false; + jBridge["Extra"] = 1; + [[maybe_unused]] STXChainBridge testBridge2(jBridge); + } + catch ([[maybe_unused]] std::exception& ec) + { + exceptionPresent = true; + } + + BEAST_EXPECT(exceptionPresent); + } + + void + testXChainCreateBridge() + { + XRPAmount res1 = reserve(1); + + using namespace jtx; + testcase("Create Bridge"); + + // Normal create_bridge => should succeed + XEnv(*this).tx(create_bridge(mcDoor)).close(); + + // Bridge not owned by one of the door account. + XEnv(*this).tx( + create_bridge(mcBob), ter(temXCHAIN_BRIDGE_NONDOOR_OWNER)); + + // Create twice on the same account + XEnv(*this) + .tx(create_bridge(mcDoor)) + .close() + .tx(create_bridge(mcDoor), ter(tecDUPLICATE)); + + // Create USD bridge Alice -> Bob ... should succeed + XEnv(*this).tx( + create_bridge( + mcAlice, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"])), + ter(tesSUCCESS)); + + // Create USD bridge, Alice is both the locking door and locking issue, + // ... should fail. + XEnv(*this).tx( + create_bridge( + mcAlice, bridge(mcAlice, mcAlice["USD"], mcBob, mcBob["USD"])), + ter(temXCHAIN_BRIDGE_BAD_ISSUES)); + + // Bridge where the two door accounts are equal. + XEnv(*this).tx( + create_bridge( + mcBob, bridge(mcBob, mcGw["USD"], mcBob, mcGw["USD"])), + ter(temXCHAIN_EQUAL_DOOR_ACCOUNTS)); + + // Both door accounts are on the same chain is allowed (they likely + // belong to different chains. If they do belong to the same chain, that + // is silly, but doesn't violate any invariants). + XEnv(*this) + .tx(create_bridge( + mcAlice, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"]))) + .close() + .tx(create_bridge( + mcBob, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"]))) + .close(); + + // Create a bridge on an account with exactly enough balance to + // meet the new reserve should succeed + XEnv(*this) + .fund(res1, mcuDoor) // exact reserve for account + 1 object + .close() + .tx(create_bridge(mcuDoor, jvub), ter(tesSUCCESS)); + + // Create a bridge on an account with no enough balance to meet the + // new reserve + XEnv(*this) + .fund(res1 - 1, mcuDoor) // just short of required reserve + .close() + .tx(create_bridge(mcuDoor, jvub), ter(tecINSUFFICIENT_RESERVE)); + + // Reward amount is non-xrp + XEnv(*this).tx( + create_bridge(mcDoor, jvb, mcUSD(1)), + ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)); + + // Reward amount is XRP and negative + XEnv(*this).tx( + create_bridge(mcDoor, jvb, XRP(-1)), + ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)); + + // Reward amount is 1 xrp => should succeed + XEnv(*this).tx(create_bridge(mcDoor, jvb, XRP(1)), ter(tesSUCCESS)); + + // Min create amount is 1 xrp, mincreate is 1 xrp => should succeed + XEnv(*this).tx( + create_bridge(mcDoor, jvb, XRP(1), XRP(1)), ter(tesSUCCESS)); + + // Min create amount is non-xrp + XEnv(*this).tx( + create_bridge(mcDoor, jvb, XRP(1), mcUSD(100)), + ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); + + // Min create amount is zero (should fail, currently succeeds) + XEnv(*this).tx( + create_bridge(mcDoor, jvb, XRP(1), XRP(0)), + ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); + + // Min create amount is negative + XEnv(*this).tx( + create_bridge(mcDoor, jvb, XRP(1), XRP(-1)), + ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); + + // coverage test: BridgeCreate::preflight() - create bridge when feature + // disabled. + { + Env env(*this); + env(create_bridge(Account::master, jvb), ter(temDISABLED)); + } + + // coverage test: BridgeCreate::preclaim() returns tecNO_ISSUER. + XEnv(*this).tx( + create_bridge( + mcAlice, bridge(mcAlice, mcuAlice["USD"], mcBob, mcBob["USD"])), + ter(tecNO_ISSUER)); + + // coverage test: create_bridge transaction with incorrect flag + XEnv(*this).tx( + create_bridge(mcAlice, jvb), + txflags(tfFillOrKill), + ter(temINVALID_FLAG)); + + // coverage test: create_bridge transaction with xchain feature disabled + XEnv(*this) + .disableFeature(featureXChainBridge) + .tx(create_bridge(mcAlice, jvb), ter(temDISABLED)); + } + + void + testXChainBridgeCreateConstraints() + { + /** + * Bridge create constraints tests. + * + * Define the door's bridge asset collection as the collection of all + * the issuing assets for which the door account is on the issuing chain + * and all the locking assets for which the door account is on the + * locking chain. (note: a door account can simultaneously be on an + * issuing and locking chain). A new bridge is not a duplicate as long + * as the new bridge asset collection does not contain any duplicate + * currencies (even if the issuers differ). + * + * Create bridges: + * + *| Owner | Locking | Issuing | Comment | + *| a1 | a1 USD/GW | USD/B | | + *| a2 | a2 USD/GW | USD/B | Same locking & issuing assets | + *| | | | | + *| a3 | a3 USD/GW | USD/a4 | | + *| a4 | a4 USD/GW | USD/a4 | Same bridge, different accounts | + *| | | | | + *| B | A USD/GW | USD/B | | + *| B | A EUR/GW | USD/B | Fail: Same issuing asset | + *| | | | | + *| A | A USD/B | USD/C | | + *| A | A USD/B | EUR/B | Fail: Same locking asset | + *| A | A USD/C | EUR/B | Fail: Same locking asset currency | + *| | | | | + *| A | A USD/GW | USD/B | Fail: Same bridge not allowed | + *| A | B USD/GW | USD/A | Fail: "A" has USD already | + *| B | A EUR/GW | USD/B | Fail: | + * + * Note that, now from sidechain's point of view, A is both + * a local locking door and a foreign locking door on different + * bridges. Txns such as commits specify bridge spec, but not the + * local door account. So we test the transactors can figure out + * the correct local door account from bridge spec. + * + * Commit to sidechain door accounts: + * | bridge spec | result + * case 6 | A -> B | B's balance increase + * case 7 | C <- A | A's balance increase + * + * We also test ModifyBridge txns modify correct bridges. + */ + + using namespace jtx; + testcase("Bridge create constraints"); + XEnv env(*this, true); + auto& A = scAlice; + auto& B = scBob; + auto& C = scCarol; + auto AUSD = A["USD"]; + auto BUSD = B["USD"]; + auto CUSD = C["USD"]; + auto GUSD = scGw["USD"]; + auto AEUR = A["EUR"]; + auto BEUR = B["EUR"]; + auto CEUR = C["EUR"]; + auto GEUR = scGw["EUR"]; + + // Accounts to own single brdiges + Account const a1("a1"); + Account const a2("a2"); + Account const a3("a3"); + Account const a4("a4"); + + env.fund(XRP(10000), a1, a2, a3, a4); + env.close(); + + // Add a bridge on two different accounts with the same locking and + // issuing assets + env.tx(create_bridge(a1, bridge(a1, GUSD, B, BUSD))).close(); + env.tx(create_bridge(a2, bridge(a2, GUSD, B, BUSD))).close(); + + // Add the exact same bridge to two different accoutns (one must locking + // account and one must be issuing) + env.tx(create_bridge(a3, bridge(a3, GUSD, a4, a4["USD"]))).close(); + env.tx(create_bridge(a4, bridge(a3, GUSD, a4, a4["USD"]))).close(); + + // Test case 1 ~ 5, create bridges + auto const goodBridge1 = bridge(A, GUSD, B, BUSD); + auto const goodBridge2 = bridge(A, BUSD, C, CUSD); + env.tx(create_bridge(B, goodBridge1)).close(); + // Issuing asset is the same, this is a duplicate + env.tx(create_bridge(B, bridge(A, GEUR, B, BUSD)), ter(tecDUPLICATE)) + .close(); + env.tx(create_bridge(A, goodBridge2), ter(tesSUCCESS)).close(); + // Locking asset is the same - this is a duplicate + env.tx(create_bridge(A, bridge(A, BUSD, B, BEUR)), ter(tecDUPLICATE)) + .close(); + // Locking asset is USD - this is a duplicate even tho it has a + // different issuer + env.tx(create_bridge(A, bridge(A, CUSD, B, BEUR)), ter(tecDUPLICATE)) + .close(); + + // Test case 6 and 7, commits + env.tx(trust(C, BUSD(1000))) + .tx(trust(A, BUSD(1000))) + .close() + .tx(pay(B, C, BUSD(1000))) + .close(); + auto const aBalanceStart = env.balance(A, BUSD); + auto const cBalanceStart = env.balance(C, BUSD); + env.tx(xchain_commit(C, goodBridge1, 1, BUSD(50))).close(); + BEAST_EXPECT(env.balance(A, BUSD) - aBalanceStart == BUSD(0)); + BEAST_EXPECT(env.balance(C, BUSD) - cBalanceStart == BUSD(-50)); + env.tx(xchain_commit(C, goodBridge2, 1, BUSD(60))).close(); + BEAST_EXPECT(env.balance(A, BUSD) - aBalanceStart == BUSD(60)); + BEAST_EXPECT(env.balance(C, BUSD) - cBalanceStart == BUSD(-50 - 60)); + + // bridge modify test cases + env.tx(bridge_modify(B, goodBridge1, XRP(33), std::nullopt)).close(); + BEAST_EXPECT((*env.bridge(goodBridge1))[sfSignatureReward] == XRP(33)); + env.tx(bridge_modify(A, goodBridge2, XRP(44), std::nullopt)).close(); + BEAST_EXPECT((*env.bridge(goodBridge2))[sfSignatureReward] == XRP(44)); + } + + void + testXChainCreateBridgeMatrix() + { + using namespace jtx; + testcase("Create Bridge Matrix"); + + // Test all combinations of the following:` + // -------------------------------------- + // - Locking chain is IOU with locking chain door account as issuer + // - Locking chain is IOU with issuing chain door account that + // exists on the locking chain as issuer + // - Locking chain is IOU with issuing chain door account that does + // not exists on the locking chain as issuer + // - Locking chain is IOU with non-door account (that exists on the + // locking chain ledger) as issuer + // - Locking chain is IOU with non-door account (that does not exist + // exists on the locking chain ledger) as issuer + // - Locking chain is XRP + // --------------------------------------------------------------------- + // - Issuing chain is IOU with issuing chain door account as the + // issuer + // - Issuing chain is IOU with locking chain door account (that + // exists on the issuing chain ledger) as the issuer + // - Issuing chain is IOU with locking chain door account (that does + // not exist on the issuing chain ledger) as the issuer + // - Issuing chain is IOU with non-door account (that exists on the + // issuing chain ledger) as the issuer + // - Issuing chain is IOU with non-door account (that does not + // exists on the issuing chain ledger) as the issuer + // - Issuing chain is XRP and issuing chain door account is not the + // root account + // - Issuing chain is XRP and issuing chain door account is the root + // account + // --------------------------------------------------------------------- + // That's 42 combinations. The only combinations that should succeed + // are: + // - Locking chain is any IOU, + // - Issuing chain is IOU with issuing chain door account as the + // issuer + // Locking chain is XRP, + // - Issuing chain is XRP with issuing chain is the root account. + // --------------------------------------------------------------------- + Account a, b; + Issue ia, ib; + + std::tuple lcs{ + std::make_pair( + "Locking chain is IOU(locking chain door)", + [&](auto& env, bool) { + a = mcDoor; + ia = mcDoor["USD"]; + }), + std::make_pair( + "Locking chain is IOU(issuing chain door funded on locking " + "chain)", + [&](auto& env, bool shouldFund) { + a = mcDoor; + ia = scDoor["USD"]; + if (shouldFund) + env.fund(XRP(10000), scDoor); + }), + std::make_pair( + "Locking chain is IOU(issuing chain door account unfunded " + "on locking chain)", + [&](auto& env, bool) { + a = mcDoor; + ia = scDoor["USD"]; + }), + std::make_pair( + "Locking chain is IOU(bob funded on locking chain)", + [&](auto& env, bool) { + a = mcDoor; + ia = mcGw["USD"]; + }), + std::make_pair( + "Locking chain is IOU(bob unfunded on locking chain)", + [&](auto& env, bool) { + a = mcDoor; + ia = mcuGw["USD"]; + }), + std::make_pair("Locking chain is XRP", [&](auto& env, bool) { + a = mcDoor; + ia = xrpIssue(); + })}; + + std::tuple ics{ + std::make_pair( + "Issuing chain is IOU(issuing chain door account)", + [&](auto& env, bool) { + b = scDoor; + ib = scDoor["USD"]; + }), + std::make_pair( + "Issuing chain is IOU(locking chain door funded on issuing " + "chain)", + [&](auto& env, bool shouldFund) { + b = scDoor; + ib = mcDoor["USD"]; + if (shouldFund) + env.fund(XRP(10000), mcDoor); + }), + std::make_pair( + "Issuing chain is IOU(locking chain door unfunded on " + "issuing chain)", + [&](auto& env, bool) { + b = scDoor; + ib = mcDoor["USD"]; + }), + std::make_pair( + "Issuing chain is IOU(bob funded on issuing chain)", + [&](auto& env, bool) { + b = scDoor; + ib = mcGw["USD"]; + }), + std::make_pair( + "Issuing chain is IOU(bob unfunded on issuing chain)", + [&](auto& env, bool) { + b = scDoor; + ib = mcuGw["USD"]; + }), + std::make_pair( + "Issuing chain is XRP and issuing chain door account is " + "not the root account", + [&](auto& env, bool) { + b = scDoor; + ib = xrpIssue(); + }), + std::make_pair( + "Issuing chain is XRP and issuing chain door account is " + "the root account ", + [&](auto& env, bool) { + b = Account::master; + ib = xrpIssue(); + })}; + + std::vector> expected_result{ + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {tesSUCCESS, tesSUCCESS}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {tecNO_ISSUER, tesSUCCESS}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {tesSUCCESS, tesSUCCESS}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {tecNO_ISSUER, tesSUCCESS}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES}, + {tesSUCCESS, tesSUCCESS}}; + + std::vector> test_result; + + auto testcase = [&](auto const& lc, auto const& ic) { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + lc.second(mcEnv, true); + lc.second(scEnv, false); + + ic.second(mcEnv, false); + ic.second(scEnv, true); + + auto const& expected = expected_result[test_result.size()]; + + mcEnv.tx( + create_bridge(a, bridge(a, ia, b, ib)), + ter(TER::fromInt(expected.first))); + TER mcTER = mcEnv.env_.ter(); + + scEnv.tx( + create_bridge(b, bridge(a, ia, b, ib)), + ter(TER::fromInt(expected.second))); + TER scTER = scEnv.env_.ter(); + + bool pass = mcTER == tesSUCCESS && scTER == tesSUCCESS; + + test_result.emplace_back(mcTER, scTER, pass); + }; + + auto apply_ics = [&](auto const& lc, auto const& ics) { + std::apply( + [&](auto const&... ic) { (testcase(lc, ic), ...); }, ics); + }; + + std::apply([&](auto const&... lc) { (apply_ics(lc, ics), ...); }, lcs); + +#if GENERATE_MTX_OUTPUT + // optional output of matrix results in markdown format + // ---------------------------------------------------- + std::string fname{std::tmpnam(nullptr)}; + fname += ".md"; + std::cout << "Markdown output for matrix test: " << fname << "\n"; + + auto print_res = [](auto tup) -> std::string { + std::string status = std::string(transToken(std::get<0>(tup))) + + " / " + transToken(std::get<1>(tup)); + + if (std::get<2>(tup)) + return status; + else + { + // red + return std::string("`") + status + "`"; + } + }; + + auto output_table = [&](auto print_res) { + size_t test_idx = 0; + std::string res; + res.reserve(10000); // should be enough :-) + + // first two header lines + res += "| `issuing ->` | "; + std::apply( + [&](auto const&... ic) { + ((res += ic.first, res += " | "), ...); + }, + ics); + res += "\n"; + + res += "| :--- | "; + std::apply( + [&](auto const&... ic) { + (((void)ic.first, res += ":---: | "), ...); + }, + ics); + res += "\n"; + + auto output = [&](auto const& lc, auto const& ic) { + res += print_res(test_result[test_idx]); + res += " | "; + ++test_idx; + }; + + auto output_ics = [&](auto const& lc, auto const& ics) { + res += "| "; + res += lc.first; + res += " | "; + std::apply( + [&](auto const&... ic) { (output(lc, ic), ...); }, ics); + res += "\n"; + }; + + std::apply( + [&](auto const&... lc) { (output_ics(lc, ics), ...); }, lcs); + + return res; + }; + + std::ofstream(fname) << output_table(print_res); + + std::string ter_fname{std::tmpnam(nullptr)}; + std::cout << "ter output for matrix test: " << ter_fname << "\n"; + + std::ofstream ofs(ter_fname); + for (auto& t : test_result) + { + ofs << "{ " << std::string(transToken(std::get<0>(t))) << ", " + << std::string(transToken(std::get<1>(t))) << "}\n,"; + } +#endif + } + + void + testXChainModifyBridge() + { + using namespace jtx; + testcase("Modify Bridge"); + + // Changing a non-existent bridge should fail + XEnv(*this).tx( + bridge_modify( + mcAlice, + bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"]), + XRP(2), + std::nullopt), + ter(tecNO_ENTRY)); + + // must change something + // XEnv(*this) + // .tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1))) + // .tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(1)), + // ter(temMALFORMED)); + + // must change something + XEnv(*this) + .tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1))) + .close() + .tx(bridge_modify(mcDoor, jvb, {}, {}), ter(temMALFORMED)); + + // Reward amount is non-xrp + XEnv(*this).tx( + bridge_modify(mcDoor, jvb, mcUSD(2), XRP(10)), + ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)); + + // Reward amount is XRP and negative + XEnv(*this).tx( + bridge_modify(mcDoor, jvb, XRP(-2), XRP(10)), + ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)); + + // Min create amount is non-xrp + XEnv(*this).tx( + bridge_modify(mcDoor, jvb, XRP(2), mcUSD(10)), + ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); + + // Min create amount is zero + XEnv(*this).tx( + bridge_modify(mcDoor, jvb, XRP(2), XRP(0)), + ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); + + // Min create amount is negative + XEnv(*this).tx( + bridge_modify(mcDoor, jvb, XRP(2), XRP(-10)), + ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); + + // First check the regular claim process (without bridge_modify) + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)) + .close(); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + } + + BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum)); + } + + // Check that the reward paid from a claim Id was the reward when + // the claim id was created, not the reward since the bridge was + // modified. + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + // Now modify the reward on the bridge + mcEnv.tx(bridge_modify(mcDoor, jvb, XRP(2), XRP(10))).close(); + scEnv.tx(bridge_modify(Account::master, jvb, XRP(2), XRP(10))) + .close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)) + .close(); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + } + + // make sure the reward accounts indeed received the original + // split reward (1 split 5 ways) instead of the updated 2 XRP. + BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum)); + } + + // Check that the signatures used to verify attestations and decide + // if there is a quorum are the current signer's list on the door + // account, not the signer's list that was in effect when the claim + // id was created. + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + // change signers - claim should not be processed is the batch + // is signed by original signers + scEnv.tx(jtx::signers(Account::master, quorum, alt_signers)) + .close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + + // submit claim using outdated signers - should fail + scEnv + .multiTx( + claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers), + ter(tecNO_PERMISSION)) + .close(); + if (withClaim) + { + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecXCHAIN_CLAIM_NO_QUORUM)) + .close(); + } + + // make sure transfer has not happened as we sent attestations + // using outdated signers + BEAST_EXPECT(transfer.has_not_happened()); + + // submit claim using current signers - should succeed + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + alt_signers)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + } + + // make sure the transfer went through as we sent attestations + // using new signers + BEAST_EXPECT( + transfer.has_happened(amt, split_reward_quorum, false)); + } + + // coverage test: bridge_modify transaction with incorrect flag + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .close() + .tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(2)), + txflags(tfFillOrKill), + ter(temINVALID_FLAG)); + + // coverage test: bridge_modify transaction with xchain feature + // disabled + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .disableFeature(featureXChainBridge) + .close() + .tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(2)), ter(temDISABLED)); + + // coverage test: bridge_modify return temSIDECHAIN_NONDOOR_OWNER; + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .close() + .tx(bridge_modify(mcAlice, jvb, XRP(1), XRP(2)), + ter(temXCHAIN_BRIDGE_NONDOOR_OWNER)); + + /** + * test tfClearAccountCreateAmount flag in BridgeModify tx + * -- tx has both minAccountCreateAmount and the flag, temMALFORMED + * -- tx has the flag and also modifies signature reward, tesSUCCESS + * -- XChainCreateAccountCommit tx fail after previous step + */ + XEnv(*this) + .tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))) + .close() + .tx(sidechain_xchain_account_create( + mcAlice, jvb, scuAlice, XRP(100), reward)) + .close() + .tx(bridge_modify(mcDoor, jvb, {}, XRP(2)), + txflags(tfClearAccountCreateAmount), + ter(temMALFORMED)) + .close() + .tx(bridge_modify(mcDoor, jvb, XRP(3), {}), + txflags(tfClearAccountCreateAmount)) + .close() + .tx(sidechain_xchain_account_create( + mcAlice, jvb, scuBob, XRP(100), XRP(3)), + ter(tecXCHAIN_CREATE_ACCOUNT_DISABLED)) + .close(); + } + + void + testXChainCreateClaimID() + { + using namespace jtx; + XRPAmount res1 = reserve(1); + XRPAmount tx_fee = txFee(); + + testcase("Create ClaimID"); + + // normal bridge create for sanity check with the exact necessary + // account balance + XEnv(*this, true) + .tx(create_bridge(Account::master, jvb)) + .fund(res1, scuAlice) // acct reserve + 1 object + .close() + .tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice)) + .close(); + + // check reward not deducted when claim id is created + { + XEnv xenv(*this, true); + + Balance scAlice_bal(xenv, scAlice); + + xenv.tx(create_bridge(Account::master, jvb)) + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + BEAST_EXPECT(scAlice_bal.diff() == -tx_fee); + } + + // Non-existent bridge + XEnv(*this, true) + .tx(xchain_create_claim_id( + scAlice, + bridge(mcAlice, mcAlice["USD"], scBob, scBob["USD"]), + reward, + mcAlice), + ter(tecNO_ENTRY)) + .close(); + + // Creating the new object would put the account below the reserve + XEnv(*this, true) + .tx(create_bridge(Account::master, jvb)) + .fund(res1 - xrp_dust, scuAlice) // barely not enough + .close() + .tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice), + ter(tecINSUFFICIENT_RESERVE)) + .close(); + + // The specified reward doesn't match the reward on the bridge (test + // by giving the reward amount for the other side, as well as a + // completely non-matching reward) + XEnv(*this, true) + .tx(create_bridge(Account::master, jvb)) + .close() + .tx(xchain_create_claim_id( + scAlice, jvb, split_reward_quorum, mcAlice), + ter(tecXCHAIN_REWARD_MISMATCH)) + .close(); + + // A reward amount that isn't XRP + XEnv(*this, true) + .tx(create_bridge(Account::master, jvb)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, mcUSD(1), mcAlice), + ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)) + .close(); + + // coverage test: xchain_create_claim_id transaction with incorrect + // flag + XEnv(*this, true) + .tx(create_bridge(Account::master, jvb)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice), + txflags(tfFillOrKill), + ter(temINVALID_FLAG)) + .close(); + + // coverage test: xchain_create_claim_id transaction with xchain + // feature disabled + XEnv(*this, true) + .tx(create_bridge(Account::master, jvb)) + .disableFeature(featureXChainBridge) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice), + ter(temDISABLED)) + .close(); + } + + void + testXChainCommit() + { + using namespace jtx; + XRPAmount res0 = reserve(0); + XRPAmount tx_fee = txFee(); + + testcase("Commit"); + + // Commit to a non-existent bridge + XEnv(*this).tx( + xchain_commit(mcAlice, jvb, 1, one_xrp, scBob), ter(tecNO_ENTRY)); + + // check that reward not deducted when doing the commit + { + XEnv xenv(*this); + + Balance alice_bal(xenv, mcAlice); + auto const amt = XRP(1000); + + xenv.tx(create_bridge(mcDoor, jvb)) + .close() + .tx(xchain_commit(mcAlice, jvb, 1, amt, scBob)) + .close(); + + STAmount claim_cost = amt; + BEAST_EXPECT(alice_bal.diff() == -(claim_cost + tx_fee)); + } + + // Commit a negative amount + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .close() + .tx(xchain_commit(mcAlice, jvb, 1, XRP(-1), scBob), + ter(temBAD_AMOUNT)); + + // Commit an amount whose issue that does not match the expected + // issue on the bridge (either LockingChainIssue or + // IssuingChainIssue, depending on the chain). + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .close() + .tx(xchain_commit(mcAlice, jvb, 1, mcUSD(100), scBob), + ter(temBAD_ISSUER)); + + // Commit an amount that would put the sender below the required + // reserve (if XRP) + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .fund(res0 + one_xrp - xrp_dust, mcuAlice) // barely not enough + .close() + .tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob), + ter(tecUNFUNDED_PAYMENT)); + + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .fund( + res0 + one_xrp + xrp_dust, // "xrp_dust" for tx fees + mcuAlice) // exactly enough => should succeed + .close() + .tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob)); + + // Commit an amount above the account's balance (for both XRP and + // IOUs) + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .fund(res0, mcuAlice) // barely not enough + .close() + .tx(xchain_commit(mcuAlice, jvb, 1, res0 + one_xrp, scBob), + ter(tecUNFUNDED_PAYMENT)); + + auto jvb_USD = bridge(mcDoor, mcUSD, scGw, scUSD); + + // commit sent from iou issuer (mcGw) succeeds - should it? + XEnv(*this) + .tx(trust(mcDoor, mcUSD(10000))) // door needs to have a trustline + .tx(create_bridge(mcDoor, jvb_USD)) + .close() + .tx(xchain_commit(mcGw, jvb_USD, 1, mcUSD(1), scBob)); + + // commit to a door account from the door account. This should fail. + XEnv(*this) + .tx(trust(mcDoor, mcUSD(10000))) // door needs to have a trustline + .tx(create_bridge(mcDoor, jvb_USD)) + .close() + .tx(xchain_commit(mcDoor, jvb_USD, 1, mcUSD(1), scBob), + ter(tecXCHAIN_SELF_COMMIT)); + + // commit sent from mcAlice which has no IOU balance => should fail + XEnv(*this) + .tx(trust(mcDoor, mcUSD(10000))) // door needs to have a trustline + .tx(create_bridge(mcDoor, jvb_USD)) + .close() + .tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(1), scBob), + ter(terNO_LINE)); + + // commit sent from mcAlice which has no IOU balance => should fail + // just changed the destination to scGw (which is the door account + // and may not make much sense) + XEnv(*this) + .tx(trust(mcDoor, mcUSD(10000))) // door needs to have a trustline + .tx(create_bridge(mcDoor, jvb_USD)) + .close() + .tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(1), scGw), + ter(terNO_LINE)); + + // commit sent from mcAlice which has a IOU balance => should + // succeed + XEnv(*this) + .tx(trust(mcDoor, mcUSD(10000))) + .tx(trust(mcAlice, mcUSD(10000))) + .close() + .tx(pay(mcGw, mcAlice, mcUSD(10))) + .tx(create_bridge(mcDoor, jvb_USD)) + .close() + //.tx(pay(mcAlice, mcDoor, mcUSD(10))); + .tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(10), scAlice)); + + // coverage test: xchain_commit transaction with incorrect flag + XEnv(*this) + .tx(create_bridge(mcDoor)) + .close() + .tx(xchain_commit(mcAlice, jvb, 1, one_xrp, scBob), + txflags(tfFillOrKill), + ter(temINVALID_FLAG)); + + // coverage test: xchain_commit transaction with xchain feature + // disabled + XEnv(*this) + .tx(create_bridge(mcDoor)) + .disableFeature(featureXChainBridge) + .close() + .tx(xchain_commit(mcAlice, jvb, 1, one_xrp, scBob), + ter(temDISABLED)); + } + + void + testXChainAddAttestation() + { + using namespace jtx; + + testcase("Add Attestation"); + XRPAmount res0 = reserve(0); + XRPAmount tx_fee = txFee(); + + auto multiTtxFee = [&](std::uint32_t m) -> STAmount { + return multiply(tx_fee, STAmount(m), xrpIssue()); + }; + + // Add an attestation to a claim id that has already reached quorum. + // This should succeed and share in the reward. + // note: this is true only when either: + // 1. dest account is not specified, so transfer requires a claim + // 2. or the extra attestation is sent in the same batch as the + // one reaching quorum + for (auto withClaim : {true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + std::uint32_t const claimID = 1; + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, Account::master, scBob, scAlice, payees, withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers, + UT_XCHAIN_DEFAULT_QUORUM)) + .close(); + scEnv + .tx(claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[UT_XCHAIN_DEFAULT_QUORUM], + true, + claimID, + dst, + signers[UT_XCHAIN_DEFAULT_QUORUM])) + .close(); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + BEAST_EXPECT(!scEnv.claimID(jvb, claimID)); // claim id deleted + BEAST_EXPECT(scEnv.claimID(jvb) == claimID); + } + + BEAST_EXPECT(transfer.has_happened(amt, split_reward_everyone)); + } + + // Test that signature weights are correctly handled. Assign + // signature weights of 1,2,4,4 and a quorum of 7. Check that the + // 4,4 signatures reach a quorum, the 1,2,4, reach a quorum, but the + // 4,2, 4,1 and 1,2 do not. + + // 1,2,4 => should succeed + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + std::uint32_t const quorum_7 = 7; + std::vector const signers_ = [] { + constexpr int numSigners = 4; + std::uint32_t weights[] = {1, 2, 4, 4}; + + std::vector result; + result.reserve(numSigners); + for (int i = 0; i < numSigners; ++i) + { + using namespace std::literals; + auto const a = Account("signer_"s + std::to_string(i)); + result.emplace_back(a, weights[i]); + } + return result; + }(); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum_7, signers_)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + std::uint32_t const claimID = 1; + BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + 3, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers_, + 3)) + .close(); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + } + + BEAST_EXPECT(!scEnv.claimID(jvb, 1)); // claim id deleted + + BEAST_EXPECT(transfer.has_happened( + amt, divide(reward, STAmount(3), reward.issue()))); + } + + // 4,4 => should succeed + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + std::uint32_t const quorum_7 = 7; + std::vector const signers_ = [] { + constexpr int numSigners = 4; + std::uint32_t weights[] = {1, 2, 4, 4}; + + std::vector result; + result.reserve(numSigners); + for (int i = 0; i < numSigners; ++i) + { + using namespace std::literals; + auto const a = Account("signer_"s + std::to_string(i)); + result.emplace_back(a, weights[i]); + } + return result; + }(); + STAmount const split_reward_ = + divide(reward, STAmount(signers_.size()), reward.issue()); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum_7, signers_)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + std::uint32_t const claimID = 1; + BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[2], + 2, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers_, + 2, + 2)) + .close(); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + } + + BEAST_EXPECT(!scEnv.claimID(jvb, claimID)); // claim id deleted + + BEAST_EXPECT(transfer.has_happened( + amt, divide(reward, STAmount(2), reward.issue()))); + } + + // 1,2 => should fail + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + std::uint32_t const quorum_7 = 7; + std::vector const signers_ = [] { + constexpr int numSigners = 4; + std::uint32_t weights[] = {1, 2, 4, 4}; + + std::vector result; + result.reserve(numSigners); + for (int i = 0; i < numSigners; ++i) + { + using namespace std::literals; + auto const a = Account("signer_"s + std::to_string(i)); + result.emplace_back(a, weights[i]); + } + return result; + }(); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum_7, signers_)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + std::uint32_t const claimID = 1; + BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + 2, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers_, + 2)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecXCHAIN_CLAIM_NO_QUORUM)) + .close(); + } + + BEAST_EXPECT( + !!scEnv.claimID(jvb, claimID)); // claim id still present + BEAST_EXPECT(transfer.has_not_happened()); + } + + // 2,4 => should fail + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + std::uint32_t const quorum_7 = 7; + std::vector const signers_ = [] { + constexpr int numSigners = 4; + std::uint32_t weights[] = {1, 2, 4, 4}; + + std::vector result; + result.reserve(numSigners); + for (int i = 0; i < numSigners; ++i) + { + using namespace std::literals; + auto const a = Account("signer_"s + std::to_string(i)); + result.emplace_back(a, weights[i]); + } + return result; + }(); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum_7, signers_)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + std::uint32_t const claimID = 1; + BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[1], + 2, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers_, + 2, + 1)) + .close(); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecXCHAIN_CLAIM_NO_QUORUM)) + .close(); + } + + BEAST_EXPECT( + !!scEnv.claimID(jvb, claimID)); // claim id still present + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Confirm that account create transactions happen in the correct + // order. If they reach quorum out of order they should not execute + // until all the previous created transactions have occurred. + // Re-adding an attestation should move funds. + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + auto const amt = XRP(1000); + auto const amt_plus_reward = amt + reward; + + { + Balance door(mcEnv, mcDoor); + Balance carol(mcEnv, mcCarol); + + mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))) + .close() + .tx(sidechain_xchain_account_create( + mcAlice, jvb, scuAlice, amt, reward)) + .tx(sidechain_xchain_account_create( + mcBob, jvb, scuBob, amt, reward)) + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuCarol, amt, reward)) + .close(); + + BEAST_EXPECT( + door.diff() == + (multiply(amt_plus_reward, STAmount(3), xrpIssue()) - + tx_fee)); + BEAST_EXPECT(carol.diff() == -(amt + reward + tx_fee)); + } + + scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20))) + .tx(jtx::signers(Account::master, quorum, signers)) + .close(); + + { + // send first batch of account create attest for all 3 + // account create + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + + scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2)) + .multiTx(att_create_acct_vec(3, amt, scuCarol, 2)) + .multiTx(att_create_acct_vec(2, amt, scuBob, 2)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + // att_create_acct_vec return vectors of size 2, so 2*3 txns + BEAST_EXPECT(attester.diff() == -multiTtxFee(6)); + + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // ca claim id present + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2)); // ca claim id present + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // ca claim id present + BEAST_EXPECT( + scEnv.claimCount(jvb) == 0); // claim count still 0 + } + + { + // complete attestations for 2nd account create => should + // not complete + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + + scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 3, 2)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + // att_create_acct_vec return vectors of size 3, so 3 txns + BEAST_EXPECT(attester.diff() == -multiTtxFee(3)); + + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2)); // ca claim id present + BEAST_EXPECT( + scEnv.claimCount(jvb) == 0); // claim count still 0 + } + + { + // complete attestations for 3rd account create => should + // not complete + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + + scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 3, 2)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + // att_create_acct_vec return vectors of size 3, so 3 txns + BEAST_EXPECT(attester.diff() == -multiTtxFee(3)); + + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // ca claim id present + BEAST_EXPECT( + scEnv.claimCount(jvb) == 0); // claim count still 0 + } + + { + // complete attestations for 1st account create => account + // should be created + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + + scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 3, 1)) + .close(); + + BEAST_EXPECT(door.diff() == -amt_plus_reward); + // att_create_acct_vec return vectors of size 3, so 3 txns + BEAST_EXPECT(attester.diff() == -multiTtxFee(3)); + BEAST_EXPECT(scEnv.balance(scuAlice) == amt); + + BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id 1 deleted + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2)); // claim id 2 present + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // claim id 3 present + BEAST_EXPECT(scEnv.claimCount(jvb) == 1); // claim count now 1 + } + + { + // resend attestations for 3rd account create => still + // should not complete + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + + scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 3, 2)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + // att_create_acct_vec return vectors of size 3, so 3 txns + BEAST_EXPECT(attester.diff() == -multiTtxFee(3)); + + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2)); // claim id 2 present + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // claim id 3 present + BEAST_EXPECT( + scEnv.claimCount(jvb) == 1); // claim count still 1 + } + + { + // resend attestations for 2nd account create => account + // should be created + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + + scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 1)).close(); + + BEAST_EXPECT(door.diff() == -amt_plus_reward); + BEAST_EXPECT(attester.diff() == -tx_fee); + BEAST_EXPECT(scEnv.balance(scuBob) == amt); + + BEAST_EXPECT(!scEnv.caClaimID(jvb, 2)); // claim id 2 deleted + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // claim id 3 present + BEAST_EXPECT(scEnv.claimCount(jvb) == 2); // claim count now 2 + } + { + // resend attestations for 3rc account create => account + // should be created + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + + scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1)).close(); + + BEAST_EXPECT(door.diff() == -amt_plus_reward); + BEAST_EXPECT(attester.diff() == -tx_fee); + BEAST_EXPECT(scEnv.balance(scuCarol) == amt); + + BEAST_EXPECT(!scEnv.caClaimID(jvb, 3)); // claim id 3 deleted + BEAST_EXPECT(scEnv.claimCount(jvb) == 3); // claim count now 3 + } + } + + // Check that creating an account with less than the minimum reserve + // fails. + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + auto const amt = res0 - XRP(1); + auto const amt_plus_reward = amt + reward; + + mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close(); + + { + Balance door(mcEnv, mcDoor); + Balance carol(mcEnv, mcCarol); + + mcEnv + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuAlice, amt, reward)) + .close(); + + BEAST_EXPECT(door.diff() == amt_plus_reward); + BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee)); + } + + scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20))) + .tx(jtx::signers(Account::master, quorum, signers)) + .close(); + + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + + scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2)).close(); + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // claim id present + BEAST_EXPECT( + scEnv.claimCount(jvb) == 0); // claim count is one less + + scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2, 2)).close(); + BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id deleted + BEAST_EXPECT( + scEnv.claimCount(jvb) == 1); // claim count was incremented + + BEAST_EXPECT(attester.diff() == -multiTtxFee(4)); + BEAST_EXPECT(door.diff() == -reward); + BEAST_EXPECT(!scEnv.account(scuAlice)); + } + + // Check that sending funds with an account create txn to an + // existing account works. + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + auto const amt = XRP(111); + auto const amt_plus_reward = amt + reward; + + mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close(); + + { + Balance door(mcEnv, mcDoor); + Balance carol(mcEnv, mcCarol); + + mcEnv + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scAlice, amt, reward)) + .close(); + + BEAST_EXPECT(door.diff() == amt_plus_reward); + BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee)); + } + + scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20))) + .tx(jtx::signers(Account::master, quorum, signers)) + .close(); + + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + Balance alice(scEnv, scAlice); + + scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2)).close(); + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // claim id present + BEAST_EXPECT( + scEnv.claimCount(jvb) == 0); // claim count is one less + + scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2, 2)).close(); + BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id deleted + BEAST_EXPECT( + scEnv.claimCount(jvb) == 1); // claim count was incremented + + BEAST_EXPECT(door.diff() == -amt_plus_reward); + BEAST_EXPECT(attester.diff() == -multiTtxFee(4)); + BEAST_EXPECT(alice.diff() == amt); + } + + // Check that sending funds to an existing account with deposit auth + // set fails for account create transactions. + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + auto const amt = XRP(1000); + auto const amt_plus_reward = amt + reward; + + mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close(); + + { + Balance door(mcEnv, mcDoor); + Balance carol(mcEnv, mcCarol); + + mcEnv + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scAlice, amt, reward)) + .close(); + + BEAST_EXPECT(door.diff() == amt_plus_reward); + BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee)); + } + + scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20))) + .tx(jtx::signers(Account::master, quorum, signers)) + .tx(fset("scAlice", asfDepositAuth)) // set deposit auth + .close(); + + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + Balance alice(scEnv, scAlice); + + scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2)).close(); + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // claim id present + BEAST_EXPECT( + scEnv.claimCount(jvb) == 0); // claim count is one less + + scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2, 2)).close(); + BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id deleted + BEAST_EXPECT( + scEnv.claimCount(jvb) == 1); // claim count was incremented + + BEAST_EXPECT(door.diff() == -reward); + BEAST_EXPECT(attester.diff() == -multiTtxFee(4)); + BEAST_EXPECT(alice.diff() == STAmount(0)); + } + + // If an account is unable to pay the reserve, check that it fails. + // [greg todo] I don't know what this should test?? + + // If an attestation already exists for that server and claim id, + // the new attestation should replace the old attestation + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + auto const amt = XRP(1000); + auto const amt_plus_reward = amt + reward; + + { + Balance door(mcEnv, mcDoor); + Balance carol(mcEnv, mcCarol); + + mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))) + .close() + .tx(sidechain_xchain_account_create( + mcAlice, jvb, scuAlice, amt, reward)) + .close() // make sure Alice gets claim #1 + .tx(sidechain_xchain_account_create( + mcBob, jvb, scuBob, amt, reward)) + .close() // make sure Bob gets claim #2 + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuCarol, amt, reward)) + .close(); // and Carol will get claim #3 + + BEAST_EXPECT( + door.diff() == + (multiply(amt_plus_reward, STAmount(3), xrpIssue()) - + tx_fee)); + BEAST_EXPECT(carol.diff() == -(amt + reward + tx_fee)); + } + + std::uint32_t const red_quorum = 2; + scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20))) + .tx(jtx::signers(Account::master, red_quorum, signers)) + .close(); + + { + Balance attester(scEnv, scAttester); + Balance door(scEnv, Account::master); + auto const bad_amt = XRP(10); + std::uint32_t txCount = 0; + + // send attestations with incorrect amounts to for all 3 + // AccountCreate. They will be replaced later + scEnv.multiTx(att_create_acct_vec(1, bad_amt, scuAlice, 1)) + .multiTx(att_create_acct_vec(2, bad_amt, scuBob, 1, 2)) + .multiTx(att_create_acct_vec(3, bad_amt, scuCarol, 1, 1)) + .close(); + txCount += 3; + + BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 1), "claim id 1 created"); + BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 2), "claim id 2 created"); + BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 3), "claim id 3 created"); + + // note: if we send inconsistent attestations in the same + // batch, the transaction errors. + + // from now on we send correct attestations + scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 1, 0)) + .multiTx(att_create_acct_vec(2, amt, scuBob, 1, 2)) + .multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 4)) + .close(); + txCount += 3; + + BEAST_EXPECTS( + !!scEnv.caClaimID(jvb, 1), "claim id 1 still there"); + BEAST_EXPECTS( + !!scEnv.caClaimID(jvb, 2), "claim id 2 still there"); + BEAST_EXPECTS( + !!scEnv.caClaimID(jvb, 3), "claim id 3 still there"); + BEAST_EXPECTS( + scEnv.claimCount(jvb) == 0, "No account created yet"); + + scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 1)) + .close(); + txCount += 1; + + BEAST_EXPECTS( + !!scEnv.caClaimID(jvb, 3), "claim id 3 still there"); + BEAST_EXPECTS( + scEnv.claimCount(jvb) == 0, "No account created yet"); + + scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 1, 2)) + .close(); + txCount += 1; + + BEAST_EXPECTS(!scEnv.caClaimID(jvb, 1), "claim id 1 deleted"); + BEAST_EXPECTS(scEnv.claimCount(jvb) == 1, "scuAlice created"); + + scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 1, 3)) + .multiTx( + att_create_acct_vec(1, amt, scuAlice, 1, 3), + ter(tecXCHAIN_ACCOUNT_CREATE_PAST)) + .close(); + txCount += 2; + + BEAST_EXPECTS(!scEnv.caClaimID(jvb, 2), "claim id 2 deleted"); + BEAST_EXPECTS(!scEnv.caClaimID(jvb, 1), "claim id 1 not added"); + BEAST_EXPECTS( + scEnv.claimCount(jvb) == 2, "scuAlice & scuBob created"); + + scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 0)) + .close(); + txCount += 1; + + BEAST_EXPECTS(!scEnv.caClaimID(jvb, 3), "claim id 3 deleted"); + BEAST_EXPECTS( + scEnv.claimCount(jvb) == 3, "All 3 accounts created"); + + // because of the division of the rewards among attesters, + // sometimes a couple drops are left over unspent in the + // door account (here 2 drops) + BEAST_EXPECT( + multiply(amt_plus_reward, STAmount(3), xrpIssue()) + + door.diff() < + drops(3)); + BEAST_EXPECT(attester.diff() == -multiTtxFee(txCount)); + BEAST_EXPECT(scEnv.balance(scuAlice) == amt); + BEAST_EXPECT(scEnv.balance(scuBob) == amt); + BEAST_EXPECT(scEnv.balance(scuCarol) == amt); + } + } + + // If attestation moves funds, confirm the claim ledger objects are + // removed (for both account create and "regular" transactions) + // [greg] we do this in all attestation tests + + // coverage test: add_attestation transaction with incorrect flag + { + XEnv scEnv(*this, true); + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(claim_attestation( + scAttester, + jvb, + mcAlice, + XRP(1000), + payees[0], + true, + 1, + {}, + signers[0]), + txflags(tfFillOrKill), + ter(temINVALID_FLAG)) + .close(); + } + + // coverage test: add_attestation with xchain feature + // disabled + { + XEnv scEnv(*this, true); + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .disableFeature(featureXChainBridge) + .close() + .tx(claim_attestation( + scAttester, + jvb, + mcAlice, + XRP(1000), + payees[0], + true, + 1, + {}, + signers[0]), + ter(temDISABLED)) + .close(); + } + } + + void + testXChainAddClaimNonBatchAttestation() + { + using namespace jtx; + + testcase("Add Non Batch Claim Attestation"); + + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + std::uint32_t const claimID = 1; + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present + + Account const dst{scBob}; + auto const amt = XRP(1000); + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + auto const dstStartBalance = scEnv.env_.balance(dst); + + for (int i = 0; i < signers.size(); ++i) + { + auto const att = claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[i], + true, + claimID, + dst, + signers[i]); + + TER const expectedTER = + i < quorum ? tesSUCCESS : TER{tecXCHAIN_NO_CLAIM_ID}; + if (i + 1 == quorum) + scEnv.tx(att, ter(expectedTER)).close(); + else + scEnv.tx(att, ter(expectedTER)).close(); + + if (i + 1 < quorum) + BEAST_EXPECT(dstStartBalance == scEnv.env_.balance(dst)); + else + BEAST_EXPECT( + dstStartBalance + amt == scEnv.env_.balance(dst)); + } + BEAST_EXPECT(dstStartBalance + amt == scEnv.env_.balance(dst)); + } + + { + /** + * sfAttestationSignerAccount related cases. + * + * Good cases: + * --G1: master key + * --G2: regular key + * --G3: public key and non-exist (unfunded) account match + * + * Bad cases: + * --B1: disabled master key + * --B2: single item signer list + * --B3: public key and non-exist (unfunded) account mismatch + * --B4: not on signer list + * --B5: missing sfAttestationSignerAccount field + */ + + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + + for (auto i = 0; i < UT_XCHAIN_DEFAULT_NUM_SIGNERS - 2; ++i) + scEnv.fund(amt, alt_signers[i].account); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, alt_signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + Account const dst{scBob}; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + auto const dstStartBalance = scEnv.env_.balance(dst); + + { + // G1: master key + auto att = claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[0], + true, + claimID, + dst, + alt_signers[0]); + scEnv.tx(att).close(); + } + { + // G2: regular key + // alt_signers[0] is the regular key of alt_signers[1] + // There should be 2 attestations after the transaction + scEnv + .tx(jtx::regkey( + alt_signers[1].account, alt_signers[0].account)) + .close(); + auto att = claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[1], + true, + claimID, + dst, + alt_signers[0]); + att[sfAttestationSignerAccount.getJsonName()] = + alt_signers[1].account.human(); + scEnv.tx(att).close(); + } + { + // B3: public key and non-exist (unfunded) account mismatch + // G3: public key and non-exist (unfunded) account match + auto const unfundedSigner1 = + alt_signers[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 1]; + auto const unfundedSigner2 = + alt_signers[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 2]; + auto att = claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 1], + true, + claimID, + dst, + unfundedSigner1); + att[sfAttestationSignerAccount.getJsonName()] = + unfundedSigner2.account.human(); + scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR)) + .close(); + att[sfAttestationSignerAccount.getJsonName()] = + unfundedSigner1.account.human(); + scEnv.tx(att).close(); + } + { + // B2: single item signer list + std::vector tempSignerList = {signers[0]}; + scEnv.tx( + jtx::signers(alt_signers[2].account, 1, tempSignerList)); + auto att = claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[2], + true, + claimID, + dst, + tempSignerList.front()); + att[sfAttestationSignerAccount.getJsonName()] = + alt_signers[2].account.human(); + scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR)) + .close(); + } + { + // B1: disabled master key + scEnv.tx(fset(alt_signers[2].account, asfDisableMaster, 0)); + auto att = claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[2], + true, + claimID, + dst, + alt_signers[2]); + scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR)) + .close(); + } + { + // --B4: not on signer list + auto att = claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[0], + true, + claimID, + dst, + signers[0]); + scEnv.tx(att, ter(tecNO_PERMISSION)).close(); + } + { + // --B5: missing sfAttestationSignerAccount field + // Then submit the one with the field. Should rearch quorum. + auto att = claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[3], + true, + claimID, + dst, + alt_signers[3]); + att.removeMember(sfAttestationSignerAccount.getJsonName()); + scEnv.tx(att, ter(temMALFORMED)).close(); + BEAST_EXPECT(dstStartBalance == scEnv.env_.balance(dst)); + att[sfAttestationSignerAccount.getJsonName()] = + alt_signers[3].account.human(); + scEnv.tx(att).close(); + BEAST_EXPECT(dstStartBalance + amt == scEnv.env_.balance(dst)); + } + } + } + + void + testXChainAddAccountCreateNonBatchAttestation() + { + using namespace jtx; + + testcase("Add Non Batch Account Create Attestation"); + + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + XRPAmount tx_fee = mcEnv.txFee(); + + Account a{"a"}; + Account doorA{"doorA"}; + + STAmount funds{XRP(10000)}; + mcEnv.fund(funds, a); + mcEnv.fund(funds, doorA); + + Account ua{"ua"}; // unfunded account we want to create + + BridgeDef xrp_b{ + doorA, + xrpIssue(), + Account::master, + xrpIssue(), + XRP(1), // reward + XRP(20), // minAccountCreate + 4, // quorum + signers, + Json::nullValue}; + + xrp_b.initBridge(mcEnv, scEnv); + + auto const amt = XRP(777); + auto const amt_plus_reward = amt + xrp_b.reward; + { + Balance bal_doorA(mcEnv, doorA); + Balance bal_a(mcEnv, a); + + mcEnv + .tx(sidechain_xchain_account_create( + a, xrp_b.jvb, ua, amt, xrp_b.reward)) + .close(); + + BEAST_EXPECT(bal_doorA.diff() == amt_plus_reward); + BEAST_EXPECT(bal_a.diff() == -(amt_plus_reward + tx_fee)); + } + + for (int i = 0; i < signers.size(); ++i) + { + auto const att = create_account_attestation( + signers[0].account, + xrp_b.jvb, + a, + amt, + xrp_b.reward, + signers[i].account, + true, + 1, + ua, + signers[i]); + TER const expectedTER = i < xrp_b.quorum + ? tesSUCCESS + : TER{tecXCHAIN_ACCOUNT_CREATE_PAST}; + + scEnv.tx(att, ter(expectedTER)).close(); + if (i + 1 < xrp_b.quorum) + BEAST_EXPECT(!scEnv.env_.le(ua)); + else + BEAST_EXPECT(scEnv.env_.le(ua)); + } + BEAST_EXPECT(scEnv.env_.le(ua)); + } + + void + testXChainClaim() + { + using namespace jtx; + + XRPAmount res0 = reserve(0); + XRPAmount tx_fee = txFee(); + + testcase("Claim"); + + // Claim where the amount matches what is attested to, to an account + // that exists, and there are enough attestations to reach a quorum + // => should succeed + // ----------------------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + } + + BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum)); + } + + // Claim with just one attestation signed by the Master key + // => should not succeed + // ----------------------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv + .tx(create_bridge(Account::master, jvb)) + //.tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + 1, + withClaim); + + jtx::signer master_signer(Account::master); + scEnv + .tx(claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[0], + true, + claimID, + dst, + master_signer), + ter(tecXCHAIN_NO_SIGNERS_LIST)) + .close(); + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim with just one attestation signed by a regular key + // associated to the master account + // => should not succeed + // ----------------------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv + .tx(create_bridge(Account::master, jvb)) + //.tx(jtx::signers(Account::master, quorum, signers)) + .tx(jtx::regkey(Account::master, payees[0])) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + 1, + withClaim); + + jtx::signer master_signer(payees[0]); + scEnv + .tx(claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[0], + true, + claimID, + dst, + master_signer), + ter(tecXCHAIN_NO_SIGNERS_LIST)) + .close(); + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim against non-existent bridge + // --------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + auto jvb_unknown = + bridge(mcBob, xrpIssue(), Account::master, xrpIssue()); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id( + scAlice, jvb_unknown, reward, mcAlice), + ter(tecNO_ENTRY)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv + .tx(xchain_commit(mcAlice, jvb_unknown, claimID, amt, dst), + ter(tecNO_ENTRY)) + .close(); + + BalanceTransfer transfer( + scEnv, Account::master, scBob, scAlice, payees, withClaim); + scEnv + .tx(claim_attestation( + scAttester, + jvb_unknown, + mcAlice, + amt, + payees[0], + true, + claimID, + dst, + signers[0]), + ter(tecNO_ENTRY)) + .close(); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb_unknown, claimID, amt, scBob), + ter(tecNO_ENTRY)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim against non-existent claim id + // ----------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, Account::master, scBob, scAlice, payees, withClaim); + + // attest using non-existent claim id + scEnv + .tx(claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[0], + true, + 999, + dst, + signers[0]), + ter(tecXCHAIN_NO_CLAIM_ID)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // claim using non-existent claim id + scEnv + .tx(xchain_claim(scAlice, jvb, 999, amt, scBob), + ter(tecXCHAIN_NO_CLAIM_ID)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim against a claim id owned by another account + // ------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // submit a claim transaction with the wrong account (scGw + // instead of scAlice) + scEnv + .tx(xchain_claim(scGw, jvb, claimID, amt, scBob), + ter(tecXCHAIN_BAD_CLAIM_ID)) + .close(); + BEAST_EXPECT(transfer.has_not_happened()); + } + else + { + BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum)); + } + } + + // Claim against a claim id with no attestations + // --------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, Account::master, scBob, scAlice, payees, withClaim); + + // don't send any attestations + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecXCHAIN_CLAIM_NO_QUORUM)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim against a claim id with attestations, but not enough to + // make a quorum + // -------------------------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, Account::master, scBob, scAlice, payees, withClaim); + + auto tooFew = quorum - 1; + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers, + tooFew)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecXCHAIN_CLAIM_NO_QUORUM)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim id of zero + // ---------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, Account::master, scBob, scAlice, payees, withClaim); + + scEnv + .multiTx( + claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + 0, + dst, + signers), + ter(tecXCHAIN_NO_CLAIM_ID)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, 0, amt, scBob), + ter(tecXCHAIN_NO_CLAIM_ID)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim issue that does not match the expected issue on the bridge + // (either LockingChainIssue or IssuingChainIssue, depending on the + // chain). The claim id should already have enough attestations to + // reach a quorum for this amount (for a different issuer). + // --------------------------------------------------------------------- + for (auto withClaim : {true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)) + .close(); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, scUSD(1000), scBob), + ter(temBAD_AMOUNT)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim to a destination that does not already exist on the chain + // ----------------------------------------------------------------- + for (auto withClaim : {true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scuBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scuBob), + ter(tecNO_DST)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim where the claim id owner does not have enough XRP to pay + // the reward + // ------------------------------------------------------------------ + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + STAmount huge_reward{XRP(20000)}; + BEAST_EXPECT(huge_reward > scEnv.balance(scAlice)); + + scEnv.tx(create_bridge(Account::master, jvb, huge_reward)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, huge_reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + + if (withClaim) + { + scEnv + .multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)) + .close(); + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecUNFUNDED_PAYMENT)) + .close(); + } + else + { + auto txns = claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers); + for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i) + { + scEnv.tx(txns[i]).close(); + } + scEnv.tx(txns.back()); + scEnv.close(); + // The attestation should succeed, because it adds an + // attestation, but the claim should fail with insufficient + // funds + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecUNFUNDED_PAYMENT)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Claim where the claim id owner has enough XRP to pay the reward, + // but it would put his balance below the reserve + // -------------------------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .fund( + res0 + reward, + scuAlice) // just not enough because of fees + .close() + .tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice), + ter(tecINSUFFICIENT_RESERVE)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, Account::master, scBob, scuAlice, payees, withClaim); + + scEnv + .tx(claim_attestation( + scAttester, + jvb, + mcAlice, + amt, + payees[0], + true, + claimID, + dst, + signers[0]), + ter(tecXCHAIN_NO_CLAIM_ID)) + .close(); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scuAlice, jvb, claimID, amt, scBob), + ter(tecXCHAIN_NO_CLAIM_ID)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Pay to an account with deposit auth set + // --------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .tx(fset("scBob", asfDepositAuth)) // set deposit auth + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + auto txns = claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers); + for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i) + { + scEnv.tx(txns[i]).close(); + } + if (withClaim) + { + scEnv.tx(txns.back()).close(); + + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecNO_PERMISSION)) + .close(); + + // the transfer failed, but check that we can still use the + // claimID with a different account + Balance scCarol_bal(scEnv, scCarol); + + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol)) + .close(); + BEAST_EXPECT(scCarol_bal.diff() == amt); + } + else + { + scEnv.tx(txns.back()).close(); + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecNO_PERMISSION)) + .close(); + // A way would be to remove deposit auth and resubmit the + // attestations (even though the witness servers won't do + // it) + scEnv + .tx(fset("scBob", 0, asfDepositAuth)) // clear deposit auth + .close(); + + Balance scBob_bal(scEnv, scBob); + scEnv.tx(txns.back()).close(); + BEAST_EXPECT(scBob_bal.diff() == amt); + } + } + + // Pay to an account with Destination Tag set + // ------------------------------------------ + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .tx(fset("scBob", asfRequireDest)) // set dest tag + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + auto txns = claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers); + for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i) + { + scEnv.tx(txns[i]).close(); + } + if (withClaim) + { + scEnv.tx(txns.back()).close(); + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecDST_TAG_NEEDED)) + .close(); + + // the transfer failed, but check that we can still use the + // claimID with a different account + Balance scCarol_bal(scEnv, scCarol); + + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol)) + .close(); + BEAST_EXPECT(scCarol_bal.diff() == amt); + } + else + { + scEnv.tx(txns.back()).close(); + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, amt, scBob), + ter(tecDST_TAG_NEEDED)) + .close(); + // A way would be to remove the destination tag requirement + // and resubmit the attestations (even though the witness + // servers won't do it) + scEnv + .tx(fset("scBob", 0, asfRequireDest)) // clear dest tag + .close(); + + Balance scBob_bal(scEnv, scBob); + + scEnv.tx(txns.back()).close(); + BEAST_EXPECT(scBob_bal.diff() == amt); + } + } + + // Pay to an account with deposit auth set. Check that the attestations + // are still validated and that we can used the claimID to transfer the + // funds to a different account (which doesn't have deposit auth set) + // -------------------------------------------------------------------- + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .tx(fset("scBob", asfDepositAuth)) // set deposit auth + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + // we should be able to submit the attestations, but the transfer + // should not occur because dest account has deposit auth set + Balance scBob_bal(scEnv, scBob); + + scEnv.multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)); + BEAST_EXPECT(scBob_bal.diff() == STAmount(0)); + + // Check that check that we still can use the claimID to transfer + // the amount to a different account + Balance scCarol_bal(scEnv, scCarol); + + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol)).close(); + BEAST_EXPECT(scCarol_bal.diff() == amt); + } + + // Claim where the amount different from what is attested to + // --------------------------------------------------------- + for (auto withClaim : {true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + scEnv.multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)); + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // claim wrong amount + scEnv + .tx(xchain_claim(scAlice, jvb, claimID, one_xrp, scBob), + ter(tecXCHAIN_CLAIM_NO_QUORUM)) + .close(); + } + + BEAST_EXPECT(transfer.has_not_happened()); + } + + // Verify that rewards are paid from the account that owns the claim + // id + // -------------------------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + Balance scAlice_bal(scEnv, scAlice); + scEnv.multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)); + + STAmount claim_cost = reward; + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + claim_cost += tx_fee; + } + + BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum)); + BEAST_EXPECT( + scAlice_bal.diff() == -claim_cost); // because reward % 4 == 0 + } + + // Verify that if a reward is not evenly divisible among the reward + // accounts, the remaining amount goes to the claim id owner. + // ---------------------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb, tiny_reward)).close(); + + scEnv.tx(create_bridge(Account::master, jvb, tiny_reward)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, tiny_reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM, + withClaim); + Balance scAlice_bal(scEnv, scAlice); + scEnv.multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)); + STAmount claim_cost = tiny_reward; + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + claim_cost += tx_fee; + } + + BEAST_EXPECT(transfer.has_happened(amt, tiny_reward_split)); + BEAST_EXPECT( + scAlice_bal.diff() == -(claim_cost - tiny_reward_remainder)); + } + + // If a reward distribution fails for one of the reward accounts + // (the reward account doesn't exist or has deposit auth set), then + // the txn should still succeed, but that portion should go to the + // claim id owner. + // ------------------------------------------------------------------- + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + + std::vector alt_payees{payees.begin(), payees.end() - 1}; + alt_payees.back() = Account("inexistent"); + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM - 1, + withClaim); + scEnv.multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + alt_payees, + true, + claimID, + dst, + signers)); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + } + + // this also checks that only 3 * split_reward was deducted from + // scAlice (the payor account), since we passed alt_payees to + // BalanceTransfer + BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum)); + } + + for (auto withClaim : {false, true}) + { + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + mcEnv.tx(create_bridge(mcDoor, jvb)).close(); + auto& unpaid = payees[UT_XCHAIN_DEFAULT_QUORUM - 1]; + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .tx(fset(unpaid, asfDepositAuth)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + auto dst(withClaim ? std::nullopt : std::optional{scBob}); + auto const amt = XRP(1000); + std::uint32_t const claimID = 1; + mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close(); + + // balance of last signer should not change (has deposit auth) + Balance last_signer(scEnv, unpaid); + + // make sure all signers except the last one get the + // split_reward + + BalanceTransfer transfer( + scEnv, + Account::master, + scBob, + scAlice, + &payees[0], + UT_XCHAIN_DEFAULT_QUORUM - 1, + withClaim); + scEnv.multiTx(claim_attestations( + scAttester, + jvb, + mcAlice, + amt, + payees, + true, + claimID, + dst, + signers)); + + if (withClaim) + { + BEAST_EXPECT(transfer.has_not_happened()); + + // need to submit a claim transactions + scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob)) + .close(); + } + + // this also checks that only 3 * split_reward was deducted from + // scAlice (the payor account), since we passed payees.size() - + // 1 to BalanceTransfer + BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum)); + + // and make sure the account with deposit auth received nothing + BEAST_EXPECT(last_signer.diff() == STAmount(0)); + } + + // coverage test: xchain_claim transaction with incorrect flag + XEnv(*this, true) + .tx(create_bridge(Account::master, jvb)) + .close() + .tx(xchain_claim(scAlice, jvb, 1, XRP(1000), scBob), + txflags(tfFillOrKill), + ter(temINVALID_FLAG)) + .close(); + + // coverage test: xchain_claim transaction with xchain feature + // disabled + XEnv(*this, true) + .tx(create_bridge(Account::master, jvb)) + .disableFeature(featureXChainBridge) + .close() + .tx(xchain_claim(scAlice, jvb, 1, XRP(1000), scBob), + ter(temDISABLED)) + .close(); + + // coverage test: XChainClaim::preclaim - isLockingChain = true; + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .close() + .tx(xchain_claim(mcAlice, jvb, 1, XRP(1000), mcBob), + ter(tecXCHAIN_NO_CLAIM_ID)); + } + + void + testXChainCreateAccount() + { + using namespace jtx; + + testcase("Bridge Create Account"); + XRPAmount tx_fee = txFee(); + + // coverage test: transferHelper() - dst == src + { + XEnv scEnv(*this, true); + + auto const amt = XRP(111); + auto const amt_plus_reward = amt + reward; + + scEnv.tx(create_bridge(Account::master, jvb)) + .tx(jtx::signers(Account::master, quorum, signers)) + .close(); + + Balance door(scEnv, Account::master); + + // scEnv.tx(att_create_acct_batch1(1, amt, + // Account::master)).close(); + scEnv.multiTx(att_create_acct_vec(1, amt, Account::master, 2)) + .close(); + BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // claim id present + BEAST_EXPECT( + scEnv.claimCount(jvb) == 0); // claim count is one less + + // scEnv.tx(att_create_acct_batch2(1, amt, + // Account::master)).close(); + scEnv.multiTx(att_create_acct_vec(1, amt, Account::master, 2, 2)) + .close(); + BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id deleted + BEAST_EXPECT( + scEnv.claimCount(jvb) == 1); // claim count was incremented + + BEAST_EXPECT(door.diff() == -reward); + } + + // Check that creating an account with less than the minimum create + // amount fails. + { + XEnv mcEnv(*this); + + mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close(); + + Balance door(mcEnv, mcDoor); + Balance carol(mcEnv, mcCarol); + + mcEnv + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuAlice, XRP(19), reward), + ter(tecXCHAIN_INSUFF_CREATE_AMOUNT)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + BEAST_EXPECT(carol.diff() == -tx_fee); + } + + // Check that creating an account with invalid flags fails. + { + XEnv mcEnv(*this); + + mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close(); + + Balance door(mcEnv, mcDoor); + + mcEnv + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuAlice, XRP(20), reward), + txflags(tfFillOrKill), + ter(temINVALID_FLAG)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + } + + // Check that creating an account with the XChainBridge feature + // disabled fails. + { + XEnv mcEnv(*this); + + mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close(); + + Balance door(mcEnv, mcDoor); + + mcEnv.disableFeature(featureXChainBridge) + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuAlice, XRP(20), reward), + ter(temDISABLED)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + } + + // Check that creating an account with a negative amount fails + { + XEnv mcEnv(*this); + + mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close(); + + Balance door(mcEnv, mcDoor); + + mcEnv + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuAlice, XRP(-20), reward), + ter(temBAD_AMOUNT)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + } + + // Check that creating an account with a negative reward fails + { + XEnv mcEnv(*this); + + mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close(); + + Balance door(mcEnv, mcDoor); + + mcEnv + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuAlice, XRP(20), XRP(-1)), + ter(temBAD_AMOUNT)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + } + + // Check that door account can't lock funds onto itself + { + XEnv mcEnv(*this); + + mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close(); + + Balance door(mcEnv, mcDoor); + + mcEnv + .tx(sidechain_xchain_account_create( + mcDoor, jvb, scuAlice, XRP(20), XRP(1)), + ter(tecXCHAIN_SELF_COMMIT)) + .close(); + + BEAST_EXPECT(door.diff() == -tx_fee); + } + + // Check that reward matches the amount specified in bridge + { + XEnv mcEnv(*this); + + mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close(); + + Balance door(mcEnv, mcDoor); + + mcEnv + .tx(sidechain_xchain_account_create( + mcCarol, jvb, scuAlice, XRP(20), XRP(2)), + ter(tecXCHAIN_REWARD_MISMATCH)) + .close(); + + BEAST_EXPECT(door.diff() == STAmount(0)); + } + } + + void + testFeeDipsIntoReserve() + { + using namespace jtx; + XRPAmount res0 = reserve(0); + XRPAmount tx_fee = txFee(); + + testcase("Fee dips into reserve"); + + // commit where the fee dips into the reserve, this should succeed + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .fund(res0 + one_xrp + tx_fee - drops(1), mcuAlice) + .close() + .tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob), + ter(tesSUCCESS)); + + // commit where the commit amount drips into the reserve, this should + // fail + XEnv(*this) + .tx(create_bridge(mcDoor, jvb)) + .fund(res0 + one_xrp - drops(1), mcuAlice) + .close() + .tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob), + ter(tecUNFUNDED_PAYMENT)); + + auto const minAccountCreate = XRP(20); + + // account create commit where the fee dips into the reserve, + // this should succeed + XEnv(*this) + .tx(create_bridge(mcDoor, jvb, reward, minAccountCreate)) + .fund( + res0 + tx_fee + minAccountCreate + reward - drops(1), mcuAlice) + .close() + .tx(sidechain_xchain_account_create( + mcuAlice, jvb, scuAlice, minAccountCreate, reward), + ter(tesSUCCESS)); + + // account create commit where the commit dips into the reserve, + // this should fail + XEnv(*this) + .tx(create_bridge(mcDoor, jvb, reward, minAccountCreate)) + .fund(res0 + minAccountCreate + reward - drops(1), mcuAlice) + .close() + .tx(sidechain_xchain_account_create( + mcuAlice, jvb, scuAlice, minAccountCreate, reward), + ter(tecUNFUNDED_PAYMENT)); + } + + void + testXChainDeleteDoor() + { + using namespace jtx; + + testcase("Bridge Delete Door Account"); + + auto const acctDelFee{ + drops(XEnv(*this).env_.current()->fees().increment)}; + + // Deleting an account that owns bridge should fail + { + XEnv mcEnv(*this); + + mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1))).close(); + + // We don't allow an account to be deleted if its sequence + // number is within 256 of the current ledger. + for (size_t i = 0; i < 256; ++i) + mcEnv.close(); + + // try to delete mcDoor, send funds to mcAlice + mcEnv.tx( + acctdelete(mcDoor, mcAlice), + fee(acctDelFee), + ter(tecHAS_OBLIGATIONS)); + } + + // Deleting an account that owns a claim id should fail + { + XEnv scEnv(*this, true); + + scEnv.tx(create_bridge(Account::master, jvb)) + .close() + .tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)) + .close(); + + // We don't allow an account to be deleted if its sequence + // number is within 256 of the current ledger. + for (size_t i = 0; i < 256; ++i) + scEnv.close(); + + // try to delete scAlice, send funds to scBob + scEnv.tx( + acctdelete(scAlice, scBob), + fee(acctDelFee), + ter(tecHAS_OBLIGATIONS)); + } + } + + void + run() override + { + testXChainBridgeExtraFields(); + testXChainCreateBridge(); + testXChainBridgeCreateConstraints(); + testXChainCreateBridgeMatrix(); + testXChainModifyBridge(); + testXChainCreateClaimID(); + testXChainCommit(); + testXChainAddAttestation(); + testXChainAddClaimNonBatchAttestation(); + testXChainAddAccountCreateNonBatchAttestation(); + testXChainClaim(); + testXChainCreateAccount(); + testFeeDipsIntoReserve(); + testXChainDeleteDoor(); + } +}; + +// ----------------------------------------------------------- +// ----------------------------------------------------------- +struct XChainSim_test : public beast::unit_test::suite, + public jtx::XChainBridgeObjects +{ +private: + static constexpr size_t num_signers = 5; + + // -------------------------------------------------- + enum class WithClaim { no, yes }; + struct Transfer + { + jtx::Account from; + jtx::Account to; + jtx::Account finaldest; + STAmount amt; + bool a2b; // direction of transfer + WithClaim with_claim{WithClaim::no}; + uint32_t claim_id{0}; + std::array attested{}; + }; + + struct AccountCreate + { + jtx::Account from; + jtx::Account to; + STAmount amt; + STAmount reward; + bool a2b; + uint32_t claim_id{0}; + std::array attested{}; + }; + + using ENV = XEnv; + using BridgeID = BridgeDef const*; + + // tracking chain state + // -------------------- + struct AccountStateTrack + { + STAmount startAmount{0}; + STAmount expectedDiff{0}; + + void + init(ENV& env, jtx::Account const& acct) + { + startAmount = env.balance(acct); + expectedDiff = STAmount(0); + } + + bool + verify(ENV& env, jtx::Account const& acct) const + { + STAmount diff{env.balance(acct) - startAmount}; + bool check = diff == expectedDiff; + return check; + } + }; + + // -------------------------------------------------- + struct ChainStateTrack + { + using ClaimVec = jtx::JValueVec; + using CreateClaimVec = jtx::JValueVec; + using CreateClaimMap = std::map; + + ChainStateTrack(ENV& env) + : env(env), tx_fee(env.env_.current()->fees().base) + { + } + + void + sendAttestations(size_t signer_idx, BridgeID bridge, ClaimVec& claims) + { + for (auto const& c : claims) + { + env.tx(c).close(); + spendFee(bridge->signers[signer_idx].account); + } + claims.clear(); + } + + uint32_t + sendCreateAttestations( + size_t signer_idx, + BridgeID bridge, + CreateClaimVec& claims) + { + size_t num_successful = 0; + for (auto const& c : claims) + { + env.tx(c).close(); + if (env.ter() == tesSUCCESS) + { + counters[bridge].signers.push_back(signer_idx); + num_successful++; + } + spendFee(bridge->signers[signer_idx].account); + } + claims.clear(); + return num_successful; + } + + void + sendAttestations() + { + bool callback_called; + + // we have this "do {} while" loop because we want to process + // all the account create which can reach quorum at this time + // stamp. + do + { + callback_called = false; + for (size_t i = 0; i < signers_attns.size(); ++i) + { + for (auto& [bridge, claims] : signers_attns[i]) + { + sendAttestations(i, bridge, claims.xfer_claims); + + auto& c = counters[bridge]; + auto& create_claims = + claims.create_claims[c.claim_count]; + auto num_attns = create_claims.size(); + if (num_attns) + { + c.num_create_attn_sent += sendCreateAttestations( + i, bridge, create_claims); + } + assert(claims.create_claims[c.claim_count].empty()); + } + } + for (auto& [bridge, c] : counters) + { + if (c.num_create_attn_sent >= bridge->quorum) + { + callback_called = true; + c.create_callbacks[c.claim_count](c.signers); + ++c.claim_count; + c.num_create_attn_sent = 0; + c.signers.clear(); + } + } + } while (callback_called); + } + + void + init(jtx::Account const& acct) + { + accounts[acct].init(env, acct); + } + + void + receive( + jtx::Account const& acct, + STAmount amt, + std::uint64_t divisor = 1) + { + if (amt.issue() != xrpIssue()) + return; + auto it = accounts.find(acct); + if (it == accounts.end()) + { + accounts[acct].init(env, acct); + // we just looked up the account, so expectedDiff == 0 + } + else + { + it->second.expectedDiff += + (divisor == 1 ? amt + : divide( + amt, + STAmount(amt.issue(), divisor), + amt.issue())); + } + } + + void + spend(jtx::Account const& acct, STAmount amt, std::uint64_t times = 1) + { + if (amt.issue() != xrpIssue()) + return; + receive( + acct, + times == 1 + ? -amt + : -multiply( + amt, STAmount(amt.issue(), times), amt.issue())); + } + + void + transfer(jtx::Account const& from, jtx::Account const& to, STAmount amt) + { + spend(from, amt); + receive(to, amt); + } + + void + spendFee(jtx::Account const& acct, size_t times = 1) + { + spend(acct, tx_fee, times); + } + + bool + verify() const + { + for (auto const& [acct, state] : accounts) + if (!state.verify(env, acct)) + return false; + return true; + } + + struct BridgeCounters + { + using complete_cb = + std::function const& signers)>; + + uint32_t claim_id{0}; + uint32_t create_count{0}; // for account create. First should be 1 + uint32_t claim_count{ + 0}; // for account create. Increments after quorum for + // current create_count (starts at 1) is reached. + + uint32_t num_create_attn_sent{0}; // for current claim_count + std::vector signers; + std::vector create_callbacks; + }; + + struct Claims + { + ClaimVec xfer_claims; + CreateClaimMap create_claims; + }; + + using SignerAttns = std::unordered_map; + using SignersAttns = std::array; + + ENV& env; + std::map accounts; + SignersAttns signers_attns; + std::map counters; + STAmount tx_fee; + }; + + struct ChainStateTracker + { + ChainStateTracker(ENV& a_env, ENV& b_env) : a_(a_env), b_(b_env) + { + } + + bool + verify() const + { + return a_.verify() && b_.verify(); + } + + void + sendAttestations() + { + a_.sendAttestations(); + b_.sendAttestations(); + } + + void + init(jtx::Account const& acct) + { + a_.init(acct); + b_.init(acct); + } + + ChainStateTrack a_; + ChainStateTrack b_; + }; + + enum SmState { + st_initial, + st_claimid_created, + st_attesting, + st_attested, + st_completed, + st_closed, + }; + + enum Act_Flags { af_a2b = 1 << 0 }; + + // -------------------------------------------------- + template + class SmBase + { + public: + SmBase( + const std::shared_ptr& chainstate, + const BridgeDef& bridge) + : bridge_(bridge), st_(chainstate) + { + } + + ChainStateTrack& + srcState() + { + return static_cast(*this).a2b() ? st_->a_ : st_->b_; + } + + ChainStateTrack& + destState() + { + return static_cast(*this).a2b() ? st_->b_ : st_->a_; + } + + jtx::Account const& + srcDoor() + { + return static_cast(*this).a2b() ? bridge_.doorA : bridge_.doorB; + } + + jtx::Account const& + dstDoor() + { + return static_cast(*this).a2b() ? bridge_.doorB : bridge_.doorA; + } + + protected: + const BridgeDef& bridge_; + std::shared_ptr st_; + }; + + // -------------------------------------------------- + class SmCreateAccount : public SmBase + { + public: + using Base = SmBase; + + SmCreateAccount( + const std::shared_ptr& chainstate, + const BridgeDef& bridge, + AccountCreate create) + : Base(chainstate, bridge) + , sm_state(st_initial) + , cr(std::move(create)) + { + } + + bool + a2b() const + { + return cr.a2b; + } + + uint32_t + issue_account_create() + { + ChainStateTrack& st = srcState(); + jtx::Account const& srcdoor = srcDoor(); + + st.env + .tx(sidechain_xchain_account_create( + cr.from, bridge_.jvb, cr.to, cr.amt, cr.reward)) + .close(); // needed for claim_id sequence to be correct' + st.spendFee(cr.from); + st.transfer(cr.from, srcdoor, cr.amt); + st.transfer(cr.from, srcdoor, cr.reward); + + return ++st.counters[&bridge_].create_count; + } + + void + attest(uint64_t time, uint32_t rnd) + { + ChainStateTrack& st = destState(); + + // check all signers, but start at a random one + size_t i; + for (i = 0; i < num_signers; ++i) + { + size_t signer_idx = (rnd + i) % num_signers; + + if (!(cr.attested[signer_idx])) + { + // enqueue one attestation for this signer + cr.attested[signer_idx] = true; + + st.signers_attns[signer_idx][&bridge_] + .create_claims[cr.claim_id - 1] + .emplace_back(create_account_attestation( + bridge_.signers[signer_idx].account, + bridge_.jvb, + cr.from, + cr.amt, + cr.reward, + bridge_.signers[signer_idx].account, + cr.a2b, + cr.claim_id, + cr.to, + bridge_.signers[signer_idx])); + break; + } + } + + if (i == num_signers) + return; // did not attest + + auto& counters = st.counters[&bridge_]; + if (counters.create_callbacks.size() < cr.claim_id) + counters.create_callbacks.resize(cr.claim_id); + + auto complete_cb = [&](std::vector const& signers) { + auto num_attestors = signers.size(); + st.env.close(); + assert( + num_attestors <= + std::count(cr.attested.begin(), cr.attested.end(), true)); + assert(num_attestors >= bridge_.quorum); + assert(cr.claim_id - 1 == counters.claim_count); + + auto r = cr.reward; + auto reward = divide(r, STAmount(num_attestors), r.issue()); + + for (auto i : signers) + st.receive(bridge_.signers[i].account, reward); + + st.spend(dstDoor(), reward, num_attestors); + st.transfer(dstDoor(), cr.to, cr.amt); + st.env.env_.memoize(cr.to); + sm_state = st_completed; + }; + + counters.create_callbacks[cr.claim_id - 1] = std::move(complete_cb); + } + + SmState + advance(uint64_t time, uint32_t rnd) + { + switch (sm_state) + { + case st_initial: + cr.claim_id = issue_account_create(); + sm_state = st_attesting; + break; + + case st_attesting: + attest(time, rnd); + break; + + default: + assert(0); + break; + + case st_completed: + break; // will get this once + } + return sm_state; + } + + private: + SmState sm_state; + AccountCreate cr; + }; + + // -------------------------------------------------- + class SmTransfer : public SmBase + { + public: + using Base = SmBase; + + SmTransfer( + const std::shared_ptr& chainstate, + const BridgeDef& bridge, + Transfer xfer) + : Base(chainstate, bridge) + , xfer(std::move(xfer)) + , sm_state(st_initial) + { + } + + bool + a2b() const + { + return xfer.a2b; + } + + uint32_t + create_claim_id() + { + ChainStateTrack& st = destState(); + + st.env + .tx(xchain_create_claim_id( + xfer.to, bridge_.jvb, bridge_.reward, xfer.from)) + .close(); // needed for claim_id sequence to be + // correct' + st.spendFee(xfer.to); + return ++st.counters[&bridge_].claim_id; + } + + void + commit() + { + ChainStateTrack& st = srcState(); + jtx::Account const& srcdoor = srcDoor(); + + if (xfer.amt.issue() != xrpIssue()) + { + st.env.tx(pay(srcdoor, xfer.from, xfer.amt)); + st.spendFee(srcdoor); + } + st.env.tx(xchain_commit( + xfer.from, + bridge_.jvb, + xfer.claim_id, + xfer.amt, + xfer.with_claim == WithClaim::yes + ? std::nullopt + : std::optional(xfer.finaldest))); + st.spendFee(xfer.from); + st.transfer(xfer.from, srcdoor, xfer.amt); + } + + void + distribute_reward(ChainStateTrack& st) + { + auto r = bridge_.reward; + auto reward = divide(r, STAmount(bridge_.quorum), r.issue()); + + for (size_t i = 0; i < num_signers; ++i) + { + if (xfer.attested[i]) + st.receive(bridge_.signers[i].account, reward); + } + st.spend(xfer.to, reward, bridge_.quorum); + } + + bool + attest(uint64_t time, uint32_t rnd) + { + ChainStateTrack& st = destState(); + + // check all signers, but start at a random one + for (size_t i = 0; i < num_signers; ++i) + { + size_t signer_idx = (rnd + i) % num_signers; + if (!(xfer.attested[signer_idx])) + { + // enqueue one attestation for this signer + xfer.attested[signer_idx] = true; + + st.signers_attns[signer_idx][&bridge_] + .xfer_claims.emplace_back(claim_attestation( + bridge_.signers[signer_idx].account, + bridge_.jvb, + xfer.from, + xfer.amt, + bridge_.signers[signer_idx].account, + xfer.a2b, + xfer.claim_id, + xfer.with_claim == WithClaim::yes + ? std::nullopt + : std::optional(xfer.finaldest), + bridge_.signers[signer_idx])); + break; + } + } + + // return true if quorum was reached, false otherwise + bool quorum = + std::count(xfer.attested.begin(), xfer.attested.end(), true) >= + bridge_.quorum; + if (quorum && xfer.with_claim == WithClaim::no) + { + distribute_reward(st); + st.transfer(dstDoor(), xfer.finaldest, xfer.amt); + } + return quorum; + } + + void + claim() + { + ChainStateTrack& st = destState(); + st.env.tx(xchain_claim( + xfer.to, bridge_.jvb, xfer.claim_id, xfer.amt, xfer.finaldest)); + distribute_reward(st); + st.transfer(dstDoor(), xfer.finaldest, xfer.amt); + st.spendFee(xfer.to); + } + + SmState + advance(uint64_t time, uint32_t rnd) + { + switch (sm_state) + { + case st_initial: + xfer.claim_id = create_claim_id(); + sm_state = st_claimid_created; + break; + + case st_claimid_created: + commit(); + sm_state = st_attesting; + break; + + case st_attesting: + sm_state = attest(time, rnd) + ? (xfer.with_claim == WithClaim::yes ? st_attested + : st_completed) + : st_attesting; + break; + + case st_attested: + assert(xfer.with_claim == WithClaim::yes); + claim(); + sm_state = st_completed; + break; + + default: + case st_completed: + assert(0); // should have been removed + break; + } + return sm_state; + } + + private: + Transfer xfer; + SmState sm_state; + }; + + // -------------------------------------------------- + using Sm = std::variant; + using SmCont = std::list>; + + SmCont sm_; + + void + xfer( + uint64_t time, + const std::shared_ptr& chainstate, + BridgeDef const& bridge, + Transfer transfer) + { + sm_.emplace_back( + time, SmTransfer(chainstate, bridge, std::move(transfer))); + } + + void + ac(uint64_t time, + const std::shared_ptr& chainstate, + BridgeDef const& bridge, + AccountCreate ac) + { + sm_.emplace_back( + time, SmCreateAccount(chainstate, bridge, std::move(ac))); + } + +public: + void + runSimulation( + std::shared_ptr const& st, + bool verify_balances = true) + { + using namespace jtx; + uint64_t time = 0; + std::mt19937 gen(27); // Standard mersenne_twister_engine + std::uniform_int_distribution distrib(0, 9); + + while (!sm_.empty()) + { + ++time; + for (auto it = sm_.begin(); it != sm_.end();) + { + auto vis = [&](auto& sm) { + uint32_t rnd = distrib(gen); + return sm.advance(time, rnd); + }; + auto& [t, sm] = *it; + if (t <= time && std::visit(vis, sm) == st_completed) + it = sm_.erase(it); + else + ++it; + } + + // send attestations + st->sendAttestations(); + + // make sure all transactions have been applied + st->a_.env.close(); + st->b_.env.close(); + + if (verify_balances) + { + BEAST_EXPECT(st->verify()); + } + } + } + + void + testXChainSimulation() + { + using namespace jtx; + + testcase("Bridge usage simulation"); + + XEnv mcEnv(*this); + XEnv scEnv(*this, true); + + auto st = std::make_shared(mcEnv, scEnv); + + // create 10 accounts + door funded on both chains, and store + // in ChainStateTracker the initial amount of these accounts + Account doorXRPLocking, doorUSDLocking, doorUSDIssuing; + + constexpr size_t num_acct = 10; + auto a = [&doorXRPLocking, &doorUSDLocking, &doorUSDIssuing]() { + using namespace std::literals; + std::vector result; + result.reserve(num_acct); + for (int i = 0; i < num_acct; ++i) + result.emplace_back( + "a"s + std::to_string(i), + (i % 2) ? KeyType::ed25519 : KeyType::secp256k1); + result.emplace_back("doorXRPLocking"); + doorXRPLocking = result.back(); + result.emplace_back("doorUSDLocking"); + doorUSDLocking = result.back(); + result.emplace_back("doorUSDIssuing"); + doorUSDIssuing = result.back(); + return result; + }(); + + for (auto& acct : a) + { + STAmount amt{XRP(100000)}; + + mcEnv.fund(amt, acct); + scEnv.fund(amt, acct); + } + Account USDLocking{"USDLocking"}; + IOU usdLocking{USDLocking["USD"]}; + IOU usdIssuing{doorUSDIssuing["USD"]}; + + mcEnv.fund(XRP(100000), USDLocking); + mcEnv.close(); + mcEnv.tx(trust(doorUSDLocking, usdLocking(100000))); + mcEnv.close(); + mcEnv.tx(pay(USDLocking, doorUSDLocking, usdLocking(50000))); + + for (int i = 0; i < a.size(); ++i) + { + auto& acct{a[i]}; + if (i < num_acct) + { + mcEnv.tx(trust(acct, usdLocking(100000))); + scEnv.tx(trust(acct, usdIssuing(100000))); + } + st->init(acct); + } + for (auto& s : signers) + st->init(s.account); + + st->b_.init(Account::master); + + // also create some unfunded accounts + constexpr size_t num_ua = 20; + auto ua = []() { + using namespace std::literals; + std::vector result; + result.reserve(num_ua); + for (int i = 0; i < num_ua; ++i) + result.emplace_back( + "ua"s + std::to_string(i), + (i % 2) ? KeyType::ed25519 : KeyType::secp256k1); + return result; + }(); + + // initialize a bridge from a BridgeDef + auto initBridge = [&mcEnv, &scEnv, &st](BridgeDef& bd) { + bd.initBridge(mcEnv, scEnv); + st->a_.spendFee(bd.doorA, 2); + st->b_.spendFee(bd.doorB, 2); + }; + + // create XRP -> XRP bridge + // ------------------------ + BridgeDef xrp_b{ + doorXRPLocking, + xrpIssue(), + Account::master, + xrpIssue(), + XRP(1), + XRP(20), + quorum, + signers, + Json::nullValue}; + + initBridge(xrp_b); + + // create USD -> USD bridge + // ------------------------ + BridgeDef usd_b{ + doorUSDLocking, + usdLocking, + doorUSDIssuing, + usdIssuing, + XRP(1), + XRP(20), + quorum, + signers, + Json::nullValue}; + + initBridge(usd_b); + + // try a single account create + transfer to validate the simulation + // engine. Do the transfer 8 time steps after the account create, to + // give time enough for ua[0] to be funded now so it can reserve + // the claimID + // ----------------------------------------------------------------- + ac(0, st, xrp_b, {a[0], ua[0], XRP(777), xrp_b.reward, true}); + xfer(8, st, xrp_b, {a[0], ua[0], a[2], XRP(3), true}); + runSimulation(st); + + // try the same thing in the other direction + // ----------------------------------------- + ac(0, st, xrp_b, {a[0], ua[0], XRP(777), xrp_b.reward, false}); + xfer(8, st, xrp_b, {a[0], ua[0], a[2], XRP(3), false}); + runSimulation(st); + + // run multiple XRP transfers + // -------------------------- + xfer(0, st, xrp_b, {a[0], a[0], a[1], XRP(6), true, WithClaim::no}); + xfer(1, st, xrp_b, {a[0], a[0], a[1], XRP(8), false, WithClaim::no}); + xfer(1, st, xrp_b, {a[1], a[1], a[1], XRP(1), true}); + xfer(2, st, xrp_b, {a[0], a[0], a[1], XRP(3), false}); + xfer(2, st, xrp_b, {a[1], a[1], a[1], XRP(5), false}); + xfer(2, st, xrp_b, {a[0], a[0], a[1], XRP(7), false, WithClaim::no}); + xfer(2, st, xrp_b, {a[1], a[1], a[1], XRP(9), true}); + runSimulation(st); + + // run one USD transfer + // -------------------- + xfer(0, st, usd_b, {a[0], a[1], a[2], usdLocking(3), true}); + runSimulation(st); + + // run multiple USD transfers + // -------------------------- + xfer(0, st, usd_b, {a[0], a[0], a[1], usdLocking(6), true}); + xfer(1, st, usd_b, {a[0], a[0], a[1], usdIssuing(8), false}); + xfer(1, st, usd_b, {a[1], a[1], a[1], usdLocking(1), true}); + xfer(2, st, usd_b, {a[0], a[0], a[1], usdIssuing(3), false}); + xfer(2, st, usd_b, {a[1], a[1], a[1], usdIssuing(5), false}); + xfer(2, st, usd_b, {a[0], a[0], a[1], usdIssuing(7), false}); + xfer(2, st, usd_b, {a[1], a[1], a[1], usdLocking(9), true}); + runSimulation(st); + + // run mixed transfers + // ------------------- + xfer(0, st, xrp_b, {a[0], a[0], a[0], XRP(1), true}); + xfer(0, st, usd_b, {a[1], a[3], a[3], usdIssuing(3), false}); + xfer(0, st, usd_b, {a[3], a[2], a[1], usdIssuing(5), false}); + + xfer(1, st, xrp_b, {a[0], a[0], a[0], XRP(4), false}); + xfer(1, st, xrp_b, {a[1], a[1], a[0], XRP(8), true}); + xfer(1, st, usd_b, {a[4], a[1], a[1], usdLocking(7), true}); + + xfer(3, st, xrp_b, {a[1], a[1], a[0], XRP(7), true}); + xfer(3, st, xrp_b, {a[0], a[4], a[3], XRP(2), false}); + xfer(3, st, xrp_b, {a[1], a[1], a[0], XRP(9), true}); + xfer(3, st, usd_b, {a[3], a[1], a[1], usdIssuing(11), false}); + runSimulation(st); + + // run multiple account create to stress attestation batching + // ---------------------------------------------------------- + ac(0, st, xrp_b, {a[0], ua[1], XRP(301), xrp_b.reward, true}); + ac(0, st, xrp_b, {a[1], ua[2], XRP(302), xrp_b.reward, true}); + ac(1, st, xrp_b, {a[0], ua[3], XRP(303), xrp_b.reward, true}); + ac(2, st, xrp_b, {a[1], ua[4], XRP(304), xrp_b.reward, true}); + ac(3, st, xrp_b, {a[0], ua[5], XRP(305), xrp_b.reward, true}); + ac(4, st, xrp_b, {a[1], ua[6], XRP(306), xrp_b.reward, true}); + ac(6, st, xrp_b, {a[0], ua[7], XRP(307), xrp_b.reward, true}); + ac(7, st, xrp_b, {a[2], ua[8], XRP(308), xrp_b.reward, true}); + ac(9, st, xrp_b, {a[0], ua[9], XRP(309), xrp_b.reward, true}); + ac(9, st, xrp_b, {a[0], ua[9], XRP(309), xrp_b.reward, true}); + ac(10, st, xrp_b, {a[0], ua[10], XRP(310), xrp_b.reward, true}); + ac(12, st, xrp_b, {a[0], ua[11], XRP(311), xrp_b.reward, true}); + ac(12, st, xrp_b, {a[3], ua[12], XRP(312), xrp_b.reward, true}); + ac(12, st, xrp_b, {a[4], ua[13], XRP(313), xrp_b.reward, true}); + ac(12, st, xrp_b, {a[3], ua[14], XRP(314), xrp_b.reward, true}); + ac(12, st, xrp_b, {a[6], ua[15], XRP(315), xrp_b.reward, true}); + ac(13, st, xrp_b, {a[7], ua[16], XRP(316), xrp_b.reward, true}); + ac(15, st, xrp_b, {a[3], ua[17], XRP(317), xrp_b.reward, true}); + runSimulation(st, true); // balances verification working now. + } + + void + run() override + { + testXChainSimulation(); + } +}; + +BEAST_DEFINE_TESTSUITE(XChain, app, ripple); +BEAST_DEFINE_TESTSUITE(XChainSim, app, ripple); + +} // namespace ripple::test diff --git a/src/test/jtx/attester.h b/src/test/jtx/attester.h new file mode 100644 index 00000000000..7741991b752 --- /dev/null +++ b/src/test/jtx/attester.h @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_ATTESTER_H_INCLUDED +#define RIPPLE_TEST_JTX_ATTESTER_H_INCLUDED + +#include +#include + +#include +#include + +namespace ripple { + +class PublicKey; +class SecretKey; +class STXChainBridge; +class STAmount; + +namespace test { +namespace jtx { + +Buffer +sign_claim_attestation( + PublicKey const& pk, + SecretKey const& sk, + STXChainBridge const& bridge, + AccountID const& sendingAccount, + STAmount const& sendingAmount, + AccountID const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t claimID, + std::optional const& dst); + +Buffer +sign_create_account_attestation( + PublicKey const& pk, + SecretKey const& sk, + STXChainBridge const& bridge, + AccountID const& sendingAccount, + STAmount const& sendingAmount, + STAmount const& rewardAmount, + AccountID const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t createCount, + AccountID const& dst); +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/impl/attester.cpp b/src/test/jtx/impl/attester.cpp new file mode 100644 index 00000000000..dd00f536af8 --- /dev/null +++ b/src/test/jtx/impl/attester.cpp @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +Buffer +sign_claim_attestation( + PublicKey const& pk, + SecretKey const& sk, + STXChainBridge const& bridge, + AccountID const& sendingAccount, + STAmount const& sendingAmount, + AccountID const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t claimID, + std::optional const& dst) +{ + auto const toSign = Attestations::AttestationClaim::message( + bridge, + sendingAccount, + sendingAmount, + rewardAccount, + wasLockingChainSend, + claimID, + dst); + return sign(pk, sk, makeSlice(toSign)); +} + +Buffer +sign_create_account_attestation( + PublicKey const& pk, + SecretKey const& sk, + STXChainBridge const& bridge, + AccountID const& sendingAccount, + STAmount const& sendingAmount, + STAmount const& rewardAmount, + AccountID const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t createCount, + AccountID const& dst) +{ + auto const toSign = Attestations::AttestationCreateAccount::message( + bridge, + sendingAccount, + sendingAmount, + rewardAmount, + rewardAccount, + wasLockingChainSend, + createCount, + dst); + return sign(pk, sk, makeSlice(toSign)); +} + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp new file mode 100644 index 00000000000..0b81ccdcd91 --- /dev/null +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -0,0 +1,516 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +// use this for creating a bridge for a transaction +Json::Value +bridge( + Account const& lockingChainDoor, + Issue const& lockingChainIssue, + Account const& issuingChainDoor, + Issue const& issuingChainIssue) +{ + Json::Value jv; + jv[sfLockingChainDoor.getJsonName()] = lockingChainDoor.human(); + jv[sfLockingChainIssue.getJsonName()] = to_json(lockingChainIssue); + jv[sfIssuingChainDoor.getJsonName()] = issuingChainDoor.human(); + jv[sfIssuingChainIssue.getJsonName()] = to_json(issuingChainIssue); + return jv; +} + +// use this for creating a bridge for a rpc query +Json::Value +bridge_rpc( + Account const& lockingChainDoor, + Issue const& lockingChainIssue, + Account const& issuingChainDoor, + Issue const& issuingChainIssue) +{ + Json::Value jv; + jv[sfLockingChainDoor.getJsonName()] = lockingChainDoor.human(); + jv[sfLockingChainIssue.getJsonName()] = to_json(lockingChainIssue); + jv[sfIssuingChainDoor.getJsonName()] = issuingChainDoor.human(); + jv[sfIssuingChainIssue.getJsonName()] = to_json(issuingChainIssue); + return jv; +} + +Json::Value +bridge_create( + Account const& acc, + Json::Value const& bridge, + STAmount const& reward, + std::optional const& minAccountCreate) +{ + Json::Value jv; + + jv[jss::Account] = acc.human(); + jv[sfXChainBridge.getJsonName()] = bridge; + jv[sfSignatureReward.getJsonName()] = reward.getJson(JsonOptions::none); + if (minAccountCreate) + jv[sfMinAccountCreateAmount.getJsonName()] = + minAccountCreate->getJson(JsonOptions::none); + + jv[jss::TransactionType] = jss::XChainCreateBridge; + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +bridge_modify( + Account const& acc, + Json::Value const& bridge, + std::optional const& reward, + std::optional const& minAccountCreate) +{ + Json::Value jv; + + jv[jss::Account] = acc.human(); + jv[sfXChainBridge.getJsonName()] = bridge; + if (reward) + jv[sfSignatureReward.getJsonName()] = + reward->getJson(JsonOptions::none); + if (minAccountCreate) + jv[sfMinAccountCreateAmount.getJsonName()] = + minAccountCreate->getJson(JsonOptions::none); + + jv[jss::TransactionType] = jss::XChainModifyBridge; + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +xchain_create_claim_id( + Account const& acc, + Json::Value const& bridge, + STAmount const& reward, + Account const& otherChainSource) +{ + Json::Value jv; + + jv[jss::Account] = acc.human(); + jv[sfXChainBridge.getJsonName()] = bridge; + jv[sfSignatureReward.getJsonName()] = reward.getJson(JsonOptions::none); + jv[sfOtherChainSource.getJsonName()] = otherChainSource.human(); + + jv[jss::TransactionType] = jss::XChainCreateClaimID; + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +xchain_commit( + Account const& acc, + Json::Value const& bridge, + std::uint32_t claimID, + AnyAmount const& amt, + std::optional const& dst) +{ + Json::Value jv; + + jv[jss::Account] = acc.human(); + jv[sfXChainBridge.getJsonName()] = bridge; + jv[sfXChainClaimID.getJsonName()] = claimID; + jv[jss::Amount] = amt.value.getJson(JsonOptions::none); + if (dst) + jv[sfOtherChainDestination.getJsonName()] = dst->human(); + + jv[jss::TransactionType] = jss::XChainCommit; + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +xchain_claim( + Account const& acc, + Json::Value const& bridge, + std::uint32_t claimID, + AnyAmount const& amt, + Account const& dst) +{ + Json::Value jv; + + jv[sfAccount.getJsonName()] = acc.human(); + jv[sfXChainBridge.getJsonName()] = bridge; + jv[sfXChainClaimID.getJsonName()] = claimID; + jv[sfDestination.getJsonName()] = dst.human(); + jv[sfAmount.getJsonName()] = amt.value.getJson(JsonOptions::none); + + jv[jss::TransactionType] = jss::XChainClaim; + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +sidechain_xchain_account_create( + Account const& acc, + Json::Value const& bridge, + Account const& dst, + AnyAmount const& amt, + AnyAmount const& reward) +{ + Json::Value jv; + + jv[sfAccount.getJsonName()] = acc.human(); + jv[sfXChainBridge.getJsonName()] = bridge; + jv[sfDestination.getJsonName()] = dst.human(); + jv[sfAmount.getJsonName()] = amt.value.getJson(JsonOptions::none); + jv[sfSignatureReward.getJsonName()] = + reward.value.getJson(JsonOptions::none); + + jv[jss::TransactionType] = jss::XChainAccountCreateCommit; + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +claim_attestation( + jtx::Account const& submittingAccount, + Json::Value const& jvBridge, + jtx::Account const& sendingAccount, + jtx::AnyAmount const& sendingAmount, + jtx::Account const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t claimID, + std::optional const& dst, + jtx::signer const& signer) +{ + STXChainBridge const stBridge(jvBridge); + + auto const& pk = signer.account.pk(); + auto const& sk = signer.account.sk(); + auto const sig = sign_claim_attestation( + pk, + sk, + stBridge, + sendingAccount, + sendingAmount.value, + rewardAccount, + wasLockingChainSend, + claimID, + dst); + + Json::Value result; + + result[sfAccount.getJsonName()] = submittingAccount.human(); + result[sfXChainBridge.getJsonName()] = jvBridge; + + result[sfAttestationSignerAccount.getJsonName()] = signer.account.human(); + result[sfPublicKey.getJsonName()] = strHex(pk.slice()); + result[sfSignature.getJsonName()] = strHex(sig); + result[sfOtherChainSource.getJsonName()] = toBase58(sendingAccount); + result[sfAmount.getJsonName()] = + sendingAmount.value.getJson(JsonOptions::none); + result[sfAttestationRewardAccount.getJsonName()] = toBase58(rewardAccount); + result[sfWasLockingChainSend.getJsonName()] = wasLockingChainSend ? 1 : 0; + + result[sfXChainClaimID.getJsonName()] = + STUInt64{claimID}.getJson(JsonOptions::none); + if (dst) + result[sfDestination.getJsonName()] = toBase58(*dst); + + result[jss::TransactionType] = jss::XChainAddClaimAttestation; + result[jss::Flags] = tfUniversal; + + return result; +} + +Json::Value +create_account_attestation( + jtx::Account const& submittingAccount, + Json::Value const& jvBridge, + jtx::Account const& sendingAccount, + jtx::AnyAmount const& sendingAmount, + jtx::AnyAmount const& rewardAmount, + jtx::Account const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t createCount, + jtx::Account const& dst, + jtx::signer const& signer) +{ + STXChainBridge const stBridge(jvBridge); + + auto const& pk = signer.account.pk(); + auto const& sk = signer.account.sk(); + auto const sig = jtx::sign_create_account_attestation( + pk, + sk, + stBridge, + sendingAccount, + sendingAmount.value, + rewardAmount.value, + rewardAccount, + wasLockingChainSend, + createCount, + dst); + + Json::Value result; + + result[sfAccount.getJsonName()] = submittingAccount.human(); + result[sfXChainBridge.getJsonName()] = jvBridge; + + result[sfAttestationSignerAccount.getJsonName()] = signer.account.human(); + result[sfPublicKey.getJsonName()] = strHex(pk.slice()); + result[sfSignature.getJsonName()] = strHex(sig); + result[sfOtherChainSource.getJsonName()] = toBase58(sendingAccount); + result[sfAmount.getJsonName()] = + sendingAmount.value.getJson(JsonOptions::none); + result[sfAttestationRewardAccount.getJsonName()] = toBase58(rewardAccount); + result[sfWasLockingChainSend.getJsonName()] = wasLockingChainSend ? 1 : 0; + + result[sfXChainAccountCreateCount.getJsonName()] = + STUInt64{createCount}.getJson(JsonOptions::none); + result[sfDestination.getJsonName()] = toBase58(dst); + result[sfSignatureReward.getJsonName()] = + rewardAmount.value.getJson(JsonOptions::none); + + result[jss::TransactionType] = jss::XChainAddAccountCreateAttestation; + result[jss::Flags] = tfUniversal; + + return result; +} + +JValueVec +claim_attestations( + jtx::Account const& submittingAccount, + Json::Value const& jvBridge, + jtx::Account const& sendingAccount, + jtx::AnyAmount const& sendingAmount, + std::vector const& rewardAccounts, + bool wasLockingChainSend, + std::uint64_t claimID, + std::optional const& dst, + std::vector const& signers, + std::size_t const numAtts, + std::size_t const fromIdx) +{ + assert(fromIdx + numAtts <= rewardAccounts.size()); + assert(fromIdx + numAtts <= signers.size()); + JValueVec vec; + vec.reserve(numAtts); + for (auto i = fromIdx; i < fromIdx + numAtts; ++i) + vec.emplace_back(claim_attestation( + submittingAccount, + jvBridge, + sendingAccount, + sendingAmount, + rewardAccounts[i], + wasLockingChainSend, + claimID, + dst, + signers[i])); + return vec; +} + +JValueVec +create_account_attestations( + jtx::Account const& submittingAccount, + Json::Value const& jvBridge, + jtx::Account const& sendingAccount, + jtx::AnyAmount const& sendingAmount, + jtx::AnyAmount const& rewardAmount, + std::vector const& rewardAccounts, + bool wasLockingChainSend, + std::uint64_t createCount, + jtx::Account const& dst, + std::vector const& signers, + std::size_t const numAtts, + std::size_t const fromIdx) +{ + assert(fromIdx + numAtts <= rewardAccounts.size()); + assert(fromIdx + numAtts <= signers.size()); + JValueVec vec; + vec.reserve(numAtts); + for (auto i = fromIdx; i < fromIdx + numAtts; ++i) + vec.emplace_back(create_account_attestation( + submittingAccount, + jvBridge, + sendingAccount, + sendingAmount, + rewardAmount, + rewardAccounts[i], + wasLockingChainSend, + createCount, + dst, + signers[i])); + return vec; +} + +XChainBridgeObjects::XChainBridgeObjects() + : mcDoor("mcDoor") + , mcAlice("mcAlice") + , mcBob("mcBob") + , mcCarol("mcCarol") + , mcGw("mcGw") + , scDoor("scDoor") + , scAlice("scAlice") + , scBob("scBob") + , scCarol("scCarol") + , scGw("scGw") + , scAttester("scAttester") + , scReward("scReward") + , mcuDoor("mcuDoor") + , mcuAlice("mcuAlice") + , mcuBob("mcuBob") + , mcuCarol("mcuCarol") + , mcuGw("mcuGw") + , scuDoor("scuDoor") + , scuAlice("scuAlice") + , scuBob("scuBob") + , scuCarol("scuCarol") + , scuGw("scuGw") + , mcUSD(mcGw["USD"]) + , scUSD(scGw["USD"]) + , jvXRPBridgeRPC( + bridge_rpc(mcDoor, xrpIssue(), Account::master, xrpIssue())) + , jvb(bridge(mcDoor, xrpIssue(), Account::master, xrpIssue())) + , jvub(bridge(mcuDoor, xrpIssue(), Account::master, xrpIssue())) + , features(supported_amendments() | FeatureBitset{featureXChainBridge}) + , signers([] { + constexpr int numSigners = UT_XCHAIN_DEFAULT_NUM_SIGNERS; + std::vector result; + result.reserve(numSigners); + for (int i = 0; i < numSigners; ++i) + { + using namespace std::literals; + auto const a = Account( + "signer_"s + std::to_string(i), + (i % 2) ? KeyType::ed25519 : KeyType::secp256k1); + result.emplace_back(a); + } + return result; + }()) + , alt_signers([] { + constexpr int numSigners = UT_XCHAIN_DEFAULT_NUM_SIGNERS; + std::vector result; + result.reserve(numSigners); + for (int i = 0; i < numSigners; ++i) + { + using namespace std::literals; + auto const a = Account( + "alt_signer_"s + std::to_string(i), + (i % 2) ? KeyType::ed25519 : KeyType::secp256k1); + result.emplace_back(a); + } + return result; + }()) + , payee([&] { + std::vector r; + r.reserve(signers.size()); + for (int i = 0, e = signers.size(); i != e; ++i) + { + r.push_back(scReward); + } + return r; + }()) + , payees([&] { + std::vector r; + r.reserve(signers.size()); + for (int i = 0, e = signers.size(); i != e; ++i) + { + using namespace std::literals; + auto const a = Account("reward_"s + std::to_string(i)); + r.push_back(a); + } + return r; + }()) + , quorum(UT_XCHAIN_DEFAULT_QUORUM) + , reward(XRP(1)) + , split_reward_quorum( + divide(reward, STAmount(UT_XCHAIN_DEFAULT_QUORUM), reward.issue())) + , split_reward_everyone(divide( + reward, + STAmount(UT_XCHAIN_DEFAULT_NUM_SIGNERS), + reward.issue())) + , tiny_reward(drops(37)) + , tiny_reward_split((divide( + tiny_reward, + STAmount(UT_XCHAIN_DEFAULT_QUORUM), + tiny_reward.issue()))) + , tiny_reward_remainder( + tiny_reward - + multiply( + tiny_reward_split, + STAmount(UT_XCHAIN_DEFAULT_QUORUM), + tiny_reward.issue())) + , one_xrp(XRP(1)) + , xrp_dust(divide(one_xrp, STAmount(10000), one_xrp.issue())) +{ +} + +void +XChainBridgeObjects::createMcBridgeObjects(Env& mcEnv) +{ + STAmount xrp_funds{XRP(10000)}; + mcEnv.fund(xrp_funds, mcDoor, mcAlice, mcBob, mcCarol, mcGw); + + // Signer's list must match the attestation signers + mcEnv(jtx::signers(mcDoor, signers.size(), signers)); + + // create XRP bridges in both direction + auto const reward = XRP(1); + STAmount const minCreate = XRP(20); + + mcEnv(bridge_create(mcDoor, jvb, reward, minCreate)); + mcEnv.close(); +} + +void +XChainBridgeObjects::createScBridgeObjects(Env& scEnv) +{ + STAmount xrp_funds{XRP(10000)}; + scEnv.fund( + xrp_funds, scDoor, scAlice, scBob, scCarol, scGw, scAttester, scReward); + + // Signer's list must match the attestation signers + scEnv(jtx::signers(Account::master, signers.size(), signers)); + + // create XRP bridges in both direction + auto const reward = XRP(1); + STAmount const minCreate = XRP(20); + + scEnv(bridge_create(Account::master, jvb, reward, minCreate)); + scEnv.close(); +} + +void +XChainBridgeObjects::createBridgeObjects(Env& mcEnv, Env& scEnv) +{ + createMcBridgeObjects(mcEnv); + createScBridgeObjects(scEnv); +} +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/xchain_bridge.h b/src/test/jtx/xchain_bridge.h new file mode 100644 index 00000000000..8a398bc6f20 --- /dev/null +++ b/src/test/jtx/xchain_bridge.h @@ -0,0 +1,260 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_XCHAINBRIDGE_H_INCLUDED +#define RIPPLE_TEST_JTX_XCHAINBRIDGE_H_INCLUDED + +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +using JValueVec = std::vector; + +constexpr std::size_t UT_XCHAIN_DEFAULT_NUM_SIGNERS = 5; +constexpr std::size_t UT_XCHAIN_DEFAULT_QUORUM = 4; + +Json::Value +bridge( + Account const& lockingChainDoor, + Issue const& lockingChainIssue, + Account const& issuingChainDoor, + Issue const& issuingChainIssue); + +Json::Value +bridge_create( + Account const& acc, + Json::Value const& bridge, + STAmount const& reward, + std::optional const& minAccountCreate = std::nullopt); + +Json::Value +bridge_modify( + Account const& acc, + Json::Value const& bridge, + std::optional const& reward, + std::optional const& minAccountCreate = std::nullopt); + +Json::Value +xchain_create_claim_id( + Account const& acc, + Json::Value const& bridge, + STAmount const& reward, + Account const& otherChainSource); + +Json::Value +xchain_commit( + Account const& acc, + Json::Value const& bridge, + std::uint32_t claimID, + AnyAmount const& amt, + std::optional const& dst = std::nullopt); + +Json::Value +xchain_claim( + Account const& acc, + Json::Value const& bridge, + std::uint32_t claimID, + AnyAmount const& amt, + Account const& dst); + +Json::Value +sidechain_xchain_account_create( + Account const& acc, + Json::Value const& bridge, + Account const& dst, + AnyAmount const& amt, + AnyAmount const& xChainFee); + +Json::Value +sidechain_xchain_account_claim( + Account const& acc, + Json::Value const& bridge, + Account const& dst, + AnyAmount const& amt); + +Json::Value +claim_attestation( + jtx::Account const& submittingAccount, + Json::Value const& jvBridge, + jtx::Account const& sendingAccount, + jtx::AnyAmount const& sendingAmount, + jtx::Account const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t claimID, + std::optional const& dst, + jtx::signer const& signer); + +Json::Value +create_account_attestation( + jtx::Account const& submittingAccount, + Json::Value const& jvBridge, + jtx::Account const& sendingAccount, + jtx::AnyAmount const& sendingAmount, + jtx::AnyAmount const& rewardAmount, + jtx::Account const& rewardAccount, + bool wasLockingChainSend, + std::uint64_t createCount, + jtx::Account const& dst, + jtx::signer const& signer); + +JValueVec +claim_attestations( + jtx::Account const& submittingAccount, + Json::Value const& jvBridge, + jtx::Account const& sendingAccount, + jtx::AnyAmount const& sendingAmount, + std::vector const& rewardAccounts, + bool wasLockingChainSend, + std::uint64_t claimID, + std::optional const& dst, + std::vector const& signers, + std::size_t const numAtts = UT_XCHAIN_DEFAULT_QUORUM, + std::size_t const fromIdx = 0); + +JValueVec +create_account_attestations( + jtx::Account const& submittingAccount, + Json::Value const& jvBridge, + jtx::Account const& sendingAccount, + jtx::AnyAmount const& sendingAmount, + jtx::AnyAmount const& rewardAmount, + std::vector const& rewardAccounts, + bool wasLockingChainSend, + std::uint64_t createCount, + jtx::Account const& dst, + std::vector const& signers, + std::size_t const numAtts = UT_XCHAIN_DEFAULT_QUORUM, + std::size_t const fromIdx = 0); + +struct XChainBridgeObjects +{ + // funded accounts + Account const mcDoor; + Account const mcAlice; + Account const mcBob; + Account const mcCarol; + Account const mcGw; + Account const scDoor; + Account const scAlice; + Account const scBob; + Account const scCarol; + Account const scGw; + Account const scAttester; + Account const scReward; + + // unfunded accounts + Account const mcuDoor; + Account const mcuAlice; + Account const mcuBob; + Account const mcuCarol; + Account const mcuGw; + Account const scuDoor; + Account const scuAlice; + Account const scuBob; + Account const scuCarol; + Account const scuGw; + + IOU const mcUSD; + IOU const scUSD; + + Json::Value const jvXRPBridgeRPC; + Json::Value jvb; // standard xrp bridge def for tx + Json::Value jvub; // standard xrp bridge def for tx, unfunded accounts + + FeatureBitset const features; + std::vector const signers; + std::vector const alt_signers; + std::vector const payee; + std::vector const payees; + std::uint32_t const quorum; + + STAmount const reward; // 1 xrp + STAmount const split_reward_quorum; // 250,000 drops + STAmount const split_reward_everyone; // 200,000 drops + + const STAmount tiny_reward; // 37 drops + const STAmount tiny_reward_split; // 9 drops + const STAmount tiny_reward_remainder; // 1 drops + + const STAmount one_xrp; + const STAmount xrp_dust; + + static constexpr int drop_per_xrp = 1000000; + + XChainBridgeObjects(); + + void + createMcBridgeObjects(Env& mcEnv); + + void + createScBridgeObjects(Env& scEnv); + + void + createBridgeObjects(Env& mcEnv, Env& scEnv); + + JValueVec + att_create_acct_vec( + std::uint64_t createCount, + jtx::AnyAmount const& amt, + jtx::Account const& dst, + std::size_t const numAtts, + std::size_t const fromIdx = 0) + { + return create_account_attestations( + scAttester, + jvb, + mcCarol, + amt, + reward, + payees, + true, + createCount, + dst, + signers, + numAtts, + fromIdx); + } + + Json::Value + create_bridge( + Account const& acc, + Json::Value const& bridge = Json::nullValue, + STAmount const& _reward = XRP(1), + std::optional const& minAccountCreate = std::nullopt) + { + return bridge_create( + acc, + bridge == Json::nullValue ? jvb : bridge, + _reward, + minAccountCreate); + } +}; + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 90d4e54684f..e38c7c029b7 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include @@ -550,7 +551,9 @@ class AccountObjects_test : public beast::unit_test::suite Account const gw{"gateway"}; auto const USD = gw["USD"]; - Env env(*this); + auto const features = + supported_amendments() | FeatureBitset{featureXChainBridge}; + Env env(*this, features); // Make a lambda we can use to get "account_objects" easily. auto acct_objs = [&env]( @@ -676,6 +679,142 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(escrow[sfDestination.jsonName] == gw.human()); BEAST_EXPECT(escrow[sfAmount.jsonName].asUInt() == 100'000'000); } + { + // Create a bridge + test::jtx::XChainBridgeObjects x; + Env scEnv(*this, envconfig(port_increment, 3), features); + x.createScBridgeObjects(scEnv); + + auto scenv_acct_objs = [&](Account const& acct, char const* type) { + Json::Value params; + params[jss::account] = acct.human(); + params[jss::type] = type; + params[jss::ledger_index] = "validated"; + return scEnv.rpc("json", "account_objects", to_string(params)); + }; + + Json::Value const resp = + scenv_acct_objs(Account::master, jss::bridge); + + BEAST_EXPECT(acct_objs_is_size(resp, 1)); + auto const& acct_bridge = + resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT( + acct_bridge[sfAccount.jsonName] == Account::master.human()); + BEAST_EXPECT( + acct_bridge[sfLedgerEntryType.getJsonName()] == "Bridge"); + BEAST_EXPECT( + acct_bridge[sfXChainClaimID.getJsonName()].asUInt() == 0); + BEAST_EXPECT( + acct_bridge[sfXChainAccountClaimCount.getJsonName()].asUInt() == + 0); + BEAST_EXPECT( + acct_bridge[sfXChainAccountCreateCount.getJsonName()] + .asUInt() == 0); + BEAST_EXPECT( + acct_bridge[sfMinAccountCreateAmount.getJsonName()].asUInt() == + 20000000); + BEAST_EXPECT( + acct_bridge[sfSignatureReward.getJsonName()].asUInt() == + 1000000); + BEAST_EXPECT(acct_bridge[sfXChainBridge.getJsonName()] == x.jvb); + } + { + // Alice and Bob create a xchain sequence number that we can look + // for in the ledger. + test::jtx::XChainBridgeObjects x; + Env scEnv(*this, envconfig(port_increment, 3), features); + x.createScBridgeObjects(scEnv); + + scEnv( + xchain_create_claim_id(x.scAlice, x.jvb, x.reward, x.mcAlice)); + scEnv.close(); + scEnv(xchain_create_claim_id(x.scBob, x.jvb, x.reward, x.mcBob)); + scEnv.close(); + + auto scenv_acct_objs = [&](Account const& acct, char const* type) { + Json::Value params; + params[jss::account] = acct.human(); + params[jss::type] = type; + params[jss::ledger_index] = "validated"; + return scEnv.rpc("json", "account_objects", to_string(params)); + }; + + { + // Find the xchain sequence number for Andrea. + Json::Value const resp = + scenv_acct_objs(x.scAlice, jss::xchain_owned_claim_id); + BEAST_EXPECT(acct_objs_is_size(resp, 1)); + + auto const& xchain_seq = + resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT( + xchain_seq[sfAccount.jsonName] == x.scAlice.human()); + BEAST_EXPECT( + xchain_seq[sfXChainClaimID.getJsonName()].asUInt() == 1); + } + { + // and the one for Bob + Json::Value const resp = + scenv_acct_objs(x.scBob, jss::xchain_owned_claim_id); + BEAST_EXPECT(acct_objs_is_size(resp, 1)); + + auto const& xchain_seq = + resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT(xchain_seq[sfAccount.jsonName] == x.scBob.human()); + BEAST_EXPECT( + xchain_seq[sfXChainClaimID.getJsonName()].asUInt() == 2); + } + } + { + test::jtx::XChainBridgeObjects x; + Env scEnv(*this, envconfig(port_increment, 3), features); + x.createScBridgeObjects(scEnv); + auto const amt = XRP(1000); + + // send first batch of account create attestations, so the + // xchain_create_account_claim_id should be present on the door + // account (Account::master) to collect the signatures until a + // quorum is reached + scEnv(test::jtx::create_account_attestation( + x.scAttester, + x.jvb, + x.mcCarol, + amt, + x.reward, + x.payees[0], + true, + 1, + x.scuAlice, + x.signers[0])); + scEnv.close(); + + auto scenv_acct_objs = [&](Account const& acct, char const* type) { + Json::Value params; + params[jss::account] = acct.human(); + params[jss::type] = type; + params[jss::ledger_index] = "validated"; + return scEnv.rpc("json", "account_objects", to_string(params)); + }; + + { + // Find the xchain_create_account_claim_id + Json::Value const resp = scenv_acct_objs( + Account::master, jss::xchain_owned_create_account_claim_id); + BEAST_EXPECT(acct_objs_is_size(resp, 1)); + + auto const& xchain_create_account_claim_id = + resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT( + xchain_create_account_claim_id[sfAccount.jsonName] == + Account::master.human()); + BEAST_EXPECT( + xchain_create_account_claim_id[sfXChainAccountCreateCount + .getJsonName()] + .asUInt() == 1); + } + } + // gw creates an offer that we can look for in the ledger. env(offer(gw, USD(7), XRP(14))); env.close(); @@ -690,7 +829,8 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT(offer[sfTakerPays.jsonName][jss::value].asUInt() == 7); } { - // Create a payment channel from qw to alice that we can look for. + // Create a payment channel from qw to alice that we can look + // for. Json::Value jvPayChan; jvPayChan[jss::TransactionType] = jss::PaymentChannelCreate; jvPayChan[jss::Flags] = tfUniversal; @@ -715,7 +855,7 @@ class AccountObjects_test : public beast::unit_test::suite payChan[sfSettleDelay.jsonName].asUInt() == 24 * 60 * 60); } // Make gw multisigning by adding a signerList. - env(signers(gw, 6, {{alice, 7}})); + env(jtx::signers(gw, 6, {{alice, 7}})); env.close(); { // Find the signer list. diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 905af6aceb2..960ac3a86ee 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -21,12 +21,331 @@ #include #include #include +#include #include +#include #include #include +#include +#include +#include namespace ripple { +class LedgerRPC_XChain_test : public beast::unit_test::suite, + public test::jtx::XChainBridgeObjects +{ + void + checkErrorValue( + Json::Value const& jv, + std::string const& err, + std::string const& msg) + { + if (BEAST_EXPECT(jv.isMember(jss::status))) + BEAST_EXPECT(jv[jss::status] == "error"); + if (BEAST_EXPECT(jv.isMember(jss::error))) + BEAST_EXPECT(jv[jss::error] == err); + if (msg.empty()) + { + BEAST_EXPECT( + jv[jss::error_message] == Json::nullValue || + jv[jss::error_message] == ""); + } + else if (BEAST_EXPECT(jv.isMember(jss::error_message))) + BEAST_EXPECT(jv[jss::error_message] == msg); + } + + void + testLedgerEntryBridge() + { + testcase("ledger_entry: bridge"); + using namespace test::jtx; + + Env mcEnv{*this, features}; + Env scEnv(*this, envconfig(port_increment, 3), features); + + createBridgeObjects(mcEnv, scEnv); + + std::string const ledgerHash{to_string(mcEnv.closed()->info().hash)}; + std::string bridge_index; + Json::Value mcBridge; + { + // request the bridge via RPC + Json::Value jvParams; + jvParams[jss::bridge_account] = mcDoor.human(); + jvParams[jss::bridge] = jvb; + Json::Value const jrr = mcEnv.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + BEAST_EXPECT(jrr.isMember(jss::node)); + auto r = jrr[jss::node]; + // std::cout << to_string(r) << '\n'; + + BEAST_EXPECT(r.isMember(jss::Account)); + BEAST_EXPECT(r[jss::Account] == mcDoor.human()); + + BEAST_EXPECT(r.isMember(jss::Flags)); + + BEAST_EXPECT(r.isMember(sfLedgerEntryType.jsonName)); + BEAST_EXPECT(r[sfLedgerEntryType.jsonName] == jss::Bridge); + + // we not created an account yet + BEAST_EXPECT(r.isMember(sfXChainAccountCreateCount.jsonName)); + BEAST_EXPECT(r[sfXChainAccountCreateCount.jsonName].asInt() == 0); + + // we have not claimed a locking chain tx yet + BEAST_EXPECT(r.isMember(sfXChainAccountClaimCount.jsonName)); + BEAST_EXPECT(r[sfXChainAccountClaimCount.jsonName].asInt() == 0); + + BEAST_EXPECT(r.isMember(jss::index)); + bridge_index = r[jss::index].asString(); + mcBridge = r; + } + { + // request the bridge via RPC by index + Json::Value jvParams; + jvParams[jss::index] = bridge_index; + Json::Value const jrr = mcEnv.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + BEAST_EXPECT(jrr.isMember(jss::node)); + BEAST_EXPECT(jrr[jss::node] == mcBridge); + } + { + // swap door accounts and make sure we get an error value + Json::Value jvParams; + // Sidechain door account is "master", not scDoor + jvParams[jss::bridge_account] = Account::master.human(); + jvParams[jss::bridge] = jvb; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = mcEnv.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + checkErrorValue(jrr, "entryNotFound", ""); + } + { + // create two claim ids and verify that the bridge counter was + // incremented + mcEnv(xchain_create_claim_id(mcAlice, jvb, reward, scAlice)); + mcEnv.close(); + mcEnv(xchain_create_claim_id(mcBob, jvb, reward, scBob)); + mcEnv.close(); + + // request the bridge via RPC + Json::Value jvParams; + jvParams[jss::bridge_account] = mcDoor.human(); + jvParams[jss::bridge] = jvb; + // std::cout << to_string(jvParams) << '\n'; + Json::Value const jrr = mcEnv.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + BEAST_EXPECT(jrr.isMember(jss::node)); + auto r = jrr[jss::node]; + + // we executed two create claim id txs + BEAST_EXPECT(r.isMember(sfXChainClaimID.jsonName)); + BEAST_EXPECT(r[sfXChainClaimID.jsonName].asInt() == 2); + } + } + + void + testLedgerEntryClaimID() + { + testcase("ledger_entry: xchain_claim_id"); + using namespace test::jtx; + + Env mcEnv{*this, features}; + Env scEnv(*this, envconfig(port_increment, 3), features); + + createBridgeObjects(mcEnv, scEnv); + + scEnv(xchain_create_claim_id(scAlice, jvb, reward, mcAlice)); + scEnv.close(); + scEnv(xchain_create_claim_id(scBob, jvb, reward, mcBob)); + scEnv.close(); + + std::string bridge_index; + { + // request the xchain_claim_id via RPC + Json::Value jvParams; + jvParams[jss::xchain_owned_claim_id] = jvXRPBridgeRPC; + jvParams[jss::xchain_owned_claim_id][jss::xchain_owned_claim_id] = + 1; + // std::cout << to_string(jvParams) << '\n'; + Json::Value const jrr = scEnv.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + BEAST_EXPECT(jrr.isMember(jss::node)); + auto r = jrr[jss::node]; + // std::cout << to_string(r) << '\n'; + + BEAST_EXPECT(r.isMember(jss::Account)); + BEAST_EXPECT(r[jss::Account] == scAlice.human()); + BEAST_EXPECT( + r[sfLedgerEntryType.jsonName] == jss::XChainOwnedClaimID); + BEAST_EXPECT(r[sfXChainClaimID.jsonName].asInt() == 1); + BEAST_EXPECT(r[sfOwnerNode.jsonName].asInt() == 0); + } + + { + // request the xchain_claim_id via RPC + Json::Value jvParams; + jvParams[jss::xchain_owned_claim_id] = jvXRPBridgeRPC; + jvParams[jss::xchain_owned_claim_id][jss::xchain_owned_claim_id] = + 2; + Json::Value const jrr = scEnv.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + BEAST_EXPECT(jrr.isMember(jss::node)); + auto r = jrr[jss::node]; + // std::cout << to_string(r) << '\n'; + + BEAST_EXPECT(r.isMember(jss::Account)); + BEAST_EXPECT(r[jss::Account] == scBob.human()); + BEAST_EXPECT( + r[sfLedgerEntryType.jsonName] == jss::XChainOwnedClaimID); + BEAST_EXPECT(r[sfXChainClaimID.jsonName].asInt() == 2); + BEAST_EXPECT(r[sfOwnerNode.jsonName].asInt() == 0); + } + } + + void + testLedgerEntryCreateAccountClaimID() + { + testcase("ledger_entry: xchain_create_account_claim_id"); + using namespace test::jtx; + + Env mcEnv{*this, features}; + Env scEnv(*this, envconfig(port_increment, 3), features); + + // note: signers.size() and quorum are both 5 in createBridgeObjects + createBridgeObjects(mcEnv, scEnv); + + auto scCarol = + Account("scCarol"); // Don't fund it - it will be created with the + // xchain transaction + auto const amt = XRP(1000); + mcEnv(sidechain_xchain_account_create( + mcAlice, jvb, scCarol, amt, reward)); + mcEnv.close(); + + // send less than quorum of attestations (otherwise funds are + // immediately transferred and no "claim" object is created) + size_t constexpr num_attest = 3; + auto attestations = create_account_attestations( + scAttester, + jvb, + mcAlice, + amt, + reward, + payee, + /*wasLockingChainSend*/ true, + 1, + scCarol, + signers, + UT_XCHAIN_DEFAULT_NUM_SIGNERS); + for (size_t i = 0; i < num_attest; ++i) + { + scEnv(attestations[i]); + } + scEnv.close(); + + { + // request the create account claim_id via RPC + Json::Value jvParams; + jvParams[jss::xchain_owned_create_account_claim_id] = + jvXRPBridgeRPC; + jvParams[jss::xchain_owned_create_account_claim_id] + [jss::xchain_owned_create_account_claim_id] = 1; + // std::cout << to_string(jvParams) << '\n'; + Json::Value const jrr = scEnv.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + // std::cout << to_string(jrr) << '\n'; + + BEAST_EXPECT(jrr.isMember(jss::node)); + auto r = jrr[jss::node]; + + BEAST_EXPECT(r.isMember(jss::Account)); + BEAST_EXPECT(r[jss::Account] == Account::master.human()); + + BEAST_EXPECT(r.isMember(sfXChainAccountCreateCount.jsonName)); + BEAST_EXPECT(r[sfXChainAccountCreateCount.jsonName].asInt() == 1); + + BEAST_EXPECT( + r.isMember(sfXChainCreateAccountAttestations.jsonName)); + auto attest = r[sfXChainCreateAccountAttestations.jsonName]; + BEAST_EXPECT(attest.isArray()); + BEAST_EXPECT(attest.size() == 3); + BEAST_EXPECT(attest[Json::Value::UInt(0)].isMember( + sfXChainCreateAccountProofSig.jsonName)); + Json::Value a[num_attest]; + for (size_t i = 0; i < num_attest; ++i) + { + a[i] = attest[Json::Value::UInt(0)] + [sfXChainCreateAccountProofSig.jsonName]; + BEAST_EXPECT( + a[i].isMember(jss::Amount) && + a[i][jss::Amount].asInt() == 1000 * drop_per_xrp); + BEAST_EXPECT( + a[i].isMember(jss::Destination) && + a[i][jss::Destination] == scCarol.human()); + BEAST_EXPECT( + a[i].isMember(sfAttestationSignerAccount.jsonName) && + std::any_of( + signers.begin(), signers.end(), [&](signer const& s) { + return a[i][sfAttestationSignerAccount.jsonName] == + s.account.human(); + })); + BEAST_EXPECT( + a[i].isMember(sfAttestationRewardAccount.jsonName) && + std::any_of( + payee.begin(), + payee.end(), + [&](Account const& account) { + return a[i][sfAttestationRewardAccount.jsonName] == + account.human(); + })); + BEAST_EXPECT( + a[i].isMember(sfWasLockingChainSend.jsonName) && + a[i][sfWasLockingChainSend.jsonName] == 1); + BEAST_EXPECT( + a[i].isMember(sfSignatureReward.jsonName) && + a[i][sfSignatureReward.jsonName].asInt() == + 1 * drop_per_xrp); + } + } + + // complete attestations quorum - CreateAccountClaimID should not be + // present anymore + for (size_t i = num_attest; i < UT_XCHAIN_DEFAULT_NUM_SIGNERS; ++i) + { + scEnv(attestations[i]); + } + scEnv.close(); + { + // request the create account claim_id via RPC + Json::Value jvParams; + jvParams[jss::xchain_owned_create_account_claim_id] = + jvXRPBridgeRPC; + jvParams[jss::xchain_owned_create_account_claim_id] + [jss::xchain_owned_create_account_claim_id] = 1; + // std::cout << to_string(jvParams) << '\n'; + Json::Value const jrr = scEnv.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + } + +public: + void + run() override + { + testLedgerEntryBridge(); + testLedgerEntryClaimID(); + testLedgerEntryCreateAccountClaimID(); + } +}; + class LedgerRPC_test : public beast::unit_test::suite { void @@ -1940,5 +2259,6 @@ class LedgerRPC_test : public beast::unit_test::suite }; BEAST_DEFINE_TESTSUITE(LedgerRPC, app, ripple); +BEAST_DEFINE_TESTSUITE(LedgerRPC_XChain, app, ripple); } // namespace ripple diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 3bb0cce611c..5322319907f 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -25,6 +25,7 @@ #include #include #include +#include namespace ripple { namespace test { @@ -743,7 +744,7 @@ class Subscribe_test : public beast::unit_test::suite using namespace std::chrono_literals; using namespace jtx; - using IdxHashVec = std::vector>; + using IdxHashVec = std::vector>; Account alice("alice"); Account bob("bob"); @@ -781,11 +782,14 @@ class Subscribe_test : public beast::unit_test::suite idx = r[jss::account_history_tx_index].asInt(); if (r.isMember(jss::account_history_tx_first)) first_flag = true; + bool boundary = r.isMember(jss::account_history_boundary); + int ledger_idx = r[jss::ledger_index].asInt(); if (r.isMember(jss::transaction) && r[jss::transaction].isMember(jss::hash)) { + auto t{r[jss::transaction]}; v.emplace_back( - idx, r[jss::transaction][jss::hash].asString()); + idx, t[jss::hash].asString(), boundary, ledger_idx); continue; } } @@ -838,13 +842,13 @@ class Subscribe_test : public beast::unit_test::suite hash_map txHistoryMap; for (auto const& tx : txHistoryVec) { - txHistoryMap.emplace(tx.second, tx.first); + txHistoryMap.emplace(std::get<1>(tx), std::get<0>(tx)); } auto getHistoryIndex = [&](std::size_t i) -> std::optional { if (i >= accountVec.size()) return {}; - auto it = txHistoryMap.find(accountVec[i].second); + auto it = txHistoryMap.find(std::get<1>(accountVec[i])); if (it == txHistoryMap.end()) return {}; return it->second; @@ -862,6 +866,48 @@ class Subscribe_test : public beast::unit_test::suite return true; }; + // example of vector created from the return of `subscribe` rpc + // with jss::accounts + // boundary == true on last tx of ledger + // ------------------------------------------------------------ + // (0, "E5B8B...", false, 4 + // (0, "39E1C...", false, 4 + // (0, "14EF1...", false, 4 + // (0, "386E6...", false, 4 + // (0, "00F3B...", true, 4 + // (0, "1DCDC...", false, 5 + // (0, "BD02A...", false, 5 + // (0, "D3E16...", false, 5 + // (0, "CB593...", false, 5 + // (0, "8F28B...", true, 5 + // + // example of vector created from the return of `subscribe` rpc + // with jss::account_history_tx_stream. + // boundary == true on first tx of ledger + // ------------------------------------------------------------ + // (-1, "8F28B...", false, 5 + // (-2, "CB593...", false, 5 + // (-3, "D3E16...", false, 5 + // (-4, "BD02A...", false, 5 + // (-5, "1DCDC...", true, 5 + // (-6, "00F3B...", false, 4 + // (-7, "386E6...", false, 4 + // (-8, "14EF1...", false, 4 + // (-9, "39E1C...", false, 4 + // (-10, "E5B8B...", true, 4 + + auto checkBoundary = [](IdxHashVec const& vec, bool /* forward */) { + size_t num_tx = vec.size(); + for (size_t i = 0; i < num_tx; ++i) + { + auto [idx, hash, boundary, ledger] = vec[i]; + if ((i + 1 == num_tx || ledger != std::get<3>(vec[i + 1])) != + boundary) + return false; + } + return true; + }; + /////////////////////////////////////////////////////////////////// { @@ -880,6 +926,7 @@ class Subscribe_test : public beast::unit_test::suite auto jv = wscTxHistory->invoke("subscribe", request); if (!BEAST_EXPECT(goodSubRPC(jv))) return; + jv = wscTxHistory->invoke("subscribe", request); BEAST_EXPECT(!goodSubRPC(jv)); @@ -911,7 +958,6 @@ class Subscribe_test : public beast::unit_test::suite r = getTxHash(*wscTxHistory, vec, 1); BEAST_EXPECT(!r.first); } - { /* * subscribe genesis account tx history without txns @@ -950,8 +996,8 @@ class Subscribe_test : public beast::unit_test::suite if (!BEAST_EXPECT(r.first && r.second)) return; BEAST_EXPECT( - bobFullHistoryVec.back().second == - genesisFullHistoryVec.back().second); + std::get<1>(bobFullHistoryVec.back()) == + std::get<1>(genesisFullHistoryVec.back())); /* * unsubscribe to prepare next test @@ -987,8 +1033,8 @@ class Subscribe_test : public beast::unit_test::suite jv = wscTxHistory->invoke("unsubscribe", request); BEAST_EXPECT( - bobFullHistoryVec.back().second == - genesisFullHistoryVec.back().second); + std::get<1>(bobFullHistoryVec.back()) == + std::get<1>(genesisFullHistoryVec.back())); } { @@ -1030,11 +1076,17 @@ class Subscribe_test : public beast::unit_test::suite if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true))) return; + // check boundary tags + // only account_history_tx_stream has ledger boundary information. + if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false))) + return; + { // take out all history txns from stream to prepare next test IdxHashVec initFundTxns; if (!BEAST_EXPECT( - getTxHash(*wscTxHistory, initFundTxns, 10).second)) + getTxHash(*wscTxHistory, initFundTxns, 10).second) || + !BEAST_EXPECT(checkBoundary(initFundTxns, false))) return; } @@ -1046,6 +1098,12 @@ class Subscribe_test : public beast::unit_test::suite return; if (!BEAST_EXPECT(hashCompare(accountVec, txHistoryVec, true))) return; + + // check boundary tags + // only account_history_tx_stream has ledger boundary information. + if (!BEAST_EXPECT(checkBoundary(txHistoryVec, false))) + return; + wscTxHistory->invoke("unsubscribe", request); wscAccount->invoke("unsubscribe", stream); }