diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index 7e486af4c0..f54db8abe0 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -91,6 +91,9 @@ enum class HashPrefix : std::uint32_t { /** Batch */ batch = detail::make_hash_prefix('B', 'C', 'H'), + + /** Sponsor */ + sponsor = detail::make_hash_prefix('S', 'P', 'N'), }; template diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index b6a0400358..a06bf7fb45 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -112,6 +112,12 @@ public: boost::container::flat_set getMentionedAccounts() const; + static Blob + getSigningData(STTx const& that); + + static Blob + getSponsorSigningData(STTx const& that); + uint256 getTransactionID() const; @@ -138,6 +144,11 @@ public: RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkSponsorSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + // SQL Functions with metadata. static std::string const& getMetaSQLInsertReplaceHeader(); @@ -170,12 +181,23 @@ private: STObject const& batchSigner, RequireFullyCanonicalSig requireCanonicalSig) const; + Expected + checkSponsorSingleSign( + STObject const& signer, + RequireFullyCanonicalSig requireCanonicalSig) const; + Expected checkBatchMultiSign( STObject const& batchSigner, RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkSponsorMultiSign( + STObject const& signer, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + STBase* copy(std::size_t n, void* buf) const override; STBase* @@ -224,6 +246,46 @@ STTx::getTransactionID() const return tid_; } +/** Return a Serializer suitable for computing a multisigning TxnSignature. */ +Serializer +buildMultiSigningData(STObject const& obj, AccountID const& signingID); + +/** Break the multi-signing hash computation into 2 parts for optimization. + + We can optimize verifying multiple multisignatures by splitting the + data building into two parts; + o A large part that is shared by all of the computations. + o A small part that is unique to each signer in the multisignature. + + The following methods support that optimization: + 1. startMultiSigningData provides the large part which can be shared. + 2. finishMultiSigningData caps the passed in serializer with each + signer's unique data. +*/ +Serializer +startMultiSigningData(STObject const& obj); + +inline void +finishMultiSigningData(AccountID const& signingID, Serializer& s) +{ + s.addBitString(signingID); +} + +Serializer +buildSponsorMultiSigningData( + STObject const& obj, + AccountID const& signingID, + uint32_t flags); + +Serializer +startSponsorSigningData(STObject const& obj); + +inline void +finishSponsorSigningData(AccountID const& signerID, Serializer& s) +{ + s.addBitString(signerID); +} + } // namespace ripple #endif diff --git a/include/xrpl/protocol/Sign.h b/include/xrpl/protocol/Sign.h index 5aa9fabddc..512d82cb43 100644 --- a/include/xrpl/protocol/Sign.h +++ b/include/xrpl/protocol/Sign.h @@ -61,31 +61,6 @@ verify( PublicKey const& pk, SF_VL const& sigField = sfSignature); -/** Return a Serializer suitable for computing a multisigning TxnSignature. */ -Serializer -buildMultiSigningData(STObject const& obj, AccountID const& signingID); - -/** Break the multi-signing hash computation into 2 parts for optimization. - - We can optimize verifying multiple multisignatures by splitting the - data building into two parts; - o A large part that is shared by all of the computations. - o A small part that is unique to each signer in the multisignature. - - The following methods support that optimization: - 1. startMultiSigningData provides the large part which can be shared. - 2. finishMultiSigningData caps the passed in serializer with each - signer's unique data. -*/ -Serializer -startMultiSigningData(STObject const& obj); - -inline void -finishMultiSigningData(AccountID const& signingID, Serializer& s) -{ - s.addBitString(signingID); -} - } // namespace ripple #endif diff --git a/include/xrpl/protocol/Sponsor.h b/include/xrpl/protocol/Sponsor.h new file mode 100644 index 0000000000..0533ed496a --- /dev/null +++ b/include/xrpl/protocol/Sponsor.h @@ -0,0 +1,35 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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 + +namespace ripple { + +inline void +addSerializeSponsorData( + Serializer& msg, + AccountID const& sponsorID, + std::uint32_t const& flags) +{ + msg.addBitString(sponsorID); + msg.add32(flags); +} + +} // namespace ripple diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 2bfe555043..f6b6417bdc 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -366,7 +366,7 @@ UNTYPED_SFIELD(sfCredential, OBJECT, 33) UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) -UNTYPED_SFIELD(sfSponsor, OBJECT, 37) +UNTYPED_SFIELD(sfSponsor, OBJECT, 37, SField::sMD_Default, SField::notSigning) // array of objects (common) // ARRAY/1 is reserved for end of array diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 5d56995e86..3129c8635e 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -184,15 +185,26 @@ STTx::getMentionedAccounts() const return list; } -static Blob -getSigningData(STTx const& that) +Blob +STTx::getSigningData(STTx const& that) { Serializer s; s.add32(HashPrefix::txSign); that.addWithoutSigningFields(s); + if (that.isFieldPresent(sfSponsor)) + { + auto const sst = that.getFieldObject(sfSponsor); + addSerializeSponsorData(s, sst.getAccountID(sfAccount), sst.getFlags()); + } return s.getData(); } +Blob +STTx::getSponsorSigningData(STTx const& that) +{ + return startSponsorSigningData(that).getData(); +} + uint256 STTx::getSigningHash() const { @@ -238,7 +250,7 @@ STTx::getFeePayer() const { if (isFieldPresent(sfSponsor)) { - if (getFieldObject(sfSponsor)[sfFlags] & tfSponsorFee) + if (getFieldObject(sfSponsor).isFlag(tfSponsorFee)) { return getFieldObject(sfSponsor)[sfAccount]; } @@ -320,6 +332,42 @@ STTx::checkBatchSign( return Unexpected("Internal batch signature check failure."); } +Expected +STTx::checkSponsorSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + try + { + XRPL_ASSERT( + isFieldPresent(sfSponsor), + "STTx::checkSponsorSign: not a sponsored transaction"); + if (!isFieldPresent(sfSponsor)) + { + JLOG(debugLog().fatal()) << "not a sponsored transaction"; + return Unexpected("Not a sponsored transaction."); + } + STObject const& sponsorObj{getFieldObject(sfSponsor)}; + + Blob const& signingPubKey = sponsorObj.getFieldVL(sfSigningPubKey); + + auto const result = signingPubKey.empty() + ? checkSponsorMultiSign(sponsorObj, requireCanonicalSig, rules) + : checkSponsorSingleSign(sponsorObj, requireCanonicalSig); + + if (!result) + return result; + + return {}; + } + catch (std::exception const& e) + { + JLOG(debugLog().error()) + << "Sponsor signature check failed: " << e.what(); + } + return Unexpected("Sponsor signature check failure."); +} + Json::Value STTx::getJson(JsonOptions options) const { @@ -457,6 +505,17 @@ STTx::checkBatchSingleSign( return singleSignHelper(batchSigner, msg.slice(), fullyCanonical); } +Expected +STTx::checkSponsorSingleSign( + STObject const& signer, + RequireFullyCanonicalSig requireCanonicalSig) const +{ + auto const data = getSponsorSigningData(*this); + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + return singleSignHelper(signer, makeSlice(data), fullyCanonical); +} + Expected multiSignHelper( STObject const& signerObj, @@ -465,9 +524,9 @@ multiSignHelper( Rules const& rules) { // Make sure the MultiSigners are present. Otherwise they are not - // attempting multi-signing and we just have a bad SigningPubKey. + // attempting multi-signing and we just have a bad Signers. if (!signerObj.isFieldPresent(sfSigners)) - return Unexpected("Empty SigningPubKey."); + return Unexpected("Empty Signers."); // We don't allow both an sfSigners and an sfTxnSignature. Both fields // being present would indicate that the transaction is signed both ways. @@ -560,6 +619,32 @@ STTx::checkBatchMultiSign( rules); } +Expected +STTx::checkSponsorMultiSign( + STObject const& sponsorObj, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + // We can ease the computational load inside the loop a bit by + // pre-constructing part of the data that we hash. Fill a Serializer + // with the stuff that stays constant from signature to signature. + auto const data = startSponsorSigningData(*this); + Serializer dataStart = Serializer(data.data(), data.size()); + + return multiSignHelper( + sponsorObj, + fullyCanonical, + [&dataStart](AccountID const& accountID) mutable -> Serializer { + Serializer s = dataStart; + finishSponsorSigningData(accountID, s); + return s; + }, + rules); +} + Expected STTx::checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, @@ -848,4 +933,90 @@ isPseudoTx(STObject const& tx) return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; } +// Questions regarding buildMultiSigningData: +// +// Why do we include the Signer.Account in the blob to be signed? +// +// Unless you include the Account which is signing in the signing blob, +// you could swap out any Signer.Account for any other, which may also +// be on the SignerList and have a RegularKey matching the +// Signer.SigningPubKey. +// +// That RegularKey may be set to allow some 3rd party to sign transactions +// on the account's behalf, and that RegularKey could be common amongst all +// users of the 3rd party. That's just one example of sharing the same +// RegularKey amongst various accounts and just one vulnerability. +// +// "When you have something that's easy to do that makes entire classes of +// attacks clearly and obviously impossible, you need a damn good reason +// not to do it." -- David Schwartz +// +// Why would we include the signingFor account in the blob to be signed? +// +// In the current signing scheme, the account that a signer is `signing +// for/on behalf of` is the tx_json.Account. +// +// Later we might support more levels of signing. Suppose Bob is a signer +// for Alice, and Carol is a signer for Bob, so Carol can sign for Bob who +// signs for Alice. But suppose Alice has two signers: Bob and Dave. If +// Carol is a signer for both Bob and Dave, then the signature needs to +// distinguish between Carol signing for Bob and Carol signing for Dave. +// +// So, if we support multiple levels of signing, then we'll need to +// incorporate the "signing for" accounts into the signing data as well. +Serializer +buildMultiSigningData(STObject const& obj, AccountID const& signingID) +{ + Serializer s{startMultiSigningData(obj)}; + finishMultiSigningData(signingID, s); + return s; +} + +Serializer +startMultiSigningData(STObject const& obj) +{ + Serializer s; + s.add32(HashPrefix::txMultiSign); + obj.addWithoutSigningFields(s); + // if (obj.isFieldPresent(sfSponsor)) + // { + // auto const sst = obj.getFieldObject(sfSponsor); + // addSerializeSponsorData(s, sst.getAccountID(sfAccount), + // sst.getFlags()); + // } + return s; +} + +Serializer +buildSponsorMultiSigningData( + STObject const& obj, + AccountID const& signingID, + uint32_t flags) +{ + Serializer s{startSponsorSigningData(obj)}; + finishSponsorSigningData(signingID, s); + return s; +} + +Serializer +startSponsorSigningData(STObject const& obj) +{ + Serializer s; + s.add32(HashPrefix::sponsor); + STObject tmp = obj; + tmp.setFieldVL(sfSigningPubKey, Blob{}); + tmp.addWithoutSigningFields(s); + + XRPL_ASSERT( + tmp.isFieldPresent(sfSponsor), + "STTx::getSponsorSigningData : sponsor is not set"); + + if (tmp.isFieldPresent(sfSponsor)) + { + auto const sst = tmp.getFieldObject(sfSponsor); + addSerializeSponsorData(s, sst.getAccountID(sfAccount), sst.getFlags()); + } + return s; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/Sign.cpp b/src/libxrpl/protocol/Sign.cpp index 27c2b0435c..b72742336d 100644 --- a/src/libxrpl/protocol/Sign.cpp +++ b/src/libxrpl/protocol/Sign.cpp @@ -61,52 +61,4 @@ verify( pk, Slice(ss.data(), ss.size()), Slice(sig->data(), sig->size())); } -// Questions regarding buildMultiSigningData: -// -// Why do we include the Signer.Account in the blob to be signed? -// -// Unless you include the Account which is signing in the signing blob, -// you could swap out any Signer.Account for any other, which may also -// be on the SignerList and have a RegularKey matching the -// Signer.SigningPubKey. -// -// That RegularKey may be set to allow some 3rd party to sign transactions -// on the account's behalf, and that RegularKey could be common amongst all -// users of the 3rd party. That's just one example of sharing the same -// RegularKey amongst various accounts and just one vulnerability. -// -// "When you have something that's easy to do that makes entire classes of -// attacks clearly and obviously impossible, you need a damn good reason -// not to do it." -- David Schwartz -// -// Why would we include the signingFor account in the blob to be signed? -// -// In the current signing scheme, the account that a signer is `signing -// for/on behalf of` is the tx_json.Account. -// -// Later we might support more levels of signing. Suppose Bob is a signer -// for Alice, and Carol is a signer for Bob, so Carol can sign for Bob who -// signs for Alice. But suppose Alice has two signers: Bob and Dave. If -// Carol is a signer for both Bob and Dave, then the signature needs to -// distinguish between Carol signing for Bob and Carol signing for Dave. -// -// So, if we support multiple levels of signing, then we'll need to -// incorporate the "signing for" accounts into the signing data as well. -Serializer -buildMultiSigningData(STObject const& obj, AccountID const& signingID) -{ - Serializer s{startMultiSigningData(obj)}; - finishMultiSigningData(signingID, s); - return s; -} - -Serializer -startMultiSigningData(STObject const& obj) -{ - Serializer s; - s.add32(HashPrefix::txMultiSign); - obj.addWithoutSigningFields(s); - return s; -} - } // namespace ripple diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 263c8ce508..bb0c64edbd 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -51,6 +51,112 @@ public: env(sponsor::transfer(alice), ter(temDISABLED)); } + void + testSingleSigning() + { + testcase("Single signing"); + using namespace test::jtx; + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const invalid("invalid"); + + env.fund(XRP(10000), alice, sponsor); + env.close(); + + // Signature doesn't exist + auto tx = noop(alice); + tx[sfSponsor.jsonName][sfAccount.jsonName] = sponsor.human(); + tx[sfSponsor.jsonName][sfSigningPubKey.jsonName] = + strHex(sponsor.pk().slice()); + + env(tx, + fee(XRP(1)), + sponsor::as(sponsor, tfSponsorReserve), + ter(telENV_RPC_FAILED)); + + // Invalid signature + tx[sfSponsor.jsonName][sfTxnSignature.jsonName] = "DEADBEEF"; + env(tx, + fee(XRP(1)), + sponsor::as(sponsor, tfSponsorReserve), + ter(telENV_RPC_FAILED)); + + // Signer account doesn't exist + env(noop(alice), + fee(XRP(1)), + sponsor::as(invalid, tfSponsorReserve), + sponsor::sig(invalid), + ter(tefBAD_AUTH)); + + // Success + env(noop(alice), + fee(XRP(1)), + sponsor::as(sponsor, tfSponsorReserve), + sponsor::sig(sponsor), + ter(tesSUCCESS)); + } + + void + testMultiSigning() + { + testcase("Multi signing"); + using namespace test::jtx; + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + Account const invalid("invalid"); + + Account const signer1("signer1"); + Account const signer2("signer2"); + + env.fund(XRP(10000), alice, sponsor); + env.close(); + + env(signers(sponsor, 1, {{signer1, 1}, {signer2, 1}})); + env.close(); + + // Signature doesn't exist + env(noop(alice), + fee(XRP(1)), + sponsor::as(sponsor, tfSponsorReserve), + ter(telENV_RPC_FAILED)); + + // Invalid signature + auto tx = noop(alice); + auto& signers1 = + tx[sfSponsor.jsonName][sfSigners.jsonName][0U][sfSigner.jsonName]; + signers1[sfAccount.jsonName] = signer1.human(); + signers1[sfSigningPubKey.jsonName] = strHex(signer1.pk().slice()); + signers1[sfTxnSignature.jsonName] = "DEADBEEF"; + env(tx, + fee(XRP(1)), + sponsor::as(sponsor, tfSponsorReserve), + ter(telENV_RPC_FAILED)); + + // Signer account doesn't exist + env(noop(alice), + fee(XRP(1)), + sponsor::as(invalid, tfSponsorReserve), + sponsor::msig({signer1}), + ter(tefBAD_AUTH)); + + env(noop(alice), + fee(XRP(1)), + sponsor::as(sponsor, tfSponsorReserve), + sponsor::msig({signer1}), + ter(tesSUCCESS)); + + env(signers(sponsor, 2, {{signer1, 1}, {signer2, 1}})); + env.close(); + + env(noop(alice), + fee(XRP(1)), + sponsor::as(sponsor, tfSponsorReserve), + sponsor::msig({signer1, signer2}), + ter(tesSUCCESS)); + } + void testTransferSponsor() { @@ -658,6 +764,8 @@ public: run() override { testDisabled(); + testSingleSigning(); + testMultiSigning(); testTransferSponsor(); testSponsorFee(); testSponsorAccount(); diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 198839dd28..a074adb600 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_TEST_JTX_JTX_H_INCLUDED #define RIPPLE_TEST_JTX_JTX_H_INCLUDED +#include #include #include @@ -55,6 +56,7 @@ struct JTx bool fill_netid = true; std::shared_ptr stx; std::function signer; + std::function sponsorSigner; JTx() = default; JTx(JTx const&) = default; diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index b44479d3ca..b37340129f 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -567,6 +567,9 @@ Env::autofill_sig(JTx& jt) jtx::sign(jv, lookup(ar->getAccountID(sfRegularKey))); else jtx::sign(jv, account); + + if (jt.sponsorSigner) + jt.sponsorSigner(*this, jt); } void diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp index 385395f8e5..4d8340db96 100644 --- a/src/test/jtx/impl/sponsor.cpp +++ b/src/test/jtx/impl/sponsor.cpp @@ -21,6 +21,7 @@ #include #include +#include #include namespace ripple { @@ -50,61 +51,70 @@ as::operator()(Env& env, JTx& jt) const void sig::operator()(Env& env, JTx& jt) const { - std::optional st; - try - { - // required to cast the STObject to STTx - jt.jv[jss::SigningPubKey] = ""; - st = parse(jt.jv); - } - catch (parse_error const&) - { - env.test.log << pretty(jt.jv) << std::endl; - Rethrow(); - } + auto const signer = signer_; jt.jv[sfSponsor.jsonName][sfAccount.jsonName] = signer.acct.human(); jt.jv[sfSponsor.jsonName][sfSigningPubKey.jsonName] = strHex(signer.sig.pk().slice()); - Serializer ss; - ss.add32(HashPrefix::txSign); - parse(jt.jv).addWithoutSigningFields(ss); - auto const sig = - ripple::sign(signer.acct.pk(), signer.acct.sk(), ss.slice()); - jt.jv[sfSponsor.jsonName][jss::TxnSignature] = - strHex(Slice{sig.data(), sig.size()}); -} - -void -msig::operator()(Env& env, JTx& jt) const -{ - auto const mySigners = signers; - jt.signer = [mySigners, &env](Env&, JTx& jtx) { - jtx[sfSponsor.getJsonName()][sfSigningPubKey.getJsonName()] = ""; + jt.sponsorSigner = [signer, &env](Env&, JTx& jtx) { std::optional st; try { - st = parse(jtx.jv); + Json::Value jv = jtx.jv; + st = parse(jv); } catch (parse_error const&) { env.test.log << pretty(jtx.jv) << std::endl; Rethrow(); } - auto& js = jtx[sfSponsor.getJsonName()][sfSigners.getJsonName()]; + + auto const sst = st->getFieldObject(sfSponsor); + + auto const signingData = + STTx::getSponsorSigningData(STTx{std::move(*st)}); + + auto const sig = ripple::sign( + signer.acct.pk(), signer.acct.sk(), makeSlice(signingData)); + jtx.jv[sfSponsor.jsonName][jss::TxnSignature] = + strHex(Slice{sig.data(), sig.size()}); + }; +} + +void +msig::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSponsor.jsonName][sfSigningPubKey.jsonName] = ""; + auto const mySigners = signers; + jt.sponsorSigner = [mySigners, &env](Env&, JTx& jtx) { + std::optional st; + try + { + Json::Value jv = jtx.jv; + jv[jss::SigningPubKey] = ""; + st = parse(jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jtx.jv) << std::endl; + Rethrow(); + } + auto const sst = st->getFieldObject(sfSponsor); + auto& js = jtx[sfSponsor.jsonName][sfSigners.jsonName]; for (std::size_t i = 0; i < mySigners.size(); ++i) { auto const& e = mySigners[i]; - auto& jo = js[i][sfSigner.getJsonName()]; + auto& jo = js[i][sfSigner.jsonName]; jo[jss::Account] = e.acct.human(); jo[jss::SigningPubKey] = strHex(e.sig.pk().slice()); - Serializer ss{buildMultiSigningData(*st, e.acct.id())}; + Serializer ss{ + buildSponsorMultiSigningData(*st, e.acct.id(), sst.getFlags())}; + auto const sig = ripple::sign( *publicKeyType(e.sig.pk().slice()), e.sig.sk(), ss.slice()); - jo[sfTxnSignature.getJsonName()] = - strHex(Slice{sig.data(), sig.size()}); + jo[sfTxnSignature.jsonName] = strHex(Slice{sig.data(), sig.size()}); } }; } diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index 27b45a32cb..fed7d10a4d 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -47,10 +47,8 @@ void sign(Json::Value& jv, Account const& account) { jv[jss::SigningPubKey] = strHex(account.pk().slice()); - Serializer ss; - ss.add32(HashPrefix::txSign); - parse(jv).addWithoutSigningFields(ss); - auto const sig = ripple::sign(account.pk(), account.sk(), ss.slice()); + auto const blob = STTx::getSigningData(STTx{parse(jv)}); + auto const sig = ripple::sign(account.pk(), account.sk(), makeSlice(blob)); jv[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); } diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h index e09ccd26bd..683c2e0ae4 100644 --- a/src/test/jtx/sponsor.h +++ b/src/test/jtx/sponsor.h @@ -54,10 +54,10 @@ public: struct sig { private: - Reg signer; + Reg signer_; public: - sig(Reg signer_) : signer(std::move(signer_)) + sig(Reg signer) : signer_(std::move(signer)) { } diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index f9dee8e63c..4ad45d13ac 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -647,10 +647,11 @@ Transactor::checkSign(PreclaimContext const& ctx) return checkMultiSign(ctx.view, idAccount, txSigners, ctx.flags, ctx.j); } - // if (ctx.tx.isFieldPresent(sfSponsor)) - // { - // // TODO: check the sponsor signature - // } + if (ctx.tx.isFieldPresent(sfSponsor)) + { + if (auto const ret = checkSponsorSign(ctx); !isTesSuccess(ret)) + return ret; + } // Check Single Sign XRPL_ASSERT( @@ -721,6 +722,51 @@ Transactor::checkBatchSign(PreclaimContext const& ctx) return ret; } +NotTEC +Transactor::checkSponsorSign(PreclaimContext const& ctx) +{ + NotTEC ret = tesSUCCESS; + + if (!ctx.tx.isFieldPresent(sfSponsor)) + return tesSUCCESS; + + auto const sponsorObj = ctx.tx.getFieldObject(sfSponsor); + + auto const sponsorAcc = sponsorObj.getAccountID(sfAccount); + Blob const& pkSigner = sponsorObj.getFieldVL(sfSigningPubKey); + + auto const sleAccount = ctx.view.read(keylet::account(sponsorAcc)); + if (!sleAccount) + return tefBAD_AUTH; + + if (pkSigner.empty()) + { + STArray const& txSigners(sponsorObj.getFieldArray(sfSigners)); + if (ret = checkMultiSign( + ctx.view, sponsorAcc, txSigners, ctx.flags, ctx.j); + !isTesSuccess(ret)) + return ret; + } + else + { + // LCOV_EXCL_START + if (!publicKeyType(makeSlice(pkSigner))) + return tefBAD_AUTH; + // LCOV_EXCL_STOP + + auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); + + if (ret = checkSingleSign( + idSigner, sponsorAcc, sleAccount, ctx.view.rules(), ctx.j); + !isTesSuccess(ret)) + { + return ret; + } + } + + return ret; +} + NotTEC Transactor::checkSingleSign( AccountID const& idSigner, diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 42d4861a63..08a52cf226 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -192,6 +192,9 @@ public: static NotTEC checkBatchSign(PreclaimContext const& ctx); + static NotTEC + checkSponsorSign(PreclaimContext const& ctx); + // Returns the fee in fee units, not scaled for load. static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index e2e0adae45..e455675bb8 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -83,6 +83,17 @@ checkValidity( ? STTx::RequireFullyCanonicalSig::yes : STTx::RequireFullyCanonicalSig::no; + if (tx.isFieldPresent(sfSponsor) && rules.enabled(featureSponsor)) + { + auto const sigVerify = + tx.checkSponsorSign(requireCanonicalSig, rules); + if (!sigVerify) + { + router.setFlags(id, SF_SIGBAD); + return {Validity::SigBad, sigVerify.error()}; + } + } + auto const sigVerify = tx.checkSign(requireCanonicalSig, rules); if (!sigVerify) {