From f719300e80a6b2a470c799d34f2413cc44b8ed3e Mon Sep 17 00:00:00 2001 From: seelabs Date: Thu, 11 Feb 2021 19:09:54 -0500 Subject: [PATCH] Introduce sidechain support (XLS-38): A bridge connects two blockchains: a locking chain and an issuing chain (also called a mainchain and a sidechain). Both are independent ledgers, with their own validators and potentially their own custom transactions. Importantly, there is a way to move assets from the locking chain to the issuing chain and a way to return those assets from the issuing chain back to the locking chain: the bridge. This key operation is called a cross-chain transfer. A cross-chain transfer is not a single transaction. It happens on two chains, requires multiple transactions, and involves an additional server type called a "witness". A bridge does not exchange assets between two ledgers. Instead, it locks assets on one ledger (the "locking chain") and represents those assets with wrapped assets on another chain (the "issuing chain"). A good model to keep in mind is a box with an infinite supply of wrapped assets. Putting an asset from the locking chain into the box will release a wrapped asset onto the issuing chain. Putting a wrapped asset from the issuing chain back into the box will release one of the existing locking chain assets back onto the locking chain. There is no other way to get assets into or out of the box. Note that there is no way for the box to "run out of" wrapped assets - it has an infinite supply. Co-authored-by: Gregory Popovitch --- Builds/CMake/CMakeFuncs.cmake | 6 +- Builds/CMake/RippledCore.cmake | 9 + Builds/CMake/RippledRelease.cmake | 6 + CMakeLists.txt | 2 +- src/ripple/app/main/Application.cpp | 6 + src/ripple/app/main/Main.cpp | 54 +- src/ripple/app/misc/NetworkOPs.cpp | 30 +- src/ripple/app/tx/impl/InvariantCheck.cpp | 8 +- src/ripple/app/tx/impl/InvariantCheck.h | 3 +- src/ripple/app/tx/impl/XChainBridge.cpp | 2292 ++++++++ src/ripple/app/tx/impl/XChainBridge.h | 255 + src/ripple/app/tx/impl/applySteps.cpp | 84 + src/ripple/beast/utility/Journal.h | 1 - src/ripple/core/Config.h | 11 + src/ripple/overlay/PeerReservationTable.h | 3 - src/ripple/protocol/AccountID.h | 17 + src/ripple/protocol/Feature.h | 3 +- src/ripple/protocol/Indexes.h | 10 + src/ripple/protocol/LedgerFormats.h | 19 + src/ripple/protocol/PublicKey.h | 25 + src/ripple/protocol/SField.h | 26 + src/ripple/protocol/STAccount.h | 34 +- src/ripple/protocol/STAmount.h | 17 + src/ripple/protocol/STArray.h | 2 +- src/ripple/protocol/STIssue.h | 4 + src/ripple/protocol/STXChainBridge.h | 235 + src/ripple/protocol/TER.h | 26 +- src/ripple/protocol/TxFlags.h | 4 + src/ripple/protocol/TxFormats.h | 25 + src/ripple/protocol/XChainAttestations.h | 514 ++ src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/Indexes.cpp | 48 + .../protocol/impl/InnerObjectFormats.cpp | 60 +- src/ripple/protocol/impl/Issue.cpp | 4 +- src/ripple/protocol/impl/LedgerFormats.cpp | 43 + src/ripple/protocol/impl/SField.cpp | 44 +- src/ripple/protocol/impl/STAmount.cpp | 11 + src/ripple/protocol/impl/STArray.cpp | 2 +- src/ripple/protocol/impl/STIssue.cpp | 11 + src/ripple/protocol/impl/STParsedJSON.cpp | 19 + src/ripple/protocol/impl/STVar.cpp | 11 +- src/ripple/protocol/impl/STVar.h | 2 +- src/ripple/protocol/impl/STXChainBridge.cpp | 227 + src/ripple/protocol/impl/TER.cpp | 23 + src/ripple/protocol/impl/TxFormats.cpp | 98 + .../protocol/impl/XChainAttestations.cpp | 759 +++ src/ripple/protocol/json_get_or_throw.h | 159 + src/ripple/protocol/jss.h | 251 +- src/ripple/rpc/handlers/AccountObjects.cpp | 6 +- src/ripple/rpc/handlers/LedgerEntry.cpp | 198 + src/ripple/rpc/impl/RPCHelpers.cpp | 18 +- src/test/app/XChain_test.cpp | 5121 +++++++++++++++++ src/test/jtx/attester.h | 67 + src/test/jtx/impl/attester.cpp | 82 + src/test/jtx/impl/xchain_bridge.cpp | 516 ++ src/test/jtx/xchain_bridge.h | 260 + src/test/rpc/AccountObjects_test.cpp | 146 +- src/test/rpc/LedgerRPC_test.cpp | 320 + src/test/rpc/Subscribe_test.cpp | 78 +- 59 files changed, 12142 insertions(+), 174 deletions(-) create mode 100644 src/ripple/app/tx/impl/XChainBridge.cpp create mode 100644 src/ripple/app/tx/impl/XChainBridge.h create mode 100644 src/ripple/protocol/STXChainBridge.h create mode 100644 src/ripple/protocol/XChainAttestations.h create mode 100644 src/ripple/protocol/impl/STXChainBridge.cpp create mode 100644 src/ripple/protocol/impl/XChainAttestations.cpp create mode 100644 src/ripple/protocol/json_get_or_throw.h create mode 100644 src/test/app/XChain_test.cpp create mode 100644 src/test/jtx/attester.h create mode 100644 src/test/jtx/impl/attester.cpp create mode 100644 src/test/jtx/impl/xchain_bridge.cpp create mode 100644 src/test/jtx/xchain_bridge.h 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); }