Skip to content

Transaction ProposalSign for ordinary and batch - #8127

Open
Kassaking7 wants to merge 9 commits into
XRPLF:ripple/cosignfrom
Kassaking7:TransactionProposalSign-ordinary-batch
Open

Transaction ProposalSign for ordinary and batch#8127
Kassaking7 wants to merge 9 commits into
XRPLF:ripple/cosignfrom
Kassaking7:TransactionProposalSign-ordinary-batch

Conversation

@Kassaking7

Copy link
Copy Markdown
Contributor

High Level Overview of Change

Context of Change

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

@Kassaking7 Kassaking7 changed the title Transaction proposal sign ordinary batch Transaction proposal sign ordinary and batch Aug 26, 2026
@Kassaking7 Kassaking7 changed the title Transaction proposal sign ordinary and batch Transaction ProposalSign for ordinary and batch Aug 26, 2026
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.37201% with 37 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...x/transactors/proposal/TransactionProposalSign.cpp 82.4% 18 Missing ⚠️
src/libxrpl/ledger/helpers/ProposalHelpers.cpp 88.1% 16 Missing ⚠️
src/libxrpl/tx/Transactor.cpp 80.0% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment on lines +171 to +178
// Terminal is checked before authorization: a late Sign both fails and
// cleans up, regardless of whether this signer would have been allowed
// to contribute (On-Chain Cosigner spec §6.3.2.2).
if (proposal::isTerminal(ctx.view, (*sleProposal)[~sfExpiration], proposedTx))
{
JLOG(ctx.j.debug()) << "TransactionProposalSign: proposal is terminal.";
return tesSUCCESS;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can save compute cycles by moving this check to the top of the preclaim method.

Comment on lines +49 to +52
if (signingFor == proposedTx.getAccountID(sfAccount))
return true;
return proposedTx.isFieldPresent(sfDelegate) &&
signingFor == proposedTx.getAccountID(sfDelegate);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the proposedTx payload makes a mention of sfDelegate, the delegated-account must authorize this transaction. A TransactionProposalSign from the actual account must be rejected with an error code. The order of these two checks need to be flipped.

Reference documentation for Delegated transactions.

Other parts of rippled have switched the order of checks. i.e. we first check if sfDelegate is specified in the payload of the transaction, if not, we default to the sfAccount value.

Here is the test case that demonstrates this issue:

void
testDelegatedPayloadRejectsAccountSigningFor(FeatureBitset features)
{
    testcase("delegated payload rejects SigningFor = Account");

    using namespace jtx;
    using namespace std::chrono_literals;

    Env env{*this, features};

    Account const target{"target"};
    Account const dest{"dest"};
    Account const delegateAcct{"delegateAcct"};
    Account const ds1{"ds1"};
    env.fund(XRP(10000), target, dest, delegateAcct, ds1);
    env.close();

    env(delegate::set(target, delegateAcct, {"Payment"}));
    env(signers(delegateAcct, 1, {{ds1, 1}}));
    env.close();

    std::uint32_t const ticketSeq = proposal::createTicket(env, target);
    json::Value tx = pay(target, dest, XRP(1));
    tx[sfDelegate.jsonName] = delegateAcct.human();
    env(proposal::create(
        target,
        proposal::unsignedPayload(env, tx, ticketSeq, /*extraSigners=*/1),
        proposal::expiration(env, 100s)));
    env.close();

    // The Delegate authorizes this payload; the Account does not. Its
    // contribution must be refused rather than written into the payload's
    // own signature slot, where it would lock the Delegate out for good.
    env(proposal::sign(env, target, target, ticketSeq, target, target),
        Ter(tecNO_PERMISSION));
    env.close();

    {
        auto const sle = proposal::entry(env, target, ticketSeq);
        if (!BEAST_EXPECT(sle))
            return;
        auto const stored = sle->getFieldObject(sfProposedTransaction);
        BEAST_EXPECT(stored.getFieldVL(sfSigningPubKey).empty());
        BEAST_EXPECT(!stored.isFieldPresent(sfTxnSignature));
    }

    // The Delegate's signer is still able to contribute and complete it.
    env(proposal::sign(env, ds1, target, ticketSeq, delegateAcct, ds1));
    env.close();

    env(proposedJson(env, target, ticketSeq), Sig(kNone));
    env.close();

    BEAST_EXPECT(!proposal::entry(env, target, ticketSeq));
    BEAST_EXPECT(env.balance(dest) == XRP(10000) + XRP(1));
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants