Files
rippled/src/xrpld/app/tx/detail/XChainBridge.cpp
2025-11-19 16:23:28 +00:00

2242 lines
70 KiB
C++

#include <xrpld/app/paths/Flow.h>
#include <xrpld/app/tx/detail/SignerEntries.h>
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpld/app/tx/detail/XChainBridge.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XChainAttestations.h>
#include <xrpl/protocol/XRPAmount.h>
#include <unordered_map>
#include <unordered_set>
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<AccountID, std::uint32_t> 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<AccountID> 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 <class TAttestation>
Expected<std::vector<AccountID>, TER>
claimHelper(
XChainAttestationsBase<TAttestation>& attestations,
ReadView const& view,
typename TAttestation::MatchFields const& toMatch,
CheckDst checkDst,
std::uint32_t quorum,
std::unordered_map<AccountID, std::uint32_t> 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<AccountID> 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())
{
// LCOV_EXCL_START
UNREACHABLE(
"ripple::claimHelper : invalid inputs"); // should have already
// been checked
continue;
// LCOV_EXCL_STOP
}
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<std::vector<AccountID>> rewardAccounts;
// `changed` is true if the attestation collection changed in any way
// (added/removed/changed)
bool changed{false};
};
template <class TAttestation>
[[nodiscard]] OnNewAttestationResult
onNewAttestations(
XChainAttestationsBase<TAttestation>& attestations,
ReadView const& view,
typename TAttestation::TSignedAttestation const* attBegin,
typename TAttestation::TSignedAttestation const* attEnd,
std::uint32_t quorum,
std::unordered_map<AccountID, std::uint32_t> 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<std::vector<AccountID>, TER>
onClaim(
XChainClaimAttestations& attestations,
ReadView const& view,
STAmount const& sendingAmount,
bool wasLockingChainSend,
std::uint32_t quorum,
std::unordered_map<AccountID, std::uint32_t> 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<std::uint32_t> const& dstTag,
std::optional<AccountID> const& claimOwner,
STAmount const& amt,
CanCreateDstPolicy canCreate,
DepositAuthPolicy depositAuthPolicy,
std::optional<TransferHelperSubmittingAccountInfo> 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));
XRPL_ASSERT(sleSrc, "ripple::transferHelper : non-null source account");
if (!sleSrc)
return tecINTERNAL; // LCOV_EXCL_LINE
{
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().reserve)
{
JLOG(j.trace()) << "Insufficient payment to create account.";
return tecNO_DST_INSUF_XRP;
}
// Create the account.
sleDst = std::make_shared<SLE>(dstK);
sleDst->setAccountID(sfAccount, dst);
sleDst->setFieldU32(sfSequence, psb.seq());
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*/ OfferCrossing::no,
/*limit quality*/ std::nullopt,
/*sendmax*/ std::nullopt,
/*domain id*/ 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<TER> mainFundsTer;
// TER for transfering the reward funds
std::optional<TER> rewardTer;
// TER for removing the sle (if is sle is to be removed)
std::optional<TER> 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<std::uint32_t> const& dstTag,
AccountID const& claimOwner,
STAmount const& sendingAmount,
AccountID const& rewardPoolSrc,
STAmount const& rewardPool,
std::vector<AccountID> 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 = [&] {
auto const round_mode =
innerSb.rules().enabled(fixXChainRewardRounding)
? Number::rounding_mode::downward
: Number::getround();
saveNumberRoundMode _{Number::setround(round_mode)};
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; // LCOV_EXCL_LINE
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::unordered_map<AccountID, std::uint32_t>, std::uint32_t, TER>
getSignersListAndQuorum(
ReadView const& view,
SLE const& sleBridge,
beast::Journal j)
{
std::unordered_map<AccountID, std::uint32_t> r;
std::uint32_t q = std::numeric_limits<std::uint32_t>::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 <class R, class F>
std::shared_ptr<R>
readOrpeekBridge(F&& getter, STXChainBridge const& bridgeSpec)
{
auto tryGet = [&](STXChainBridge::ChainType ct) -> std::shared_ptr<R> {
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<SLE>
peekBridge(ApplyView& v, STXChainBridge const& bridgeSpec)
{
return readOrpeekBridge<SLE>(
[&v](STXChainBridge const& b, STXChainBridge::ChainType ct)
-> std::shared_ptr<SLE> { return v.peek(keylet::bridge(b, ct)); },
bridgeSpec);
}
std::shared_ptr<SLE const>
readBridge(ReadView const& v, STXChainBridge const& bridgeSpec)
{
return readOrpeekBridge<SLE const>(
[&v](STXChainBridge const& b, STXChainBridge::ChainType ct)
-> std::shared_ptr<SLE const> {
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 <class TIter>
TER
applyClaimAttestations(
ApplyView& view,
RawView& rawView,
TIter attBegin,
TIter attEnd,
STXChainBridge const& bridgeSpec,
STXChainBridge::ChainType const srcChain,
std::unordered_map<AccountID, std::uint32_t> 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<ScopeResult, TER> {
// 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<Attestations::AttestationClaim> 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 <class TIter>
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<AccountID, std::uint32_t> const& signersList,
std::uint32_t quorum,
beast::Journal j)
{
if (attBegin == attEnd)
return tesSUCCESS;
PaymentSandbox psb(&view);
auto const claimCountResult = [&]() -> Expected<std::uint64_t, TER> {
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<ScopeResult, TER> {
// 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<Attestations::AttestationCreateAccount> 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; // LCOV_EXCL_LINE
(*sleBridge)[sfXChainAccountClaimCount] = attBegin->createCount;
psb.update(sleBridge);
}
else if (createCID)
{
auto const createdSleClaimID = std::make_shared<SLE>(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; // LCOV_EXCL_LINE
(*createdSleClaimID)[sfOwnerNode] = *page;
auto const sleDoor = psb.peek(doorK);
if (!sleDoor)
return tecINTERNAL; // LCOV_EXCL_LINE
// Reserve was already checked
adjustOwnerCount(psb, sleDoor, 1, j);
psb.insert(createdSleClaimID);
psb.update(sleDoor);
}
psb.apply(rawView);
return tesSUCCESS;
}
template <class TAttestation>
std::optional<TAttestation>
toClaim(STTx const& tx)
{
static_assert(
std::is_same_v<TAttestation, Attestations::AttestationClaim> ||
std::is_same_v<TAttestation, Attestations::AttestationCreateAccount>);
try
{
STObject o{tx};
o.setAccountID(sfAccount, o[sfOtherChainSource]);
return TAttestation(o);
}
catch (...)
{
}
return std::nullopt;
}
template <class TAttestation>
NotTEC
attestationpreflight(PreflightContext const& ctx)
{
if (!publicKeyType(ctx.tx[sfPublicKey]))
return temMALFORMED;
auto const att = toClaim<TAttestation>(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 tesSUCCESS;
}
template <class TAttestation>
TER
attestationPreclaim(PreclaimContext const& ctx)
{
auto const att = toClaim<TAttestation>(ctx.tx);
// checked in preflight
if (!att)
return tecINTERNAL; // LCOV_EXCL_LINE
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 <class TAttestation>
TER
attestationDoApply(ApplyContext& ctx)
{
auto const att = toClaim<TAttestation>(ctx.tx);
if (!att)
// Should already be checked in preflight
return tecINTERNAL; // LCOV_EXCL_LINE
STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge];
struct ScopeResult
{
STXChainBridge::ChainType srcChain;
std::unordered_map<AccountID, std::uint32_t> signersList;
std::uint32_t quorum;
AccountID thisDoor;
Keylet bridgeK;
};
auto const scopeResult = [&]() -> Expected<ScopeResult, TER> {
// 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<TAttestation, Attestations::AttestationClaim> ||
std::is_same_v<TAttestation, Attestations::AttestationCreateAccount>);
if constexpr (std::is_same_v<TAttestation, Attestations::AttestationClaim>)
{
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)
{
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 tesSUCCESS;
}
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());
{
auto hasBridge = [&](STXChainBridge::ChainType ct) -> bool {
return ctx.view.exists(keylet::bridge(bridgeSpec, ct));
};
if (hasBridge(STXChainBridge::ChainType::issuing) ||
hasBridge(STXChainBridge::ChainType::locking))
{
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; // LCOV_EXCL_LINE
STXChainBridge::ChainType const chainType =
STXChainBridge::srcChain(account == bridgeSpec.lockingChainDoor());
Keylet const bridgeKeylet = keylet::bridge(bridgeSpec, chainType);
auto const sleBridge = std::make_shared<SLE>(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; // LCOV_EXCL_LINE
(*sleBridge)[sfOwnerNode] = *page;
}
adjustOwnerCount(ctx_.view(), sleAcct, 1, ctx_.journal);
ctx_.view().insert(sleBridge);
ctx_.view().update(sleAcct);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
std::uint32_t
BridgeModify::getFlagsMask(PreflightContext const& ctx)
{
return tfBridgeModifyMask;
}
NotTEC
BridgeModify::preflight(PreflightContext const& ctx)
{
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 tesSUCCESS;
}
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; // LCOV_EXCL_LINE
STXChainBridge::ChainType const chainType =
STXChainBridge::srcChain(account == bridgeSpec.lockingChainDoor());
auto const sleBridge =
ctx_.view().peek(keylet::bridge(bridgeSpec, chainType));
if (!sleBridge)
return tecINTERNAL; // LCOV_EXCL_LINE
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)
{
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 tesSUCCESS;
}
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; // LCOV_EXCL_LINE
}
{
// 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; // LCOV_EXCL_LINE
}
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<AccountID> rewardAccounts;
AccountID rewardPoolSrc;
STAmount sendingAmount;
STXChainBridge::ChainType srcChain;
STAmount signatureReward;
};
auto const scopeResult = [&]() -> Expected<ScopeResult, TER> {
// 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<std::uint32_t> 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)
{
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 tesSUCCESS;
}
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; // LCOV_EXCL_LINE
}
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; // LCOV_EXCL_LINE
auto const sleBridge = readBridge(psb, bridgeSpec);
if (!sleBridge)
return tecINTERNAL; // LCOV_EXCL_LINE
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)
{
auto const reward = ctx.tx[sfSignatureReward];
if (!isXRP(reward) || reward.signum() < 0 || !isLegalNet(reward))
return temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT;
return tesSUCCESS;
}
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; // LCOV_EXCL_LINE
auto const sleBridge = peekBridge(ctx_.view(), bridgeSpec);
if (!sleBridge)
return tecINTERNAL; // LCOV_EXCL_LINE
std::uint32_t const claimID = (*sleBridge)[sfXChainClaimID] + 1;
if (claimID == 0)
{
// overflow
return tecINTERNAL; // LCOV_EXCL_LINE
}
(*sleBridge)[sfXChainClaimID] = claimID;
Keylet const claimIDKeylet = keylet::xChainClaimID(bridgeSpec, claimID);
if (ctx_.view().exists(claimIDKeylet))
{
// already checked out!?!
return tecINTERNAL; // LCOV_EXCL_LINE
}
auto const sleClaimID = std::make_shared<SLE>(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; // LCOV_EXCL_LINE
(*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<Attestations::AttestationClaim>(ctx);
}
TER
XChainAddClaimAttestation::preclaim(PreclaimContext const& ctx)
{
return attestationPreclaim<Attestations::AttestationClaim>(ctx);
}
TER
XChainAddClaimAttestation::doApply()
{
return attestationDoApply<Attestations::AttestationClaim>(ctx_);
}
//------------------------------------------------------------------------------
NotTEC
XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx)
{
return attestationpreflight<Attestations::AttestationCreateAccount>(ctx);
}
TER
XChainAddAccountCreateAttestation::preclaim(PreclaimContext const& ctx)
{
return attestationPreclaim<Attestations::AttestationCreateAccount>(ctx);
}
TER
XChainAddAccountCreateAttestation::doApply()
{
return attestationDoApply<Attestations::AttestationCreateAccount>(ctx_);
}
//------------------------------------------------------------------------------
NotTEC
XChainCreateAccountCommit::preflight(PreflightContext const& ctx)
{
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 tesSUCCESS;
}
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<STAmount> 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; // LCOV_EXCL_LINE
}
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; // LCOV_EXCL_LINE
auto const sleBridge = peekBridge(psb, bridge);
if (!sleBridge)
return tecINTERNAL; // LCOV_EXCL_LINE
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