From 1fa87c21bec7b560a20eb206e72b5e309b7b8f92 Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Mon, 13 Jun 2022 16:17:22 +0000 Subject: [PATCH] add emitnonce support to various transactors, object ids to various tx formats --- src/ripple/app/tx/impl/CancelOffer.cpp | 41 +++++++++++++--- src/ripple/app/tx/impl/CreateCheck.cpp | 12 ++++- src/ripple/app/tx/impl/CreateOffer.cpp | 49 ++++++++++++++++--- src/ripple/app/tx/impl/Escrow.cpp | 44 +++++++++++++++-- src/ripple/app/tx/impl/NFTokenCreateOffer.cpp | 11 ++++- src/ripple/app/tx/impl/PayChan.cpp | 10 +++- src/ripple/beast/hash/hash_append.h | 15 ++++++ src/ripple/protocol/Indexes.h | 12 +++-- src/ripple/protocol/SField.h | 3 ++ src/ripple/protocol/impl/Indexes.cpp | 10 ++-- src/ripple/protocol/impl/SField.cpp | 2 + src/ripple/protocol/impl/TxFormats.cpp | 4 ++ src/test/app/NFToken_test.cpp | 2 +- 13 files changed, 184 insertions(+), 31 deletions(-) diff --git a/src/ripple/app/tx/impl/CancelOffer.cpp b/src/ripple/app/tx/impl/CancelOffer.cpp index 95d51501c..84dc7d498 100644 --- a/src/ripple/app/tx/impl/CancelOffer.cpp +++ b/src/ripple/app/tx/impl/CancelOffer.cpp @@ -60,11 +60,22 @@ CancelOffer::preclaim(PreclaimContext const& ctx) if (!sle) return terNO_ACCOUNT; + bool hooksEnabled = ctx.view.rules().enabled(featureHooks); + + auto const offerID = ctx.tx[~sfOfferID]; + if ((*sle)[sfSequence] <= offerSequence) { - JLOG(ctx.j.trace()) << "Malformed transaction: " - << "Sequence " << offerSequence << " is invalid."; - return temBAD_SEQUENCE; + if (hooksEnabled && offerID && offerSequence == 0) + { + // pass, here the txn provides offerID instead of offerSequence + } + else + { + JLOG(ctx.j.trace()) << "Malformed transaction: " + << "Sequence " << offerSequence << " is invalid."; + return temBAD_SEQUENCE; + } } return tesSUCCESS; @@ -81,13 +92,31 @@ CancelOffer::doApply() if (!sle) return tefINTERNAL; - if (auto sleOffer = view().peek(keylet::offer(account_, offerSequence))) + bool hooksEnabled = view().rules().enabled(featureHooks); + + std::optional offerID; + + if (hooksEnabled) + offerID = ctx_.tx[~sfOfferID]; + + Keylet cancel = + hooksEnabled && offerID && offerSequence == 0 + ? keylet::offer(account_, *offerID) + : keylet::offer(account_, offerSequence); + + if (auto sleOffer = view().peek(cancel)) { - JLOG(j_.debug()) << "Trying to cancel offer #" << offerSequence; + if (offerID) + JLOG(j_.debug()) << "Trying to cancel offer :" << *offerID; + else + JLOG(j_.debug()) << "Trying to cancel offer #" << offerSequence; return offerDelete(view(), sleOffer, ctx_.app.journal("View")); } - JLOG(j_.debug()) << "Offer #" << offerSequence << " can't be found."; + if (offerID) + JLOG(j_.debug()) << "Offer :" << *offerID << " can't be found."; + else + JLOG(j_.debug()) << "Offer #" << offerSequence << " can't be found."; return tesSUCCESS; } diff --git a/src/ripple/app/tx/impl/CreateCheck.cpp b/src/ripple/app/tx/impl/CreateCheck.cpp index a59a7c12e..7ead633e7 100644 --- a/src/ripple/app/tx/impl/CreateCheck.cpp +++ b/src/ripple/app/tx/impl/CreateCheck.cpp @@ -175,7 +175,17 @@ CreateCheck::doApply() // Note that we use the value from the sequence or ticket as the // Check sequence. For more explanation see comments in SeqProxy.h. std::uint32_t const seq = ctx_.tx.getSeqProxy().value(); - Keylet const checkKeylet = keylet::check(account_, seq); + + bool hooksEnabled = ctx_.view().rules().enabled(featureHooks); + std::optional emitDetails; + if (hooksEnabled && ctx_.tx.isFieldPresent(sfEmitDetails)) + emitDetails = const_cast(ctx_.tx).getField(sfEmitDetails).downcast(); + + Keylet const checkKeylet = + emitDetails + ? keylet::check(account_, (*emitDetails).getFieldH256(sfEmitNonce)) + : keylet::check(account_, seq); + auto sleCheck = std::make_shared(checkKeylet); sleCheck->setAccountID(sfAccount, account_); diff --git a/src/ripple/app/tx/impl/CreateOffer.cpp b/src/ripple/app/tx/impl/CreateOffer.cpp index 8cf6b5019..6852b13ef 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -73,13 +73,21 @@ CreateOffer::preflight(PreflightContext const& ctx) return temBAD_EXPIRATION; } - if (auto const cancelSequence = tx[~sfOfferSequence]; - cancelSequence && *cancelSequence == 0) + auto const offerID = tx[~sfOfferID]; + + auto const cancelSequence = tx[~sfOfferSequence]; + if (cancelSequence && *cancelSequence == 0) { JLOG(j.debug()) << "Malformed offer: bad cancel sequence"; return temBAD_SEQUENCE; } + if (offerID && cancelSequence) + { + JLOG(j.debug()) << "Malformed offer: expect exactly zero or one of: sfOfferID, sfCancelSequence"; + return temBAD_SEQUENCE; + } + STAmount saTakerPays = tx[sfTakerPays]; STAmount saTakerGets = tx[sfTakerGets]; @@ -163,6 +171,15 @@ CreateOffer::preclaim(PreclaimContext const& ctx) return tecUNFUNDED_OFFER; } + bool hooksEnabled = ctx.view.rules().enabled(featureHooks); + auto const offerID = ctx.tx[~sfOfferID]; + + if (!hooksEnabled && offerID) + return temDISABLED; + + if (offerID && cancelSequence) + return temBAD_SEQUENCE; + // This can probably be simplified to make sure that you cancel sequences // before the transaction sequence number. if (cancelSequence && (uAccountSequence <= *cancelSequence)) @@ -901,6 +918,8 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) { using beast::zero; + bool hooksEnabled = sb.rules().enabled(featureHooks); + std::uint32_t const uTxFlags = ctx_.tx.getFlags(); bool const bPassive(uTxFlags & tfPassive); @@ -912,6 +931,9 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) auto saTakerGets = ctx_.tx[sfTakerGets]; auto const cancelSequence = ctx_.tx[~sfOfferSequence]; + std::optional offerID; + if (hooksEnabled) + offerID = ctx_.tx[~sfOfferID]; // this can be used in place of cancel seq // Note that we we use the value from the sequence or ticket as the // offer sequence. For more explanation see comments in SeqProxy.h. @@ -927,17 +949,23 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) TER result = tesSUCCESS; // Process a cancellation request that's passed along with an offer. - if (cancelSequence) + if (cancelSequence || offerID) { - auto const sleCancel = - sb.peek(keylet::offer(account_, *cancelSequence)); + Keylet cancel = offerID + ? keylet::offer(account_, *offerID) + : keylet::offer(account_, *cancelSequence); + + auto const sleCancel = sb.peek(cancel); // It's not an error to not find the offer to cancel: it might have // been consumed or removed. If it is found, however, it's an error // to fail to delete it. if (sleCancel) { - JLOG(j_.debug()) << "Create cancels order " << *cancelSequence; + if (!offerID) + JLOG(j_.debug()) << "Create cancels order " << *cancelSequence; + else + JLOG(j_.debug()) << "Create cancels order " << *offerID; result = offerDelete(sb, sleCancel, viewJ); } } @@ -1135,8 +1163,15 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) } } + std::optional emitDetails; + if (hooksEnabled && ctx_.tx.isFieldPresent(sfEmitDetails)) + emitDetails = const_cast(ctx_.tx).getField(sfEmitDetails).downcast(); + // We need to place the remainder of the offer into its order book. - auto const offer_index = keylet::offer(account_, offerSequence); + Keylet offer_index = + emitDetails + ? keylet::offer(account_, (*emitDetails).getFieldH256(sfEmitNonce)) + : keylet::offer(account_, offerSequence); // Add offer to owner's directory. auto const ownerNode = sb.dirInsert( diff --git a/src/ripple/app/tx/impl/Escrow.cpp b/src/ripple/app/tx/impl/Escrow.cpp index 7486dfaca..609b8f6a4 100644 --- a/src/ripple/app/tx/impl/Escrow.cpp +++ b/src/ripple/app/tx/impl/Escrow.cpp @@ -159,6 +159,7 @@ EscrowCreate::preflight(PreflightContext const& ctx) TER EscrowCreate::doApply() { + auto const closeTime = ctx_.view().info().parentCloseTime; // Prior to fix1571, the cancel and finish times could be greater @@ -230,8 +231,17 @@ EscrowCreate::doApply() // Create escrow in ledger. Note that we we use the value from the // sequence or ticket. For more explanation see comments in SeqProxy.h. + + bool hooksEnabled = ctx_.view().rules().enabled(featureHooks); + std::optional emitDetails; + if (hooksEnabled && ctx_.tx.isFieldPresent(sfEmitDetails)) + emitDetails = const_cast(ctx_.tx).getField(sfEmitDetails).downcast(); + Keylet const escrowKeylet = - keylet::escrow(account, ctx_.tx.getSeqProxy().value()); + emitDetails + ? keylet::escrow(account, (*emitDetails).getFieldH256(sfEmitNonce)) + : keylet::escrow(account, ctx_.tx.getSeqProxy().value()); + auto const slep = std::make_shared(escrowKeylet); (*slep)[sfAmount] = ctx_.tx[sfAmount]; (*slep)[sfAccount] = account; @@ -355,7 +365,21 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) TER EscrowFinish::doApply() { - auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + bool hooksEnabled = view().rules().enabled(featureHooks); + + if (!hooksEnabled && ctx_.tx.isFieldPresent(sfEscrowID)) + return temDISABLED; + + std::optional escrowID = ctx_.tx[~sfEscrowID]; + + if (escrowID && ctx_.tx[sfOfferSequence] != 0) + return temMALFORMED; + + Keylet k = + escrowID + ? keylet::escrow(ctx_.tx[sfOwner], *escrowID) + : keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + auto const slep = ctx_.view().peek(k); if (!slep) return tecNO_TARGET; @@ -516,7 +540,21 @@ EscrowCancel::preflight(PreflightContext const& ctx) TER EscrowCancel::doApply() { - auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + bool hooksEnabled = view().rules().enabled(featureHooks); + + if (!hooksEnabled && ctx_.tx.isFieldPresent(sfEscrowID)) + return temDISABLED; + + std::optional escrowID = ctx_.tx[~sfEscrowID]; + + if (escrowID && ctx_.tx[sfOfferSequence] != 0) + return temMALFORMED; + + Keylet k = + escrowID + ? keylet::escrow(ctx_.tx[sfOwner], *escrowID) + : keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + auto const slep = ctx_.view().peek(k); if (!slep) return tecNO_TARGET; diff --git a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp index bf92472e2..c3281fa6c 100644 --- a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp @@ -174,8 +174,15 @@ NFTokenCreateOffer::doApply() auto const nftokenID = ctx_.tx[sfNFTokenID]; - auto const offerID = - keylet::nftoffer(account_, ctx_.tx.getSeqProxy().value()); + bool hooksEnabled = ctx_.view().rules().enabled(featureHooks); + std::optional emitDetails; + if (hooksEnabled && ctx_.tx.isFieldPresent(sfEmitDetails)) + emitDetails = const_cast(ctx_.tx).getField(sfEmitDetails).downcast(); + + Keylet const offerID = + emitDetails + ? keylet::nftoffer(account_, (*emitDetails).getFieldH256(sfEmitNonce)) + : keylet::nftoffer(account_, ctx_.tx.getSeqProxy().value()); // Create the offer: { diff --git a/src/ripple/app/tx/impl/PayChan.cpp b/src/ripple/app/tx/impl/PayChan.cpp index aab3dcc5a..e9306ac9e 100644 --- a/src/ripple/app/tx/impl/PayChan.cpp +++ b/src/ripple/app/tx/impl/PayChan.cpp @@ -245,8 +245,16 @@ PayChanCreate::doApply() // // Note that we we use the value from the sequence or ticket as the // payChan sequence. For more explanation see comments in SeqProxy.h. + bool hooksEnabled = ctx_.view().rules().enabled(featureHooks); + std::optional emitDetails; + if (hooksEnabled && ctx_.tx.isFieldPresent(sfEmitDetails)) + emitDetails = const_cast(ctx_.tx).getField(sfEmitDetails).downcast(); + Keylet const payChanKeylet = - keylet::payChan(account, dst, ctx_.tx.getSeqProxy().value()); + emitDetails + ? keylet::payChan(account, dst, (*emitDetails).getFieldH256(sfEmitNonce)) + : keylet::payChan(account, dst, ctx_.tx.getSeqProxy().value()); + auto const slep = std::make_shared(payChanKeylet); // Funds held in this channel diff --git a/src/ripple/beast/hash/hash_append.h b/src/ripple/beast/hash/hash_append.h index 2cbcd9ae3..d9311565d 100644 --- a/src/ripple/beast/hash/hash_append.h +++ b/src/ripple/beast/hash/hash_append.h @@ -39,6 +39,7 @@ #include #include #include +#include namespace beast { @@ -371,6 +372,7 @@ hash_append(Hasher& h, std::pair const& p) noexcept hash_append(h, p.first, p.second); } + // vector template @@ -454,6 +456,19 @@ hash_append(Hasher& h, std::tuple const& t) noexcept detail::tuple_hash(h, t, std::index_sequence_for{}); } +// variant + +template +inline std::enable_if_t< + !is_contiguously_hashable, Hasher>::value> +hash_append(Hasher& h, std::variant const& v) noexcept +{ + std::visit( + [&](auto const& arg){ + hash_append(h, arg); + }, v); +} + // shared_ptr template diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index fcfcb4e9a..f3cc2917a 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -32,6 +32,8 @@ namespace ripple { +using UInt32or256 = std::variant; + class SeqProxy; /** Keylet computation funclets. @@ -143,7 +145,7 @@ line(AccountID const& id, Issue const& issue) noexcept /** An offer from an account */ /** @{ */ Keylet -offer(AccountID const& id, std::uint32_t seq) noexcept; +offer(AccountID const& id, UInt32or256 const& seq) noexcept; inline Keylet offer(uint256 const& key) noexcept @@ -192,7 +194,7 @@ signers(AccountID const& account) noexcept; /** A Check */ /** @{ */ Keylet -check(AccountID const& id, std::uint32_t seq) noexcept; +check(AccountID const& id, UInt32or256 const& seq) noexcept; inline Keylet check(uint256 const& key) noexcept @@ -238,11 +240,11 @@ page(Keylet const& root, std::uint64_t index = 0) noexcept /** An escrow entry */ Keylet -escrow(AccountID const& src, std::uint32_t seq) noexcept; +escrow(AccountID const& src, UInt32or256 const& seq) noexcept; /** A PaymentChannel */ Keylet -payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept; +payChan(AccountID const& src, AccountID const& dst, UInt32or256 const& seq) noexcept; /** NFT page keylets @@ -266,7 +268,7 @@ nftpage(Keylet const& k, uint256 const& token); /** An offer from an account to buy or sell an NFT */ Keylet -nftoffer(AccountID const& owner, std::uint32_t seq); +nftoffer(AccountID const& owner, UInt32or256 const& seq); inline Keylet nftoffer(uint256 const& offer) diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 71ac90f13..d02bb2ecf 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -464,6 +464,9 @@ extern SF_UINT256 const sfHookStateKey; extern SF_UINT256 const sfHookHash; extern SF_UINT256 const sfHookNamespace; extern SF_UINT256 const sfHookSetTxnID; +extern SF_UINT256 const sfOfferID; +extern SF_UINT256 const sfEscrowID; + // currency amount (common) extern SF_AMOUNT const sfAmount; diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 591a6f103..5a4545a0c 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -263,7 +263,7 @@ line( } Keylet -offer(AccountID const& id, std::uint32_t seq) noexcept +offer(AccountID const& id, UInt32or256 const& seq) noexcept { return {ltOFFER, indexHash(LedgerNameSpace::OFFER, id, seq)}; } @@ -322,7 +322,7 @@ signers(AccountID const& account) noexcept } Keylet -check(AccountID const& id, std::uint32_t seq) noexcept +check(AccountID const& id, UInt32or256 const& seq) noexcept { return {ltCHECK, indexHash(LedgerNameSpace::CHECK, id, seq)}; } @@ -359,13 +359,13 @@ page(uint256 const& key, std::uint64_t index) noexcept } Keylet -escrow(AccountID const& src, std::uint32_t seq) noexcept +escrow(AccountID const& src, UInt32or256 const& seq) noexcept { return {ltESCROW, indexHash(LedgerNameSpace::ESCROW, src, seq)}; } Keylet -payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept +payChan(AccountID const& src, AccountID const& dst, UInt32or256 const& seq) noexcept { return { ltPAYCHAN, @@ -396,7 +396,7 @@ nftpage(Keylet const& k, uint256 const& token) } Keylet -nftoffer(AccountID const& owner, std::uint32_t seq) +nftoffer(AccountID const& owner, UInt32or256 const& seq) { return { ltNFTOKEN_OFFER, indexHash(LedgerNameSpace::NFTOKEN_OFFER, owner, seq)}; diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index b985e251f..0430f4eca 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -215,6 +215,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookStateKey, "HookStateKey", UINT256, CONSTRUCT_TYPED_SFIELD(sfHookHash, "HookHash", UINT256, 31); CONSTRUCT_TYPED_SFIELD(sfHookNamespace, "HookNamespace", UINT256, 32); CONSTRUCT_TYPED_SFIELD(sfHookSetTxnID, "HookSetTxnID", UINT256, 33); +CONSTRUCT_TYPED_SFIELD(sfOfferID, "OfferID", UINT256, 34); +CONSTRUCT_TYPED_SFIELD(sfEscrowID, "EscrowID", UINT256, 35); // currency amount (common) CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1); diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 1dde17f4d..a28237bec 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -78,6 +78,7 @@ TxFormats::TxFormats() {sfTakerGets, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, {sfOfferSequence, soeOPTIONAL}, + {sfOfferID, soeOPTIONAL}, // keylet as alternative to offerseq {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -86,6 +87,7 @@ TxFormats::TxFormats() ttOFFER_CANCEL, { {sfOfferSequence, soeREQUIRED}, + {sfOfferID, soeOPTIONAL}, // keylet as alternative to offerseq {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -130,6 +132,7 @@ TxFormats::TxFormats() { {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, + {sfEscrowID, soeOPTIONAL}, // keylet as alternative to offerseq {sfFulfillment, soeOPTIONAL}, {sfCondition, soeOPTIONAL}, {sfTicketSequence, soeOPTIONAL}, @@ -141,6 +144,7 @@ TxFormats::TxFormats() { {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, + {sfEscrowID, soeOPTIONAL}, // keylet as alternative to offerseq {sfTicketSequence, soeOPTIONAL}, }, commonFields); diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 7dd4a7812..de7a67148 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -1128,7 +1128,7 @@ class NFToken_test : public beast::unit_test::suite // preclaim // The buy offer must be present in the ledger. - uint256 const missingOfferIndex = keylet::nftoffer(alice, 1).key; + uint256 const missingOfferIndex = keylet::nftoffer(alice, 1U).key; env(token::acceptBuyOffer(buyer, missingOfferIndex), ter(tecOBJECT_NOT_FOUND)); env.close();