xls: 103 title: On-Chain Cosigner description: Native on-ledger proposal and multi-signature collection for XRPL transactions. author: Shawn Xie (@shawnxie999), Zhiyuan Wang (@Kassaking7), Chenna Keshava B S (@ckeshava), Mayukha Vadari (@mvadari) category: Amendment status: Draft proposal-from: https://github.com/XRPLF/XRPL-Standards/discussions/589 created: 2026-07-14 updated: 2026-09-15
- On-Chain Cosigner
- 1. Abstract
- 2. Motivation
- 3. Overview
- 4. Ledger Entry: TransactionProposal
- 5. Transaction: TransactionProposalCreate
- 6. Transaction: TransactionProposalSign
- 7. Transaction: TransactionProposalCancel
- 8. API
- 9. Rationale
- 10. Composability
- 11. Backwards Compatibility
- 12. Open Questions
- 13. Security Considerations
- Appendix
On-Chain Cosigner¶
1. Abstract¶
The XRP Ledger supports multi-signature transactions, but coordination between signers happens entirely off-chain. A transaction blob must be manually shared with each signer, signatures must be collected and assembled by hand, and a single coordinator must finally submit the fully-signed transaction. This off-chain "last mile" reintroduces the very single point of failure that multi-sign is meant to remove: if the coordinator goes offline, loses the collected signatures, or assembles the wrong transaction, the signing round fails or is compromised.
This proposal introduces On-Chain Cosigner: a native mechanism that turns the ledger itself into the "meeting room" where multi-signatures are collected. A proposer posts an unsigned transaction on-ledger as a TransactionProposal object. Authorized signers append their signatures to it directly on-chain, one transaction at a time. Each signature is validated as it arrives and appended directly into the proposed transaction's own Signers field, so the proposal is always a well-formed transaction-in-progress. Once the accumulated signer weight reaches the account's quorum, the stored transaction is already fully signed: anyone can copy it verbatim and submit it through the ordinary transaction path — no assembly and no coordinator required.
We propose:
- Creating a
TransactionProposalledger entry. - Creating a
TransactionProposalCreatetransaction. - Creating a
TransactionProposalSigntransaction. - Creating a
TransactionProposalCanceltransaction.
This feature will require an amendment, tentatively titled Cosigner.
2. Motivation¶
XRPL multi-sign today has two structural problems, both stemming from the absence of an on-ledger "meeting room" for signers:
-
Manual assembly and latency. There is no way to "send" a transaction to another signer through the ledger. The transaction blob must be passed around through external channels (email, Slack, custody tooling). Gathering signatures is slow and error-prone, which makes multi-sign unsuitable for time-sensitive operations.
-
The centralized-coordinator paradox. One party must eventually collect, sort, and submit all signatures. That party becomes a new single point of failure: if they go offline, the transaction cannot be submitted even if everyone else has signed (inaction); they can present different signers with different blobs (manipulation); and if they lose the collected signatures the round must restart (data loss).
On-Chain Cosigner solves both problems by moving signature collection onto the ledger:
- The transaction is posted once, on-ledger, with an immutable payload. Every signer signs the same object, removing ambiguity.
- Signatures are collected on the ledger itself, not assembled by a coordinator. There is no blob to lose, no blob to swap, and the collected set is always available to everyone.
- The completed transaction derives its authority solely from the signatures collected on-ledger. Because the signatures accumulate into a standard multi-signed transaction, anyone can submit it through the normal transaction path — no coordinator can withhold or alter it.
- A built-in expiration prevents abandoned proposals from accumulating and bounds the collection window, and requiring
TicketSequenceinstead ofSequence(§9.2) decouples the proposal from the target account's live sequence.
Multi-sign is inherently signature-heavy, and this feature is designed to compose with other signature-heavy XRPL features — Batch (XLS-56), sponsored fees & reserves, and lending-protocol origination — where multiple parties across custodians or institutions must co-authorize a single ledger action.
3. Overview¶
3.1. Terminology¶
- Proposal: A
TransactionProposalledger object. It holds a single unsigned proposed transaction (the payload) and the set of signatures collected for it so far. - Proposed transaction: The transaction that will be executed on behalf of the target account once enough signatures are collected. It is stored, immutable, inside the proposal, and must identify the target account's spendable authorization via
TicketSequencerather thanSequence(§4.2.1). (This is a distinct concept from a Batch "inner transaction".) - Target account: The account on whose behalf the proposed transaction executes — i.e. the
Accountof the proposed transaction. ItsSignerListconfiguration governs the quorum. - Proposer: The account that submits
TransactionProposalCreate. It owns the proposal object and pays its reserve. The proposer must be either the target account — or the proposed transaction'sDelegate, when one is present — or a member of that account's applicableSignerList(§5.1.1). - Signer: An account ID on the target account's applicable
SignerListthat can append its signature to the proposal, contributing its weight toward quorum. For multi-signing, this may be an unfunded AccountID derived from a public key, matching existing XRPL multi-sign behavior. - Quorum: The
SignerQuorumvalue of the target account's applicableSignerList. Weights and quorum are inherited unchanged from the account's existing multi-sign configuration; this feature does not define its own quorum mechanics. - Complete: A proposal is complete when the collected signatures satisfy all of the proposed transaction's signing requirements — the target account's quorum, plus any auxiliary co-signature the transaction requires (the
Counterpartyof aLoanSet, theSponsorof a sponsored transaction; §6.1) — or, for aBatch, the outer account's quorum plus a satisfied authorization for every participant account. ItsProposedTransactionfield is then a valid signed transaction that anyone can copy and submit.
3.2. Lifecycle¶
TransactionProposalCreate
Proposer ───────────────────────────────────► [ TransactionProposal: pending ]
│
Signer A ── TransactionProposalSign ─────────────────────► │ (weight 3 / quorum 6)
Signer B ── TransactionProposalSign ─────────────────────► │ (weight 6 / quorum 6) → complete
│
anyone reads the proposal, sets the proposed
transaction's Signers field to the collected
signatures, and submits it via the normal path
│
▼
proposed transaction executes (standard multi-sign
validation); consuming the target account's
TicketSequence auto-deletes the now-stale proposal
and refunds its reserve (§4.5)
At any point while the proposal is not terminal, the proposer may submit TransactionProposalCancel to abort it. Once the proposal is terminal (expired, or the proposed transaction's LastLedgerSequence has passed), it stops accepting signatures and any account may clean it up.
A proposal exists as a ledger object only until it is cancelled or cleaned up. Its full history — creation, each signature and the ledger it was recorded in, and the final outcome — remains permanently available in transaction metadata for compliance, audit, and reconciliation.
3.3. Design principles¶
- The ledger is the meeting room, not the executor. Signatures are collected and validated on-ledger; execution reuses the existing multi-sign submission path. No new execution semantics and no fourth transaction are introduced.
- Authority derives from the collected signatures, not from any submitter. Anyone can submit the completed transaction; the existing multi-sign machinery validates it against the target account's
SignerList. - Immutable payload. Once created, the proposed transaction cannot be modified. Signers sign exactly what they see.
- No new quorum model. Quorum, weights, and the "applicable
SignerList" are inherited unchanged from the existing multi-sign machinery — the account'sSignerList. - Every collected signature is pre-validated. The ledger verifies each signature (correct key, valid over the proposed transaction, signer on the
SignerList) as it is added, so a complete proposal is guaranteed to be a submittable transaction.
4. Ledger Entry: TransactionProposal¶
This object represents a pending multi-signature proposal. It holds the unsigned proposed transaction and the signatures collected for it so far.
4.1. Object Identifier¶
Key Space: 0x[TBD]
ID Calculation Algorithm:
ProposalID = hash( <TransactionProposal space key>, Account, TicketSequence )
where Account and TicketSequence are taken from the proposed transaction: Account is the target account, and TicketSequence is the ticket it spends. Nothing else contributes to the ID — not the rest of the payload, and not any signature field — so the ID is fixed at creation and never changes as signatures accumulate. This value is the ProposalID referenced by TransactionProposalSign and TransactionProposalCancel.
Since the ID depends only on the target account and its TicketSequence, any transaction that consumes that ticket lets the ledger rebuild the ID and delete the stale proposal (§4.5).
The trade-off: only one live proposal can exist per (target account, ticket). A second TransactionProposalCreate for the same pair fails with tecDUPLICATE (§5.3.2), whatever its payload or proposer. Only one of them could ever execute anyway, so this costs nothing in practice — and a proposer wanting several concurrent proposals just uses a different TicketSequence for each (§9.2).
4.2. Fields¶
| Field Name | Constant | Required | Internal Type | Default Value | Description |
|---|---|---|---|---|---|
LedgerEntryType |
Yes | Yes | UINT16 | TransactionProposal |
Identifies this as a TransactionProposal object. |
Flags |
No | Yes | UINT32 | 0 |
Flag values associated with this object. No flags are currently defined. |
Owner |
Yes | Yes | ACCOUNT | N/A | The proposer — the account that created and owns this object and pays its reserve. |
ProposedTransaction |
No | Yes | STOBJECT | N/A | The proposed transaction. Immutable except for its signature fields (Signers; CounterpartySignature/SponsorSignature for a type that requires one; and BatchSigners for a Batch), into which collected signatures accumulate (see §4.2.1). |
Expiration |
Yes | Yes | UINT32 | N/A | Ledger close-time (seconds since the Ripple Epoch) after which the proposal stops accepting signatures and becomes terminal. |
OwnerNode |
Yes | Yes | UINT64 | N/A | Hint for which page this object appears on in the owner directory. |
PreviousTxnID |
No | Yes | HASH256 | N/A | Hash of the previous transaction that modified this object. |
PreviousTxnLgrSeq |
No | Yes | UINT32 | N/A | Ledger sequence of the previous transaction that modified this object. |
4.2.1. ProposedTransaction¶
ProposedTransaction is the proposed transaction the proposal collects signatures for. Every field of it is immutable for the life of the proposal except its signature fields — the top-level SigningPubKey/TxnSignature (filled only when the target account signs with its own key, §6.1.2); Signers; the auxiliary co-signature field(s) a type requires (CounterpartySignature, SponsorSignature); and, for a Batch, BatchSigners — into which the ledger inserts each validated signature (see §4.2.2). TxnSignature, Signers, and BatchSigners are excluded from the XRPL signing payload, so appending a Signers/BatchSigners entry or filling TxnSignature never changes what any other signer signed over. The top-level SigningPubKey is the one exception: it is part of the signing payload, so filling it (§6.1.2) changes the data that Signers, CounterpartySignature, and SponsorSignature are signed over — the whole-transaction slots. (BatchSigners entries are unaffected: XLS-56 signs them over a separate batch-specific payload that never includes the top-level SigningPubKey, XLS-56 §2.1.3.2.) To keep every previously-collected whole-transaction signature valid, the ledger only accepts a top-level single-sign contribution as the first Signers/CounterpartySignature/SponsorSignature signature recorded on the proposal (§6.1.2, §6.3.2) — once any of those exists, SigningPubKey is fixed at empty for the rest of the proposal's life, and the target account (or Delegate) can still authorize by contributing a Signers entry instead. A target account with no SignerList has no such fallback — a single-signature is its only authorization path — so it must contribute that signature before any Counterparty/Sponsor co-signer does, or the proposal becomes permanently unsatisfiable and must be cancelled and recreated (§7).
The proposed transaction:
- Must be submitted unsigned: at creation its
SigningPubKeyfield must be an empty string (""), and itsTxnSignature,Signers,CounterpartySignature,SponsorSignature, and (for aBatch)BatchSignersfields must be omitted. (Fields that define an auxiliary party — e.g.Counterparty, orSponsor/SponsorFlags— are ordinary payload fields and must be present at creation if used; only the signature containers are collected on-chain.) This is the exact canonical form over which signers produce their signatures; the ledger populates the signature fields as they arrive. If it is aBatch, itsRawTransactionsmust follow the XLS-56 rules for inner transactions (each unsigned, with thetfInnerBatchTxnflag). - Must specify a
TicketSequencefor its target account and must not specifySequence. Requiring a ticket decouples the proposed transaction from the target account's live sequence, so unrelated target-account activity cannot invalidate the proposal while signatures are being collected (see §9.2). While the proposal exists, that ticket is reserved: only the proposed transaction may spend it, and any other transaction that tries fails withterTICKET_RESERVED. A transaction counts as the proposed transaction when every field except the signature fields matchesProposedTransaction, so the verbatim copy from §6.5 qualifies even if the submitter drops surplus signatures. This is distinct from the existingterPRE_TICKET, which covers aTicketSequencethat does not yet exist; here theTicketexists but is temporarily unavailable because a live proposal holds it, so a dedicated code is needed to describe that condition. Deleting the proposal (§4.5) lifts the reservation, which is why the code is a retriableter. - Must carry a
Feebig enough for all the signatures the proposal will collect. The payload is immutable, so the fee is fixed before anyone signs, but the minimum fee grows with each signature:(1 + |Signers|) × base_feefor a multi-signed transaction, plusmax(1, |CounterpartySignature.Signers|) × base_feeif the transaction has aCounterparty(XLS-66 §3.8.1.1 — a single-signing counterparty still costs onebase_fee), plus|SponsorSignature.Signers| × base_feeif it has aSponsor(XLS-68 — a single-signing sponsor adds nothing, since only the nestedSignerscount toward the minimum), and(n + 2) × base_fee + Σ inner feesfor aBatchcarryingnsignatures (XLS-56 §2.2). TheFeeis therefore a signature budget:TransactionProposalSignrejects any contribution that would push the minimum above it (§6.3.2), which is what keeps a complete proposal submittable verbatim (§6.5). Declare a largerFeeto leave room for extra signers. The fee is paid by the transaction's normal fee payer: usually the target account (the proposed transaction'sAccount), but theDelegateif the transaction is delegated (XLS-75), or theSponsorif its fee is sponsored (XLS-68). See §13.7. - Must be a transaction that can be independently multi-signed and submitted through the ordinary path. In particular it must not be:
- a
TransactionProposalCreate,TransactionProposalSign, orTransactionProposalCancel(no nesting of proposals); - a pseudo-transaction (
EnableAmendment,SetFee,UNLModify), which no account originates or signs; or - a transaction carrying the
tfInnerBatchTxnflag, which is only valid inside aBatch'sRawTransactionsand is never submittable standalone (a proposedBatch's inner transactions still carry it, per the rule above; the proposed transaction itself must not). - May include a
LastLedgerSequence. If present, it bounds the window during which the completed transaction can be submitted, and it acts as a second termination bound for the proposal (see §4.5): once the current ledger sequence exceeds it, the proposed transaction can never be applied (it would fail withtefMAX_LEDGER), so the proposal becomes terminal and permissionlessly cleanable.
The target account is the proposed transaction's Account field. It may differ from the proposer.
4.2.2. Collected signatures¶
Signatures are stored directly in the proposed transaction's own native signature fields — there is no separate signatures field on the proposal object. This means a complete proposal requires no assembly at all: the ProposedTransaction field is already a valid, fully-signed transaction that can be copied verbatim and submitted. Where a signature lands depends on the proposed transaction type:
- Ordinary transaction: into
ProposedTransaction.Signers, the standard multi-signSignersarray, authorizing the target account (the transaction'sAccount) — or, if that account signs with its own key, directly into the proposed transaction's top-levelSigningPubKey/TxnSignature(§6.1.2). Batch(XLS-56): authorization of the outer account (the Batch'sAccount) goes intoProposedTransaction.Signers; each other participant account (an account with inner transactions inRawTransactions) is authorized by an entry inProposedTransaction.BatchSigners, which holds at most 24 entries. A single-signature participant's entry carriesSigningPubKey/TxnSignaturedirectly; a multi-signing participant's entry carries a nestedSignersarray. This mirrors XLS-56 §2.1.3.- Auxiliary co-signature (e.g.
LoanSet, XLS-66; sponsored transactions, XLS-68): a transaction that requires a second party to co-authorize carries a dedicated signature field for that party —CounterpartySignaturefor theCounterparty,SponsorSignaturefor theSponsor. Each party's signature goes into its own field (SigningPubKey/TxnSignaturefor a single-signature party, or a nestedSignersarray for a multi-signing one), while the transaction's ownAccountis authorized throughProposedTransaction.Signersas above. A transaction may require more than one. See §6.1.
Every Signers array (top-level or nested in a BatchSigner) is kept sorted by Account and holds at most 32 entries (the maximum SignerList size). BatchSigners is also sorted by Account and holds at most 24 entries. Weights are not stored: a signer's weight and the relevant quorum are always read from the applicable account's SignerList, both when a signature is added and when the transaction is finally submitted (see §9.3). Clients compute "remaining weight to quorum" by joining the collected signatures against the relevant SignerList(s). §6.1 describes how TransactionProposalSign routes a signature from its SigningFor account and ProposalSignature.Account.
4.3. Ownership¶
Owner: Owner (the proposer).
Directory Registration: The object is registered in the Owner's owner directory.
4.4. Reserves¶
Reserve Requirement: Custom (flat). A TransactionProposal holds a full transaction plus its collected signatures, so it reserves more than a typical ledger entry.
- Ordinary proposed transaction: 5 owner-reserve increments (currently 1 XRP).
Batchproposed transaction: 10 owner-reserve increments (currently 2 XRP), reflecting its larger footprint (up to 8 inner transactions and signatures for multiple participant accounts).
Each increment is the standard owner-reserve amount (currently 0.2 XRP, subject to Fee Voting). There is no separate reserve mechanism: creating a proposal adds 5 (or 10) to the owner's OwnerCount instead of 1, and deleting it subtracts the same amount (§5.4, §6.4, §7.5).
4.5. Deletion¶
Terminal proposal: A proposal is terminal when it stops accepting new signatures and becomes permissionlessly cleanable, i.e. when any of the following is true relative to the parent ledger:
- The parent ledger's close time is at or after
Expiration; or - The proposed transaction includes a
LastLedgerSequenceand the current ledger sequence is greater than it; or - The proposed transaction's target account no longer exists, having been removed by
AccountDelete.AccountDeletedeletes the target's owned objects outright rather than spending them, so it also removes the reservedTicketSequencewithout any transaction ever specifying that ticket as consumed — the automatic cleanup described below, which keys off a consumedTicketSequence, never fires, so this condition is what lets the orphaned proposal be cleaned up instead.
A terminal proposal exists in ledger state only until it is cleaned up.
"Terminal" describes the proposal object, not the signatures it holds. Expiration belongs to the proposal, not to the proposed transaction, so it is not part of what anyone signed and does not bound submission: a proposal that was already complete when it expired still holds a fully signed transaction anyone can copy and submit (§8.1.2, §13.4). Only the proposed transaction's own LastLedgerSequence, spending its TicketSequence, or deletion of its target account (which removes the TicketSequence without spending it), makes it unsubmittable.
Deletion Transactions: TransactionProposalCancel, TransactionProposalSign, and — implicitly — any transaction of the target account that consumes the proposed transaction's TicketSequence (see below).
Deletion Conditions: The object is deleted when any one of the following occurs:
- Owner cancellation (non-terminal): while the proposal is not terminal, the
Owner(the proposer) may delete it viaTransactionProposalCancel(resulttesSUCCESS). - Target-account cancellation (any time): the target account may delete any proposal made for it via
TransactionProposalCancel, whether or not it is terminal and no matter how many signatures have been collected (resulttesSUCCESS). See §7.2. - Permissionless cleanup (terminal): once the proposal is terminal, any account may delete it via
TransactionProposalCancel(resulttesSUCCESS, since deletion is that transaction's intended action). - Incidental cleanup by a late signer: a
TransactionProposalSignsubmitted against a terminal proposal fails withtecEXPIRED— its intended action (recording a signature) cannot happen — but, as a side effect of that claimed-fee result, it deletes the terminal proposal and releases the reserve (see §6.4). - Automatic cleanup when the proposed transaction executes: the reserved
TicketSequencecan only be consumed by the proposal's own proposed transaction (§4.2.1), so this is the only way a ticket consumption deletes a proposal — running the completed transaction spends the ticket, and the ledger looks uphash(<space key>, Account, <consumed ticket>)(§4.1) and deletes the matching proposal, refunding theOwner's reserve.
This removes only the leftover object. Signatures already copied off-ledger stay valid and submittable until the TicketSequence is consumed (§13.4).
Account Deletion Blocker: Yes. A TransactionProposal object must be deleted before its owner account can be deleted.
4.6. Invariants¶
Expirationis always present and non-zero.- Every entry in
ProposedTransaction.Signersis unique byAccount, and the array is sorted byAccountwith at most 32 entries. - Every entry in
ProposedTransaction.BatchSigners, if present, is unique byAccount, and the array is sorted byAccountwith at most 24 entries. - Every entry in
ProposedTransaction.Signersis a signature that was cryptographically valid over the proposed transaction (excluding itsSignersfield) at the time it was added. - Only the proposed transaction's signature fields change over the life of the proposal — its top-level
SigningPubKey/TxnSignature(empty at creation; filled only when the target account signs with its own key, §6.1.2),Signers,CounterpartySignature,SponsorSignature, andBatchSigners. Every non-signature field is fixed at creation.
4.7. RPC Name¶
RPC Type Name: transaction_proposal
4.8. Example JSON¶
{
"LedgerEntryType": "TransactionProposal",
"Flags": 0,
"Owner": "rPROPOSER........................",
"Expiration": 800000000,
"ProposedTransaction": {
"TransactionType": "Payment",
"Account": "rTARGET..........................",
"Destination": "rDEST............................",
"Amount": "5000000000",
"TicketSequence": 1201,
"Fee": "30",
"SigningPubKey": "",
"Signers": [
{
"Signer": {
"Account": "rCEO............................",
"SigningPubKey": "03AB...",
"TxnSignature": "3045..."
}
}
]
},
"OwnerNode": "0000000000000000",
"PreviousTxnID": "F3B1...",
"PreviousTxnLgrSeq": 12345678
}
5. Transaction: TransactionProposalCreate¶
Creates a TransactionProposal object holding an unsigned proposed transaction, placing it in a pending state visible to all signers.
5.1. Fields¶
| Field Name | Required? | JSON Type | Internal Type | Default Value | Description |
|---|---|---|---|---|---|
TransactionType |
✔️ | string | UINT16 | TransactionProposalCreate |
Identifies this as a TransactionProposalCreate transaction. |
Account |
✔️ | string | ACCOUNT | N/A | The proposer submitting the proposal. |
ProposedTransaction |
✔️ | object | STOBJECT | N/A | The unsigned proposed transaction (see §4.2.1). |
Expiration |
✔️ | number | UINT32 | N/A | Ledger close-time after which the proposal stops accepting signatures. |
Standard common fields (Fee, Sequence, Flags, Memos, SourceTag, signing fields) apply. Memos and SourceTag MAY be used to attach a reason code or reconciliation identifier to the proposal.
5.1.1. Creation authorization¶
Only the target account, or one of its signers, may create a proposal — and when ProposedTransaction.Delegate is present, the Delegate and its signers stand in for the target account. The Account submitting TransactionProposalCreate must therefore be either:
- the target account itself, or a member of the target account's applicable
SignerList, whenProposedTransaction.Delegateis absent; or - the
Delegateaccount itself, or a member of theDelegate's applicableSignerList, whenProposedTransaction.Delegateis present.
Creation authorization is checked against the current ledger when the TransactionProposalCreate transaction is applied. It does not add a signature to ProposedTransaction; the proposal begins unsigned and still requires the normal signature collection and submission-time authorization checks.
5.2. Transaction Fee¶
Fee Structure: Standard. This transaction uses the standard transaction fee (currently 10 drops, subject to Fee Voting changes). Note that the proposed transaction's own Fee is not charged here; it is charged to that transaction's own fee payer when the completed transaction is submitted (§4.2.1, §13.7).
5.3. Failure Conditions¶
5.3.1. Data Verification¶
Except for missing required fields, Data Verification failures return a tem-level error.
- If present,
ProposedTransactionis not a well-formed transaction of a known type (temMALFORMED).ProposedTransactionis required by the transaction format, so if it is missing, deserialization fails before preflight; submission returns a parse error rather than a transaction result, and no fee is charged. - The proposed transaction fails the stateless format checks (preflight) for its own transaction type. These are the same checks it would receive if submitted directly, except checks that fail because a required signature is absent — such as XLS-56 §2.3 rule 3.6 (a
Batchmissing aBatchSignersentry for a required participant) or XLS-66's check that aLoanSetoutside aBatchcarries aCounterpartySignature— which are skipped, since every signature slot is empty at creation by design (§4.2.1). Checks that instead fail because a signature-bearing field is unexpectedly present still run unchanged: XLS-56 §2.3 rules 2.5–2.7 continue to reject an inner transaction carryingTxnSignature, a non-emptySigningPubKey, or aSignersarray (temBAD_SIGNATURE/temBAD_REGKEY/temBAD_SIGNER), enforcing that every inner transaction is unsigned per §4.2.1. If any check fails, the proposal returns thetemcode from that transaction type's preflight. Running these checks at creation is cheap and rejects malformed payloads immediately, instead of letting an invalid proposal gather signatures only to fail later. State-dependent (preclaim) checks are not run here; they are evaluated when the completed transaction is submitted. - The proposed transaction is not unsigned. The result depends on the signature field and its contents:
- A non-empty
SigningPubKeyreturnstemINVALID. (At creation the payload must carry no signing key at all; the target account's own key is only filled in later, byTransactionProposalSign, §6.1.2.) - A non-empty
TxnSignature, orSignersentries carrying real signatures, returnstemINVALID. - A proposed
LoanSetwhoseCounterpartySignatureholds a real signature returnstemINVALID. - A non-empty
SponsorSignaturereturnstemINVALID. (A mismatch with the payload's ownSponsor/SponsorFlagsfields is already caught by its preflight, check 2 above.) - The proposed transaction cannot be independently submitted through the ordinary multi-sign path — it is itself a
TransactionProposalCreate,TransactionProposalSign, orTransactionProposalCancel; or a pseudo-transaction (EnableAmendment,SetFee,UNLModify) (temINVALID). - The proposed transaction carries the
tfInnerBatchTxnflag. IffeatureBatchV1_1is disabled, this returnstemINVALID_FLAG; otherwise, it returnstemINVALID_INNER_BATCH. - The proposed transaction does not specify
TicketSequence, or specifiesSequenceinstead of or in addition toTicketSequence(temSEQ_AND_TICKET). Expirationis zero (temBAD_EXPIRATION).Expirationis required by the transaction format, so if it is missing, deserialization fails before preflight; submission returns a parse error rather than a transaction result, and no fee is charged.
5.3.2. Protocol-Level Failures¶
Expirationis already at or before the parent ledger's close time (tecEXPIRED).- The proposed transaction includes a
LastLedgerSequencethat is already at or before the current ledger sequence (tecEXPIRED). - The proposer has insufficient reserve to own the new
TransactionProposalobject (tecINSUFFICIENT_RESERVE). - The target account (the proposed transaction's
Account) does not exist (tecNO_TARGET). - The target account is a pseudo-account (e.g. an AMM, Vault, or LoanBroker pseudo-account) and therefore cannot authorize a transaction through a
SignerList(tecNO_PERMISSION). - A
TransactionProposalwith the sameProposalIDalready exists — i.e. a live proposal (owned by anyone) already targets the same account with the sameTicketSequence(tecDUPLICATE, §4.1). - The proposed transaction's
TicketSequenceis not a validTicketof the target account (tefNO_TICKET). - The proposer is neither the target account — or the
Delegate, when one is present — nor a member of that account's applicableSignerList(tecNO_PERMISSION).
5.4. State Changes¶
On Success (tesSUCCESS):
- Creates a new
TransactionProposalledger object whoseOwneris the sendingAccount. The proposed transaction is stored with no signatures yet. - Increments the
Owner'sOwnerCountby 5, or by 10 for aBatch(§4.4), so the standard reserve formula charges the elevated reserve. Every path that deletes the proposal subtracts the same amount.
5.5. Example JSON¶
{
"TransactionType": "TransactionProposalCreate",
"Account": "rPROPOSER........................",
"Fee": "10",
"Sequence": 42,
"Expiration": 800000000,
"ProposedTransaction": {
"TransactionType": "Payment",
"Account": "rTARGET..........................",
"Destination": "rDEST............................",
"Amount": "5000000000",
"TicketSequence": 1201,
"Fee": "30",
"SigningPubKey": ""
}
}
6. Transaction: TransactionProposalSign¶
Appends one signature toward the proposed transaction to the proposal. A single, uniform contribution — SigningFor + ProposalSignature — supplies a signature for one account the proposed transaction requires authorization from. The ledger derives everything else from the proposed transaction and from ProposalSignature.Account: where the signature is recorded, and whether it is a single- or multi-signature. The contributed signature is nested in ProposalSignature so it does not conflict with the standard SigningPubKey/TxnSignature fields that authorize the TransactionProposalSign transaction itself. The outer Account only submits and pays for TransactionProposalSign; it does not identify the signer. If the proposal is already terminal, this transaction cannot record a signature and instead fails with tecEXPIRED, deleting the terminal proposal as a side effect (see §6.4).
6.1. Fields¶
| Field Name | Required? | JSON Type | Internal Type | Default Value | Description |
|---|---|---|---|---|---|
TransactionType |
✔️ | string | UINT16 | TransactionProposalSign |
Identifies this as a TransactionProposalSign transaction. |
Account |
✔️ | string | ACCOUNT | N/A | The account submitting and paying for this transaction. It does not need to be the signer identified by ProposalSignature.Account. |
ProposalID |
✔️ | string | HASH256 | N/A | The ID of the TransactionProposal being signed. |
SigningFor |
✔️ | string | ACCOUNT | N/A | An account in the proposed transaction that requires a signature for it to be valid — i.e. any of its signature slots (§6.1.1). |
ProposalSignature |
✔️ | object | STOBJECT | N/A | The contributed signature: Account, SigningPubKey, and TxnSignature over SigningFor's signing data (§6.1.2). |
ProposalSignature contains three required fields: Account, SigningPubKey, and TxnSignature. ProposalSignature.Account is the signer account ID used for authorization checks and for the stored signature entry. The outer transaction Account can be any funded account willing to submit and pay the fee.
6.1.1. SigningFor — which account is being authorized, and where the signature lands¶
SigningFor answers the question: whose approval is this signature providing? It must name an account that the proposed transaction needs a signature from. In other words, SigningFor must point to one of the proposed transaction's signature slots:
- the proposed transaction's own
Accountwhen noDelegateis present, or theDelegateinstead of theAccountwhen permission delegation is used — the two are mutually exclusive, matching how a delegated transaction'sSignersare validated (§6.3.2); - the
Counterparty, if that transaction type has one (for example, aLoanSet(XLS-66) lender; if omitted, this defaults to theLoanBroker.Owner, XLS-66 §3.8); - the
Sponsor, if the transaction is sponsored (XLS-68); - for a
Batch, any account the batch needs a signature from — the set XLS-56 §2.1.3.1 defines: each inner transaction'sAccount, or itsDelegateinstead of that account when the inner transaction is delegated, plus any other account the inner transaction would need, such as aCounterpartyor a co-signingSponsor. These are the batch participants. XLS-56 requiresBatchSignersto match this set exactly, so an extra entry would make the completed transaction fail withtemBAD_SIGNER;TransactionProposalSignrejects one up front (§6.3.2).
If SigningFor does not match one of these required accounts, the transaction fails with tecNO_PERMISSION. When it does match, the ledger records the signature in the location that corresponds to that account's role:
If SigningFor is the transaction's… |
The signature is recorded in… |
|---|---|
Account / Delegate |
ProposedTransaction.Signers (or top-level, see §6.1.2) |
Counterparty |
ProposedTransaction.CounterpartySignature |
Sponsor |
ProposedTransaction.SponsorSignature |
Batch participant |
ProposedTransaction.BatchSigners[SigningFor] |
Usually these roles belong to different accounts, so one contribution fills one slot. When one account holds two of them, what matters is whether the slots are signed over the same data:
- Same signing payload → one contribution fills both. An ordinary transaction's
CounterpartyandSponsorslots are signed over the same data (the transaction minus its signature fields), so one signature is valid for both and is recorded in each. Each slot is still checked separately, and §8.1.2 reports one row per slot. - Different signing payloads → one contribution each. A
BatchSignersentry is signed over the XLS-56 batch payload (§2.1.3.2), which no other slot uses, so aBatchSignersignature can never double as aSponsorSignature. An account that is both a proposedBatch's feeSponsorand one of its participants sends twoTransactionProposalSigntransactions. No selector field is needed to tell them apart: the ledger records the contribution in whichever ofSigningFor's slots the signature verifies against.
Inside a Batch the first case never arises: every participant authorization — an inner transaction's account or Delegate, an inner Counterparty, an inner co-signing Sponsor — goes into that account's single BatchSigners entry, not into a CounterpartySignature/SponsorSignature on the inner transaction.
6.1.2. Single- vs multi-signature — derived from ProposalSignature.Account¶
The transaction does not include a flag that says whether the contribution is a single-signature or a multi-signature share. The ledger determines that from the relationship between ProposalSignature.Account and SigningFor:
ProposalSignature.Account==SigningFor→ single-signature. The account is signing for itself using its master key or regular key.ProposalSignature.SigningPubKeymust be a valid key forSigningFor. This one signature fully authorizesSigningFor. The ledger stores it directly asSigningPubKey/TxnSignature: at the proposed transaction's top level for the mainAccountorDelegate, or inside the relevantCounterparty,Sponsor, orBatchparticipant signature slot.
A top-level single-signature (for the proposed transaction's own Account/Delegate) is special: unlike BatchSigners, the top-level SigningPubKey is part of the signing payload that Signers, CounterpartySignature, and SponsorSignature cover, so filling it changes the data those slots' signatures verify against. To guarantee previously-collected signatures never invalidate, the ledger accepts this contribution only when no Signers, CounterpartySignature, or SponsorSignature entry has yet been recorded on the proposal — a Batch outer account may still single-sign after BatchSigners entries exist, since those are unaffected (§4.2.1) (§6.3.2). A target account that wants to single-sign must therefore do so before requesting any whole-transaction co-signature; if it signs later, it contributes a Signers entry instead (below), which leaves SigningPubKey empty and never disturbs the payload. A target account with no SignerList has no Signers fallback, so it must single-sign first, before any Counterparty/Sponsor co-signer — otherwise the proposal can never be completed and has to be cancelled (§4.2.1, §7).
ProposalSignature.Account!=SigningFor→ multi-signature share.ProposalSignature.Accountis contributing one multi-signature share forSigningFor. It must be inSigningFor's applicableSignerList. The ledger stores the contribution as a standardSignerentry ({Account, SigningPubKey, TxnSignature}) in the relevantSignersarray:ProposedTransaction.Signersfor the main account, or the nestedSignersarray inside theCounterpartySignature,SponsorSignature, or participantBatchSignerslot. These entries are kept sorted and deduplicated byAccount. More shares may be added untilSigningFor's quorum is reached.
6.2. Transaction Fee¶
Fee Structure: Standard. The submitter pays the standard fee for this transaction. The proposed transaction's own Fee is not charged here; it is charged to that transaction's own fee payer when the completed transaction is submitted (§4.2.1, §13.7).
6.3. Failure Conditions¶
6.3.1. Data Verification¶
All Data Verification failures return a tem-level error.
ProposalID,SigningFor, andProposalSignature(with itsAccount,SigningPubKey, andTxnSignaturesub-fields) are required by the transaction format, so if any is missing, deserialization fails before preflight; submission returns a parse error rather than a transaction result, and no fee is charged.ProposalIDis present but malformed (temMALFORMED).ProposalSignature.SigningPubKeyis not a well-formed public key (temBAD_SIGNATURE). Only the encoding is checked here. Whether that key is authorized forSigningFor, and whetherTxnSignatureverifies, both need the ledger — the key has to be matched against a live regular key orSignerList, and the data the signature covers is the storedProposedTransaction, whichTransactionProposalSigndoes not carry.
6.3.2. Protocol-Level Failures¶
- No
TransactionProposalobject exists with the givenProposalID(tecNO_ENTRY). - The proposal is terminal — its
Expirationhas passed, or the proposed transaction'sLastLedgerSequencehas passed (tecEXPIRED). This is a claimed-fee failure: no signature is recorded, but the terminal proposal is deleted as a side effect (see §6.4). This condition is checked before the authorization conditions below. Unlike the other checks in this list, it is evaluated indoApplyrather than preclaim: atecresult from preclaim never reachesdoApply, so preclaim has no opportunity to delete anything. Implementing the side effect therefore requiresltTransactionProposalto be added to thetecEXPIREDpersistent-deletion allow-list thatdoApplyconsults (alongsideltNFTokenOfferandltCredential), not merely a preclaim-time terminal check. SigningForis not an account the proposed transaction needs a signature from — not itsAccount/Delegate,Counterparty, orSponsor, and not, for aBatch, a participant as defined in §6.1.1 (tecNO_PERMISSION). This includes the top-levelAccountwhen the proposed transaction has aDelegate(submission validatesSignersagainst theDelegatealone, so a share for the plainAccountwould never be checked), and an inner account that itsDelegatereplaces: since XLS-56 requiresBatchSignersto match the required set exactly, both are refused here instead of failing at submission.- The signer is not authorized: for single-signing,
ProposalSignature.SigningPubKeyis notSigningFor's master or regular key; for multi-signing,ProposalSignature.Accountis not onSigningFor's applicableSignerList, orProposalSignature.SigningPubKeyis not valid forProposalSignature.Accountunder standard multi-sign rules (tecNO_PERMISSION). ProposalSignature.TxnSignatureis not valid over any signing payloadSigningForowes for the stored proposed transaction (§6.1.1, §6.1.2) (tefBAD_SIGNATURE). This is the stateful half of §6.3.1.3 — the payload is only available onceProposalIDhas been resolved. The contribution is recorded in the slot whose payload it verifies against.- The contribution duplicates one already recorded in the same mode: it is a multi-signature share and
ProposalSignature.Accountis already present in that destination'sSignersarray, or it is a single-signature and a single-signature entry forSigningForalready exists (tecDUPLICATE). (The sameProposalSignature.Accountmay still sign for a differentSigningFor, or for a different slot of the sameSigningForunder a different payload.) - The contribution conflicts with the existing authorization mode for
SigningFor— a multi-signature share when a single-signature entry is already recorded, or a single-signature when aSignersarray already has at least one entry (tecNO_PERMISSION). (Every mode mismatch is covered here, not #6, even where a duplicateAccountis also involved.) - The contribution is a top-level single-signature for the proposed transaction's own
Account/Delegate(§6.1.2), and aSigners,CounterpartySignature, orSponsorSignatureentry has already been recorded on the proposal (tecNO_PERMISSION). Filling the top-levelSigningPubKeychanges the signing payload those whole-transaction slots sign over, so this contribution is only accepted before any of them exist; a target account signing later must contribute aSignersentry instead.BatchSignersentries do not trigger this condition (§4.2.1). - Adding the share would exceed the maximum of 32 entries in the destination
Signersarray, or would add aBatchSignerpast the 24-entryBatchSignerslimit (tecOVERSIZE). - The contribution would leave the proposed transaction's
Feebelow the minimum for the signatures it would then carry (§4.2.1) (tecINSUFF_FEE). Checked on every contribution, so a proposal never collects more signatures than its fee pays for.
6.4. State Changes¶
On Success (tesSUCCESS):
- Validates the contribution and records it into the destination for
SigningFor's role and mode (§6.1.1, §6.1.2): a single-signature is written directly (top-level for the main account, or that slot'sSigningPubKey/TxnSignature), and a multi-signature share is appended as aSignerentry into the relevantSignersarray. AnySignersarray is kept sorted byAccount. - Re-checks the proposed transaction's
Feeagainst the signatures it now carries (§4.2.1), so the declared fee always covers the whole set. - No execution occurs. Once the collected signatures satisfy every signing requirement for the proposed transaction — the target account's quorum, plus a satisfied signature for each
Counterparty/Sponsorthe transaction requires, or, for aBatch, the outer account's quorum plus a satisfied authorization for every participant account — the proposal is complete: theProposedTransactionfield is a valid signed transaction that anyone can copy and submit (see §6.5).
On failure against a terminal proposal (tecEXPIRED):
- No signature is recorded. The terminality check runs in
doApply(§6.3.2 #2), so thetecresult it produces still reaches the ledger-modification step: the terminal proposal object is deleted and theOwner'sOwnerCountis decremented by 5, or 10 for aBatch(§4.4), releasing the reserve as a side effect. This requiresTransactionProposalto be added to the small set of ledger entry typesdoApplyis permitted to delete on atecEXPIREDresult. A signer whoseTransactionProposalSignarrives after the proposal has expired therefore both fails and cleans up in one step.
6.5. Submitting the completed transaction¶
This specification introduces no on-ledger execution step, and no assembly is required. Once a proposal is complete, any observer simply:
- Reads the
TransactionProposalbyProposalID. - Copies the
ProposedTransactionverbatim — it already contains the collectedSigners(and, for aBatch,BatchSigners), sorted, and is a fully-formed signed transaction. - Submits it through the ordinary transaction path (e.g. the
submitAPI).
The existing multi-sign (and, for a Batch, BatchSigners) validation then checks the signatures against the applicable accounts' current SignerList(s) and applies the transaction, charging its Fee to that transaction's fee payer (§4.2.1). No field of On-Chain Cosigner appears on the submitted transaction — it is an ordinary transaction. Applying it consumes the target account's TicketSequence, which auto-deletes the proposal and refunds its reserve (§4.5).
6.6. Example JSON¶
The TransactionProposalSign transaction is trivial — SigningFor plus one signature. What matters is how it mutates the TransactionProposal object, so §6.6.1 walks through one case with the full object shown before and after every signature. The remaining variants (§6.6.2–§6.6.4) show only the TransactionProposalSign JSON and the resulting fragment of ProposedTransaction, since the routing rules are already covered by §6.1.1's table.
6.6.1. Ordinary transaction — multi-sign shares accumulate to quorum¶
Setup. A Payment proposal for target account rTARGET, whose applicable SignerList is { rCEO: 4, rCFO: 3 } with SignerQuorum 6. Its Fee of 30 drops is the signature budget (§4.2.1) — (1 + 2) × base_fee, enough for both members to sign. Freshly created, it holds no signatures:
// TransactionProposal — before any signature · status: pending · signed_weight 0 / quorum 6
{
"LedgerEntryType": "TransactionProposal",
"Flags": 0,
"Owner": "rPROPOSER........................",
"Expiration": 800000000,
"ProposedTransaction": {
"TransactionType": "Payment",
"Account": "rTARGET..........................",
"Destination": "rDEST............................",
"Amount": "5000000000",
"TicketSequence": 1201,
"Fee": "30",
"SigningPubKey": ""
},
"OwnerNode": "0000000000000000",
"PreviousTxnID": "F3B1000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 12345678
}
rCEO signs. ProposalSignature.Account (rCEO) ≠ SigningFor (rTARGET) → multi-sign:
// TransactionProposalSign carrying rCEO's proposal signature
{
"TransactionType": "TransactionProposalSign",
"Account": "rCEO............................",
"Fee": "10",
"Sequence": 7,
"ProposalID": "C1A2B3D4E5F6...............................",
"SigningFor": "rTARGET..........................",
"ProposalSignature": {
"Account": "rCEO............................",
"SigningPubKey": "03AB...",
"TxnSignature": "3045..."
}
}
The object gains one ProposedTransaction.Signers entry. Weight 4 < quorum 6, so it stays pending:
// TransactionProposal — after rCEO · status: pending · signed_weight 4 / quorum 6
{
"LedgerEntryType": "TransactionProposal",
"Flags": 0,
"Owner": "rPROPOSER........................",
"Expiration": 800000000,
"ProposedTransaction": {
"TransactionType": "Payment",
"Account": "rTARGET..........................",
"Destination": "rDEST............................",
"Amount": "5000000000",
"TicketSequence": 1201,
"Fee": "30",
"SigningPubKey": "",
"Signers": [
{
"Signer": {
"Account": "rCEO............................",
"SigningPubKey": "03AB...",
"TxnSignature": "3045..."
}
}
]
},
"OwnerNode": "0000000000000000",
"PreviousTxnID": "A1A1000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 12345690
}
rCFO signs (same shape, SigningFor: rTARGET, ProposalSignature.Account: rCFO). The new share is inserted sorted by Account, and weight 4 + 3 = 7 ≥ 6 → complete:
// TransactionProposal — after rCFO · status: complete
{
"LedgerEntryType": "TransactionProposal",
"Flags": 0,
"Owner": "rPROPOSER........................",
"Expiration": 800000000,
"ProposedTransaction": {
"TransactionType": "Payment",
"Account": "rTARGET..........................",
"Destination": "rDEST............................",
"Amount": "5000000000",
"TicketSequence": 1201,
"Fee": "30",
"SigningPubKey": "",
"Signers": [
{
"Signer": {
"Account": "rCEO............................",
"SigningPubKey": "03AB...",
"TxnSignature": "3045..."
}
},
{
"Signer": {
"Account": "rCFO............................",
"SigningPubKey": "02DE...",
"TxnSignature": "3044..."
}
}
]
},
"OwnerNode": "0000000000000000",
"PreviousTxnID": "B2B2000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 12345702
}
The ProposedTransaction field is now a valid multi-signed Payment; anyone can copy it and submit it (§6.5).
6.6.2. Ordinary transaction — single-sign with the account's own key¶
If rTARGET instead authorizes with its own key — ProposalSignature.Account == SigningFor (rTARGET) → single-sign — the signature fills the proposed transaction's top-level SigningPubKey/TxnSignature (no Signers array), and alone completes it:
// TransactionProposalSign carrying rTARGET's proposal signature
{
"TransactionType": "TransactionProposalSign",
"Account": "rTARGET..........................",
"Fee": "10",
"Sequence": 4,
"ProposalID": "C1A2B3D4E5F6...............................",
"SigningFor": "rTARGET..........................",
"ProposalSignature": {
"Account": "rTARGET..........................",
"SigningPubKey": "02FF...",
"TxnSignature": "3046..."
}
}
The proposed transaction's top level gains just the two signature fields — no Signers array is used:
// ProposedTransaction fragment — after rTARGET signs for itself · status: complete
{
"SigningPubKey": "02FF...",
"TxnSignature": "3046..."
}
6.6.3. Auxiliary co-signature — a LoanSet counterparty¶
Setup. A LoanSet proposal: borrower rBORROWER (target account) with the lender rLENDER as Counterparty. The borrower's account is collected into ProposedTransaction.Signers; the lender co-signs into ProposedTransaction.CounterpartySignature. Its Fee of 30 drops covers the borrower's signature plus the counterparty's (XLS-66 §3.8.1.1). Suppose the borrower's quorum is already met and only the lender is outstanding:
// TransactionProposal — before the lender signs · status: pending (CounterpartySignature missing)
{
"LedgerEntryType": "TransactionProposal",
"Flags": 0,
"Owner": "rBORROWER.......................",
"Expiration": 800000000,
"ProposedTransaction": {
"TransactionType": "LoanSet",
"Account": "rBORROWER.......................",
"Counterparty": "rLENDER.........................",
"LoanBrokerID": "9F1E...",
"TicketSequence": 77,
"Fee": "30",
"SigningPubKey": "",
"Signers": [
{
"Signer": {
"Account": "rBORROWERKEY....................",
"SigningPubKey": "03BB...",
"TxnSignature": "3045..."
}
}
]
},
"OwnerNode": "0000000000000000",
"PreviousTxnID": "D4D4000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 12345710
}
The lender single-signs (SigningFor: rLENDER, ProposalSignature.Account: rLENDER). A new CounterpartySignature field appears, and every slot is now satisfied → complete:
// TransactionProposalSign carrying rLENDER's proposal signature
{
"TransactionType": "TransactionProposalSign",
"Account": "rLENDER.........................",
"Fee": "10",
"Sequence": 5,
"ProposalID": "E5E6...............................",
"SigningFor": "rLENDER.........................",
"ProposalSignature": {
"Account": "rLENDER.........................",
"SigningPubKey": "03CD...",
"TxnSignature": "3047..."
}
}
The proposed transaction gains a new top-level field for the auxiliary signature, alongside the borrower's existing Signers:
// ProposedTransaction fragment — after the lender signs · status: complete
{
"CounterpartySignature": {
"SigningPubKey": "03CD...",
"TxnSignature": "3047..."
}
}
A multi-sign lender would instead accumulate into CounterpartySignature.Signers — a nested array filling until the lender's own quorum is met, exactly like ProposedTransaction.Signers in §6.6.1. A SponsorSignature (for a sponsored transaction) behaves identically.
6.6.4. Batch — outer account plus participants¶
Setup. A multi-account Batch by outer account rOUTER, with inner transactions for rOUTER, rBOB, and rCAROL. Authorizations: the outer account rOUTER into ProposedTransaction.Signers; each other participant into ProposedTransaction.BatchSigners[account]. SignerLists: rOUTER = { rOUTERKEY: 1 } quorum 1; rBOB signs with its own key; rCAROL = { rCAROLKEY: 1 } quorum 1. Its Fee of 50 drops covers the three signatures it will collect — (3 + 2) × base_fee, with no inner fees (XLS-56 §2.2).
// TransactionProposal — before any signature · status: pending
{
"LedgerEntryType": "TransactionProposal",
"Flags": 0,
"Owner": "rPROPOSER2......................",
"Expiration": 800000000,
"ProposedTransaction": {
"TransactionType": "Batch",
"Account": "rOUTER..........................",
"Flags": 65536,
"TicketSequence": 500,
"Fee": "50",
"SigningPubKey": "",
"RawTransactions": [
{
"RawTransaction": {
"TransactionType": "Payment",
"Account": "rOUTER..........................",
"Destination": "rX..............................",
"Amount": "1000000",
"Flags": 1073741824,
"Sequence": 501,
"Fee": "0",
"SigningPubKey": ""
}
},
{
"RawTransaction": {
"TransactionType": "Payment",
"Account": "rBOB............................",
"Destination": "rY..............................",
"Amount": "2000000",
"Flags": 1073741824,
"Sequence": 88,
"Fee": "0",
"SigningPubKey": ""
}
},
{
"RawTransaction": {
"TransactionType": "Payment",
"Account": "rCAROL..........................",
"Destination": "rZ..............................",
"Amount": "3000000",
"Flags": 1073741824,
"Sequence": 12,
"Fee": "0",
"SigningPubKey": ""
}
}
]
},
"OwnerNode": "0000000000000000",
"PreviousTxnID": "A9C7000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 12345700
}
Three signatures arrive — one per account that must authorize:
// 1) rOUTERKEY signs for the outer account rOUTER (multi-sign) → ProposedTransaction.Signers
{
"TransactionType": "TransactionProposalSign",
"Account": "rOUTERKEY.......................",
"Fee": "10",
"Sequence": 3,
"ProposalID": "F0F0...............................",
"SigningFor": "rOUTER..........................",
"ProposalSignature": {
"Account": "rOUTERKEY.......................",
"SigningPubKey": "03A1...",
"TxnSignature": "3045..."
}
}
// 2) rBOB signs for itself (single-sign) → BatchSigners[rBOB]
{
"TransactionType": "TransactionProposalSign",
"Account": "rBOB............................",
"Fee": "10",
"Sequence": 9,
"ProposalID": "F0F0...............................",
"SigningFor": "rBOB............................",
"ProposalSignature": {
"Account": "rBOB............................",
"SigningPubKey": "02B2...",
"TxnSignature": "3044..."
}
}
// 3) rCAROLKEY signs for rCAROL (multi-sign) → BatchSigners[rCAROL].Signers
{
"TransactionType": "TransactionProposalSign",
"Account": "rCAROLKEY.......................",
"Fee": "10",
"Sequence": 4,
"ProposalID": "F0F0...............................",
"SigningFor": "rCAROL..........................",
"ProposalSignature": {
"Account": "rCAROLKEY.......................",
"SigningPubKey": "03C3...",
"TxnSignature": "3046..."
}
}
After all three, the outer account's quorum is met and every participant is authorized → complete. BatchSigners is sorted by Account; rBOB is a single-signature entry, rCAROL a nested multi-sign one. RawTransactions is unchanged from the setup above, so only the new signature fields are shown:
// ProposedTransaction fragment — after all three · status: complete
{
"Signers": [
{
"Signer": {
"Account": "rOUTERKEY.......................",
"SigningPubKey": "03A1...",
"TxnSignature": "3045..."
}
}
],
"BatchSigners": [
{
"BatchSigner": {
"Account": "rBOB............................",
"SigningPubKey": "02B2...",
"TxnSignature": "3044..."
}
},
{
"BatchSigner": {
"Account": "rCAROL..........................",
"Signers": [
{
"Signer": {
"Account": "rCAROLKEY.......................",
"SigningPubKey": "03C3...",
"TxnSignature": "3046..."
}
}
]
}
}
]
}
7. Transaction: TransactionProposalCancel¶
Deletes a TransactionProposal object and releases the owner's reserve.
7.1. Fields¶
| Field Name | Required? | JSON Type | Internal Type | Default Value | Description |
|---|---|---|---|---|---|
TransactionType |
✔️ | string | UINT16 | TransactionProposalCancel |
Identifies this as a TransactionProposalCancel transaction. |
Account |
✔️ | string | ACCOUNT | N/A | The account requesting cancellation. |
ProposalID |
✔️ | string | HASH256 | N/A | The ID of the TransactionProposal to cancel. |
7.2. Authorization¶
- Non-terminal proposal: the owner (the proposal's
Owner, i.e. the proposer) or the target account (the proposed transaction'sAccountorDelegate) may cancel. - Terminal proposal: Any account may cancel, to clean up the object and release the owner's reserve.
The target account can cancel at any point in the lifecycle — even after the proposal is complete — without owning the object. Since any of its signers — or a Delegate, when the proposed transaction has one — can create a proposal against it, and doing so reserves one of that account's tickets (§4.2.1), the target account needs a way to refuse; cancelling clears the proposal and frees the ticket.
Cancellation is only fully effective before a proposal is complete. If a quorum-weight of valid signatures has already been collected, an observer may have copied them and can still submit the completed transaction even after the proposal object is gone; see §13.4.
7.3. Transaction Fee¶
Fee Structure: Standard. This transaction uses the standard transaction fee (currently 10 drops, subject to Fee Voting changes).
7.4. Failure Conditions¶
7.4.1. Data Verification¶
ProposalIDis required by the transaction format, so if it is missing, deserialization fails before preflight; submission returns a parse error rather than a transaction result, and no fee is charged. If present but malformed, returnstemMALFORMED.
7.4.2. Protocol-Level Failures¶
- No
TransactionProposalobject exists with the givenProposalID(tecNO_ENTRY). - The proposal is not terminal and
Accountis neither theOwnernor the target account — or itsDelegate, when the proposed transaction has one — (tecNO_PERMISSION).
7.5. State Changes¶
On Success (tesSUCCESS):
- Deletes the
TransactionProposalobject. - Decrements the
Owner'sOwnerCountby 5, or by 10 for aBatch(§4.4), releasing the reserve.
7.6. Example JSON¶
{
"TransactionType": "TransactionProposalCancel",
"Account": "rPROPOSER........................",
"Fee": "10",
"Sequence": 43,
"ProposalID": "C1A2B3D4E5F6..............................."
}
8. API¶
To use a proposal, a signer or wallet has to fetch it and see how far along it is. This proposal introduces a new transaction_proposal RPC for retrieving one TransactionProposal and its computed status. (Listing the proposals an account owns is already covered by account_objects with a TransactionProposal type filter; no dedicated listing method is introduced.)
8.1. RPC: transaction_proposal¶
Returns a TransactionProposal by ID, or by the target account and proposed transaction ticket, together with a computed, per-account view of how far the proposal is from a submittable transaction.
8.1.1. Request Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
proposal_id |
string | No | The ProposalID (§4.1). Required unless account and ticket_seq are provided. |
account |
string | No | The target account. Used with ticket_seq to derive the ProposalID. Required unless proposal_id is provided. |
ticket_seq |
number | No | The proposed transaction's TicketSequence. Used with account to derive the ProposalID. Required unless proposal_id is provided. Numeric strings are accepted. |
ledger_hash |
string | No | A 32-byte hex string identifying the ledger to query. |
ledger_index |
string or number | No | The ledger index, or a shortcut such as "validated". |
account + ticket_seq is the same addressing ledger_entry accepts for a transaction_proposal object; the two methods accept identical field types and return identical error codes for malformed addressing.
8.1.2. Response Fields¶
The response returns the raw ledger object plus computed convenience fields so a client does not have to join the collected signatures against live SignerLists itself:
| Field | Type | Description |
|---|---|---|
proposal_id |
string | The ID of the TransactionProposal. |
proposal |
object | The raw TransactionProposal ledger object. |
proposal_status |
string | Where the proposal is in its lifecycle: "pending", "complete", or "expired" (see below). |
signing_status |
array | One entry per required authorization (§8.1.3.1), in a stable order: the account row first, then auxiliary co-signers, then batch participants. |
tx_blob |
string | Present whenever every signing_status row is satisfied — regardless of proposal_status — the stored ProposedTransaction serialized in submit-ready binary form, so a client does not have to reassemble and re-serialize it. This includes an expired proposal whose signatures reached quorum before it expired (§13.4): the proposal object is dead and cleanable, but the signed transaction it held remains independently submittable, and this field is how a client gets it without re-deriving completeness itself. Providing it implies nothing beyond §8.1.3.4. |
The field is named
proposal_status, notstatus, becausestatusis already the RPC envelope's success/error indicator and the two would collide in the same JSON object.
Each signing_status entry:
| Field | Type | Description |
|---|---|---|
account |
string | The account whose authorization is required. |
signed |
boolean | Whether the signature material collected so far currently authorizes this account on the queried ledger (§8.1.3.2). |
reason |
string | Present only when signed is false: why (see below). |
signed_weight |
number | (Optional, present on Accounts with SignerList only) Present only when a Signers array has been collected for this row: its weight against the account's live SignerList. |
quorum |
number | (Optional, present on Accounts with SignerList only) Present only when the account has a live SignerList: its SignerQuorum. |
signers |
array | (Optional, present on Accounts with SignerList only) Present only when the account has a live SignerList: one entry per list member — {account, weight, signed} — where signed is whether a currently-valid signature from that member has been collected. This is the list a wallet chases: every member with signed: false is a candidate next signer. Collected signatures from accounts not on the live list do not appear here; they surface as reason: "invalid_signer_set". |
There is one row per signature slot, not per account, so an account can appear twice: a proposed Batch's fee sponsor who is also an inner participant owes two authorizations, one through SponsorSignature and one through BatchSigners, and each succeeds or fails on its own. Rows carry no slot identifier — they are told apart by the stable order above: the account row, then auxiliary co-signers, then batch participants.
reason values:
| Value | Meaning |
|---|---|
inadequate_signatures |
In the case of Single-Sign configuration, the account has not received any signatures. In the case of Multi-Sign configuration, the received signatures do not yet satisfy the requisite quorum |
invalid_signer_set |
The collected Signers array contains an entry the live SignerList does not authorize; submission rejects the set wholesale. This occurs when the Account has removed a Signer from its erstwhile SignerList configuration |
no_signer_list |
A Signers array is collected but the account has no SignerList on the queried ledger. This error occurs when the Account had a SignerList but has since deleted it and switched to a SingleSign configuration |
master_disabled |
The collected signature is by the master key, and the master key has since been disabled. This is caused by the updated ledger-state since the collection of this signature. |
not_authorized |
The signing key does not currently authorize the account (e.g. a rotated regular key). |
account_not_found |
The account does not exist on the queried ledger (and is not one an earlier inner transaction of the proposed Batch would create). |
awaiting_sponsorship_signature |
Sponsor rows only: an on-ledger Sponsorship entry exists, but its flags require a co-signature for what this transaction sponsors. |
malformed |
The stored signature material is not usable (defensive; should be unreachable through TransactionProposalSign). |
proposal_status is the single field a client switches on:
pending— still collecting: at least onesigning_statusrow is unsatisfied.complete— everysigning_statusrow is satisfied, so the storedProposedTransactionis ready to copy and submit (§6.5). See §8.1.3.4 for whatcompletedoes not guarantee.expired— the proposal is terminal (§4.5): itsExpirationhas passed, or the proposed transaction'sLastLedgerSequencehas passed (§8.1.3.3). It no longer accepts signatures and can be cleaned up by anyone.
proposal_status is evaluated terminal-first: a proposal that is terminal reports expired even if every authorization is satisfied (the proposal object is dead and cleanable, though its already-collected signatures may still be independently submittable — see §13.4). Otherwise it reports complete if the requirements are met, else pending. tx_blob's presence does not follow this terminal-first rule: it tracks authorization completeness alone, so a client that only inspects tx_blob still sees a still-submittable transaction on an expired proposal, without having to special-case that state.
The computed fields are derived from live ledger state at the queried ledger and are not stored on the object. Querying the same proposal at different ledgers can give different answers — that is the point.
8.1.3. Completeness Evaluation¶
8.1.3.1. Required Authorizations¶
The server derives the set of required authorizations from the stored ProposedTransaction, mirroring exactly what submission-time validation will demand. The role names below are descriptive: they label the kinds of authorization and fix the order rows come back in (§8.1.2). They are not a response field.
| Role | Required when | Signature slot |
|---|---|---|
account |
Always: the target account — or the transaction's Delegate, when present. |
Top-level SigningPubKey/TxnSignature, or Signers |
counterparty |
The transaction carries a Counterparty; or it is a LoanSet (XLS-0066) without one, in which case the required co-signer is the owner of the LoanBroker it names. |
CounterpartySignature |
sponsor |
The transaction carries a Sponsor (XLS-0068). |
SponsorSignature, or exemption via a Sponsorship entry (§8.1.3.2) |
batch_participant |
The transaction is a Batch (XLS-0056): one row per account in the XLS-56 required-signer set other than the outer account — each inner transaction's Account, or its Delegate in its place when the inner transaction is delegated, plus any inner Counterparty or co-signing Sponsor (§6.1.1). |
That account's BatchSigners entry |
An inner transaction whose required signer is the outer account adds no row: the outer account authorizes all of its inners by signing the batch. Inner counterparties and inner co-signing sponsors appear as batch participants, not under counterparty/sponsor, because XLS-56 routes them through BatchSigners too; those two roles describe the proposed transaction itself, which for a Batch is the outer transaction.
8.1.3.2. Authorization, Not Cryptography¶
Each signature was cryptographically verified when TransactionProposalSign appended it (§6.3.2), and ledger data cannot change after that. What can change is whether the signature still authorizes the account, so signed is computed by re-applying the standard authorization rules against the queried ledger — the same rules submission applies:
- A single signature must be by the account's current regular key, or by its master key while the master key is enabled.
- A collected
Signersarray is validated against the account's liveSignerList, and it fails as a whole if any collected entry is not currently authorized: an entry absent from the list, one whose master key has since been disabled, one whose regular key has since rotated, or a non-phantom entry whose account no longer exists each void the entire set at submission (tefBAD_SIGNATURE/tefMASTER_DISABLED). Otherwise the set's weight must meet the liveSignerQuorum. Because signatures only ever accumulate on a proposal, a voided set cannot be repaired by further signing — the practical remedy isTransactionProposalCanceland a fresh proposal. - A
BatchSignersentry for an account that does not exist yet is authorized only by that account's own master key (an earlier inner transaction may create the account). - A sponsor row is satisfied by a valid
SponsorSignature, or — only while noSponsorSignaturefield has been collected at all — by an on-ledgerSponsorshipentry between the sponsor and the target account (or theDelegate, when present) whose flags do not require a co-signature for what this transaction sponsors. A collected-but-no-longer-authorizingSponsorSignatureis not rescued by the exemption, because submission validates a presentSponsorSignatureunconditionally.
Consequently a signature that counted yesterday may not count today (disabled master key, rotated regular key, replaced SignerList), and signed_weight can even meet quorum while signed is false (invalid_signer_set). Clients must treat signed as authoritative and the numbers as progress detail.
8.1.3.3. Expiry Bounds¶
expired is reported when either terminal condition of §4.5 holds at the queried ledger:
Expiration: the queried ledger's parent close time has reached or passed the proposal'sExpiration.LastLedgerSequence: the proposed transaction can no longer be included in any ledger. The earliest ledger it could still enter is the queried ledger itself when that ledger is open, and the next ledger otherwise — so the proposal is expired whenLastLedgerSequenceis below that bound. (A transaction withLastLedgerSequenceequal to the current open ledger's sequence is still submittable and reportspending/complete, notexpired.)
8.1.3.4. complete Is an Authorization Verdict¶
complete asserts that every required authorization is satisfied on the queried ledger — no more. It does not re-validate everything submission will: the target account may since have been deleted, the fixed Fee may no longer meet the minimum required for the signatures actually carried once fee escalation is accounted for, or an amendment the transaction needs may have been disabled. Clients should treat complete as "assemble and submit now, and expect success under normal conditions", not as a guarantee of tesSUCCESS.
complete is also only as fresh as the ledger it was computed against. Signatures are immutable once collected, but the ledger state they are judged against is not, and submission applies gates beyond signature authorization that this RPC does not model. Examples of state changes that can silently invalidate a complete verdict:
- Deleting the
LoanBrokeran implicit counterparty was resolved from: the completedLoanSetthen failstemBAD_SIGNERat submission, because the counterparty is re-resolved from the live broker at that time. - Revoking or narrowing a delegation (
DelegateSet): a delegate's on-ledger permission for the proposed transaction type is a submission-time gate separate from — and not attested by — the delegate's signature. - The exact-set rule for a proposed
Batch: submission requires the storedBatchSignersarray to correspond exactly to the required signer set (sorted, unique, no extras, never the outer account), and rejects any mismatch wholesale (temBAD_SIGNER); this structural property is likewise not attested by the per-row verdicts.
For the most accurate result, invoke this RPC against the most recent validated ledger and treat the verdict as point-in-time: re-evaluate immediately before assembling and submitting the completed transaction, since any intervening ledger may have changed the answer.
8.1.4. Failure Conditions¶
- Neither
proposal_idnor bothaccountandticket_seqare present, orproposal_idis combined with them (invalidParams). proposal_idis not a 256-bit hex string (malformedRequest).accountis not a valid account address (malformedAddress).ticket_seqis not a number or numeric string (malformedRequest).- No
TransactionProposalexists at the derived ID in the queried ledger — including when the index names a ledger entry of a different type (entryNotFound). - The requested ledger is not available (
lgrNotFound).
8.1.5. Example Request¶
{
"command": "transaction_proposal",
"account": "rTARGET..........................",
"ticket_seq": 1201,
"ledger_index": "validated"
}
8.1.6. Example Responses¶
An ordinary (non-batch) proposal mid-collection — the target multi-signs, two of six weight collected:
{
"proposal_id": "C1A2B3D4E5F6...............................",
"proposal": {
"LedgerEntryType": "TransactionProposal",
"Owner": "rPROPOSER........................",
"Expiration": 800000000,
"ProposedTransaction": {
"TransactionType": "Payment",
"Account": "rTARGET..........................",
"TicketSequence": 1201,
"...": "..."
}
},
"proposal_status": "pending",
"signing_status": [
{
"account": "rTARGET..........................",
"signed": false,
"reason": "inadequate_signatures",
"signed_weight": 2,
"quorum": 6,
"signers": [
{
"account": "rSIGNER1.........................",
"weight": 2,
"signed": true
},
{
"account": "rSIGNER2.........................",
"weight": 2,
"signed": false
},
{
"account": "rSIGNER3.........................",
"weight": 2,
"signed": false
}
]
}
],
"ledger_index": 12345678,
"validated": true
}
A proposed Batch — the motivating case for this design. The outer account has signed; one participant authorizes through its own SignerList and is below quorum; an inner LoanSet's counterparty has not signed at all:
{
"proposal_id": "D4E5F6A1B2C3...............................",
"proposal": { "...": "..." },
"proposal_status": "pending",
"signing_status": [
{
"account": "rOUTER...........................",
"signed": true
},
{
"account": "rLENDER..........................",
"signed": false,
"reason": "inadequate_signatures"
},
{
"account": "rPARTICIPANT.....................",
"signed": false,
"reason": "inadequate_signatures",
"signed_weight": 1,
"quorum": 2,
"signers": [
{
"account": "rPCOSIGNER1......................",
"weight": 1,
"signed": true
},
{
"account": "rPCOSIGNER2......................",
"weight": 1,
"signed": false
}
]
}
],
"ledger_index": 12345679,
"validated": true
}
9. Rationale¶
9.1. Why collect signatures on-ledger¶
The problem multi-sign users actually face is not the cryptography of signing — it is coordination: sharing the exact payload, gathering signatures, and getting them submitted without a trusted middleman. On-Chain Cosigner keeps the standard multi-sign signatures but moves their collection point from a coordinator's inbox to an immutable ledger object. Every signer signs the same immutable payload; every signature is validated on arrival; and the collected set is always available to everyone. This removes the coordinator as a single point of failure — there is no blob to lose, no blob to swap, and, because the collected set is a standard multi-signed transaction, anyone can submit it.
9.2. Avoiding sequence invalidation with Tickets¶
Standard multi-sign forces every field to be fixed before the first signature. If a proposal used the target account's live Sequence, unrelated activity by that account could advance the sequence and invalidate the proposal while signatures were still being collected. On-Chain Cosigner therefore requires the proposed transaction to use TicketSequence. The collection window is bounded by the proposal's own Expiration; the proposed transaction's LastLedgerSequence (optional) separately bounds the submission window.
9.3. How quorum is enforced¶
Quorum is never evaluated by a bespoke rule in this feature. Each signature is validated against the target account's SignerList when it is added (so garbage cannot accumulate), and the completed transaction is validated again by the existing multi-sign machinery when it is finally submitted. Both checks use the account's live SignerList, so the executed action always reflects the account's current authority model.
9.4. Why there is no execution transaction¶
Because signatures are collected directly into the proposed transaction's own Signers field, a complete proposal is a fully-signed multi-sign transaction waiting to be submitted — no assembly step exists to get wrong. Adding a dedicated on-ledger execute step (a fourth transaction, or auto-execution inside TransactionProposalSign) would duplicate logic the ledger already has and would couple execution to a specific submitter or to the moment a particular signature lands. Instead, execution reuses the ordinary submission path and any account may perform it. Proposal and execution stay decoupled — deleting the proposal does not revoke already-collected signatures (§13.4) — but the object is not left stranded: running the completed transaction consumes the target account's TicketSequence, which auto-deletes the proposal (§4.5).
10. Composability¶
- Batch (XLS-56): The proposed transaction may be a
Batch, enabling multi-account, atomic, multi-signed settlement (e.g. end-of-day repo netting, flash-style capital operations). The outer account is authorized bySigningFor= the outer account (intoProposedTransaction.Signers); each participant account bySigningFor= that participant — single-signature (its own key) or multi-sign (§6.1). A signer authorized on several of the batch's accounts produces oneProposalSignatureper account, since each signature is bound to its owning account. Once every participant's requirement is met the completed batch executes atomically. This is the primary motivating case for On-Chain Cosigner, since multi-account Batches otherwise require the most off-chain signature coordination. - Lending protocols: A borrower can post a
LoanSet(or equivalent) as a proposal; the lender signs on-chain as counterparty (SigningFor= the lender, §6.1) — single-key or multi-signed — which the ledger records in the proposed transaction's ownCounterpartySignaturefield, while the borrower's account is authorized throughProposedTransaction.Signers. This turns loan origination into a trustless, asynchronous flow with no synchronous coordination. - Sponsored fees & reserves (XLS-68): A user posts a transaction carrying
Sponsor/SponsorFlags; the sponsor signs on-chain (SigningFor= the sponsor, §6.1) — single-key or multi-signed — which the ledger records in the proposed transaction's ownSponsorSignaturefield, co-authorizing the fee/reserve sponsorship. This is the same auxiliary-co-signature mechanism used for aLoanSetcounterparty, and the two can be collected on the same proposal (e.g. a sponsoredLoanSet).
11. Backwards Compatibility¶
This proposal is purely additive: it introduces one new ledger entry type and three new transaction types, all gated behind the Cosigner amendment. Existing multi-sign, SignerListSet, and off-chain signing workflows are unaffected and continue to function. Because a completed proposal is submitted through the ordinary multi-sign path, the multi-sign validation rules are unchanged. There are two additions to the common path, both about ticket consumption. First, a validation gate: a transaction that would spend a TicketSequence reserved by a live TransactionProposal, and that is not that proposal's own transaction, fails with terTICKET_RESERVED (§4.2.1). Only reserved tickets are affected, and the gate goes away as soon as the proposal is deleted. Second, a cleanup check: when a transaction does spend such a ticket, the ledger rebuilds the ProposalID and deletes the matching proposal, refunding its reserve (§4.5). Accounts that do not use On-Chain Cosigner are not impacted.
12. Open Questions¶
- Reducing the initial construction burden: Can the initial proposed-transaction construction be simplified further, beyond the ticket-based approach in §9.2?
- Revocation: Should there be a first-class way to revoke a completed proposal's signatures on-ledger (beyond consuming the
TicketSequence), given that cancellation alone does not prevent submission of already-collected signatures (§13.4)? - Recurring / standing orders: recurring allowances and treasury stipends suggest a proposal could activate a long-lived standing order rather than a one-shot transaction, potentially composing with a Subscriptions primitive. This is out of scope for this spec but noted as a future extension.
13. Security Considerations¶
13.1. Authority derives solely from the collected signatures¶
The completed transaction is authorized entirely by the collected signatures validated against the applicable key or SignerList(s) — never by the identity of the account that finally submits it. Submission grants no authority the collected signatures did not already confer, so anyone may submit. Likewise, TransactionProposalSign may be submitted by any funded account; authorization comes from ProposalSignature.Account, ProposalSignature.SigningPubKey, and ProposalSignature.TxnSignature.
13.2. Immutable payload¶
The proposed transaction is fixed at creation and cannot be altered by any subsequent transaction. Signers therefore always sign exactly what is stored, eliminating the manipulation risk of a coordinator presenting different payloads to different signers.
13.3. Every collected signature is pre-validated¶
Each TransactionProposalSign is rejected unless ProposalSignature.TxnSignature is cryptographically valid over the immutable proposed transaction and ProposalSignature.Account is authorized for the SigningFor account (the same account for single-signing, or a member of its applicable SignerList). This prevents an attacker from polluting a proposal with junk entries and guarantees that a complete proposal will pass standard signature validation at submission.
13.4. Cancellation does not revoke already-collected signatures¶
This is the central security consideration of the copy-and-submit model. The proposal object is a bulletin board, not an execution gate: once a quorum-weight of valid signatures has been collected, any observer may have copied them, and those signatures remain valid regardless of whether the proposal object still exists. Cancelling or expiring the proposal frees the reserve but does not guarantee the transaction will not execute.
Neither mitigation is an atomic revocation:
- Spending the
TicketSequenceis best-effort. While the proposal exists its ticket is reserved for the proposed transaction (§4.2.1), so the target account cannot simply burn it. It must first delete the proposal (TransactionProposalCancel, §7.2) and then spend the ticket — two transactions, and a copied signed transaction can slip in between. LastLedgerSequenceelapsing is absolute, but only if the proposed transaction set one.Expirationis no substitute: it bounds the proposal object, not the transaction (§4.5).
Architects and wallets should surface this clearly. An atomic revocation is an open question (§12).
13.5. Stale signatures under SignerList changes¶
Because both the per-signature check and the final submission validate against the live SignerList, changing a SignerList while a proposal is pending is honored — but asymmetrically. Lowering the quorum can make a previously-incomplete set sufficient. Removing a collected signer (or a collected signer disabling their master key or rotating their regular key) does not merely subtract their weight: submission rejects the entire collected set (tefBAD_SIGNATURE), and since signatures only accumulate on a proposal, the proposal becomes permanently unsatisfiable and must be cancelled and re-proposed (§8.1.3.2). Modifying an account's SignerList therefore affects all pending proposals against that account.
13.6. Denial-of-service and reserve pressure¶
Each proposal consumes an elevated flat owner reserve (§4.4) held against the Owner — higher than a typical ledger entry, and higher still for a Batch — pricing the larger state burden and disincentivizing spam. Because every appended signature must be valid, an attacker cannot inflate a proposal with junk. Built-in expiry ensures abandoned proposals can always be cleaned up (by anyone, once terminal) so they do not accumulate indefinitely in ledger state.
Proposals cannot come from arbitrary accounts: TransactionProposalCreate is limited to the target account — or the proposed transaction's Delegate — and members of that account's applicable SignerList (§5.1.1). Squatting is therefore an insider risk, not an open one. Since there is one slot per (target account, ticket) (§4.1), a hostile signer could take a slot the real proposer wanted, blocking it with tecDUPLICATE and holding the ticket reserved (§4.2.1). Each attempt costs that signer a full reserve, and tickets give the honest proposer far more slots than a squatter could block. The target account is also never stuck with an unwanted proposal: it may delete any proposal made for it via TransactionProposalCancel at any time (§7.2), clearing the slot and the reserved ticket regardless of who created the proposal or how far along it is.
13.7. Fee accountability¶
On-Chain Cosigner does not redirect fees. The completed transaction is an ordinary transaction, so its fee goes to whoever would normally pay it: usually the target account, whose signers authorized the action; the Delegate for a delegated transaction, so a delegate cannot drain the account it acts for (XLS-75); the Sponsor when the fee is sponsored (XLS-68).
That party's own signature is one the proposal must collect (§6.1.1), so nobody pays for a payload they did not approve. The one exception is a sponsor exempted by an existing Sponsorship entry (§8.1.3.2), which is itself standing consent. And because the payload is immutable and its Fee is capped (§4.2.1), the payer knows the exact amount before signing. Each TransactionProposalCreate, TransactionProposalSign, and TransactionProposalCancel pays its own fee from its own submitter.
Appendix¶
Appendix A: FAQ¶
A.1: Who can create a proposal?¶
The target account, or any of its signers — when ProposedTransaction.Delegate is present, the Delegate and its signers stand in for the target account. Either that account itself or a member of its applicable SignerList may create the proposal (§5.1.1). The proposer owns the object and pays its reserve.
A.2: Can the target account be different from the proposer?¶
Yes. The target account is the Account of the proposed transaction; the proposer is the account that submits TransactionProposalCreate. This is what enables flows like a borrower proposing a LoanSet that a lender then signs.
A.3: How does a signer know what they are signing?¶
The full proposed transaction is stored, immutable, in the ProposedTransaction field of the on-ledger object. A signer (or their wallet) reads the object by ProposalID, inspects the payload, signs exactly that payload, and includes it in ProposalSignature. The signer or any relayer can submit it via TransactionProposalSign.
A.4: How is the proposed transaction actually executed?¶
There is no on-ledger execute step. Signatures accumulate inside the proposed transaction's own Signers field, so once quorum weight is reached the ProposedTransaction field is already a valid multi-signed transaction: any observer copies it verbatim and submits it through the normal transaction path. The existing multi-sign machinery validates and applies it. See §6.5.
A.5: Does cancelling a proposal guarantee it won't execute?¶
Only if a quorum-weight of valid signatures has not yet been collected. Once enough signatures exist on-ledger, someone may have copied them and can still submit the completed transaction. Spending the proposed transaction's TicketSequence blocks it, but only as a race: the ticket is reserved while the proposal exists, so you must cancel first and then spend it, and the copied transaction can get there first. The only absolute bound is the proposed transaction's LastLedgerSequence elapsing. See §13.4.
A.6: What happens if quorum is never reached before expiry?¶
The proposal becomes terminal at Expiration, stops accepting signatures, and any account may submit TransactionProposalCancel to delete it and release the proposer's reserve. The full signing history remains in transaction metadata.
A.7: Can there be multiple pending proposals against the same target account?¶
Yes, as long as each uses a distinct TicketSequence. A proposal's ID is derived from the target account and the proposed transaction's TicketSequence only (§4.1), so there is exactly one proposal slot per (target account, ticket), shared across all proposers. Giving each proposed transaction a distinct TicketSequence lets many concurrent proposals coexist against the same target account without collisions.
A.8: Who pays the proposed transaction's fee?¶
Whoever would pay it if the transaction had been signed off-ledger and submitted normally — this spec changes nothing about fee accountability. Usually that is the target account, on whose behalf the transaction acts. For a delegated transaction it is the Delegate (XLS-75); for a sponsored one, the Sponsor (XLS-68). Whoever submits the completed transaction pays only their own submission fee. See §13.7.
A.9: How does signing work when the proposed transaction is a multi-account Batch?¶
As described in §6.1.1/§6.1.2, plus one Batch-specific wrinkle: an XLS-56 batch signature binds the owning account (message = <batch data> + <owning account> + <signer account>), so a signer authorized on several participant accounts must submit one TransactionProposalSign per account — a distinct signature and SigningFor each time. The same signer key can therefore appear across several participants' BatchSigners.
A.10: How is a transaction with a second signer — a LoanSet counterparty or a sponsor — handled?¶
As described in §6.1.1/§4.2.2: each required party is named in its own TransactionProposalSign via SigningFor. Because the auxiliary signature fields are excluded from every party's signing data, the parties can sign in any order, and a transaction needing several (e.g. a sponsored LoanSet) collects them independently — provided the target account authorizes via a Signers entry rather than a top-level single-signature. A top-level single-signature does change the signing payload (§4.2.1, §6.1.2), so it must be the first signature collected if the target account chooses that path.
A.11: Does this replace off-chain multi-sign?¶
No. Off-chain multi-sign and standard SignerListSet continue to work unchanged. On-Chain Cosigner is an additive, opt-in coordination layer for accounts that want the ledger to be the meeting room.