Amendment XLS: On-Chain Cosigner #589
shawnxie999
started this conversation in
XLS Proposals
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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
TransactionProposalobject. 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 ownSignersfield, 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:
TransactionProposalledger entry.TransactionProposalCreatetransaction.TransactionProposalSigntransaction.TransactionProposalCanceltransaction.This feature will require an amendment, tentatively titled
Cosigner.2. Motivation
XRPL multi-sign today has three structural problems, all 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.
No auto-fill. For a single-signer transaction, fields like
SequenceandLastLedgerSequencecan be auto-filled at submission time. For multi-sign, every field must be fixed before the first signer signs. If the transaction fails to reach a ledger in time (for example becauseLastLedgerSequencewas set too low while waiting for signers), the entire round must be restarted from scratch with all signers.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 all three by moving signature collection onto the ledger:
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
TransactionProposalledger object. It holds a single unsigned proposed transaction (the payload) and the set of signatures collected for it so far.Accountof the proposed transaction. ItsSignerListconfiguration governs the quorum.TransactionProposalCreate. It owns the proposal object and pays its reserve. The proposer need not be a signer or the target account.SignerListthat can append its signature to the proposal, contributing its weight toward quorum.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.Counterpartyof aLoanSet, theSponsorof a sponsored transaction; §6.1) — or, for aBatch, the outer account's quorum plus a satisfied authorization for every participant account. ItsTransactionfield is then a valid signed transaction that anyone can copy and submit.3.2. Lifecycle
At any point while the proposal is not terminal, the proposer may submit
TransactionProposalCancelto abort it. Once the proposal is terminal (expired, or the proposed transaction'sLastLedgerSequencehas 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
SignerList.SignerList) as it is added, so a complete proposal is guaranteed to be a submittable transaction.4. Ledger Entry:
TransactionProposalThis 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:
where
AccountandTicketSequenceare taken from the proposed transaction:Accountis the target account, andTicketSequenceis 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 theProposalIDreferenced byTransactionProposalSignandTransactionProposalCancel.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 secondTransactionProposalCreatefor the same pair fails withtecDUPLICATE(§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 differentTicketSequencefor each (§9.2).4.2. Fields
LedgerEntryTypeTransactionProposalTransactionProposalobject.Flags0OwnerTransactionSigners;CounterpartySignature/SponsorSignaturefor a type that requires one; andBatchSignersfor aBatch), into which collected signatures accumulate (see §4.2.1).ExpirationOwnerNodePreviousTxnIDPreviousTxnLgrSeqField Details:
4.2.1.
TransactionTransactionis 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-levelSigningPubKey/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 aBatch,BatchSigners— into which the ledger inserts each validated signature (see §4.2.2). Because the XRPL signing payloads exclude these signature fields, appending a signature never changes what any signer signed over, so previously-collected signatures stay valid and later signers sign the same canonical payload.The proposed transaction:
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).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).Fee. The proposed transaction's fee is paid by the target account (the proposed transaction'sAccount) when the completed transaction is submitted.TransactionProposalCreate,TransactionProposalSign, orTransactionProposalCancel(no nesting of proposals);EnableAmendment,SetFee,UNLModify), which no account originates or signs; ortfInnerBatchTxnflag, 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).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
Accountfield. 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
Transactionfield is already a valid, fully-signed transaction that can be copied verbatim and submitted. Where a signature lands depends on the proposed transaction type:Transaction.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 intoTransaction.Signers; each other participant account (an account with inner transactions inRawTransactions) is authorized by an entry inTransaction.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.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 throughTransaction.Signersas above. A transaction may require more than one. See §6.1.Every
Signersarray (top-level or nested in aBatchSigner) is kept sorted byAccountand holds at most 32 entries (the maximumSignerListsize).BatchSignersis also sorted byAccountand holds at most 24 entries. Weights are not stored: a signer's weight and the relevant quorum are always read from the applicable account'sSignerList, 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 relevantSignerList(s). §6.1 describes howTransactionProposalSignroutes a signature to the correct location from itsSigningForaccount and submitter.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
TransactionProposalholds a full transaction plus its collected signatures, so it reserves more than a typical ledger entry.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).
4.5. Deletion
Terminal proposal: A proposal is terminal when it can no longer be completed and submitted, i.e. when either of the following is true relative to the parent ledger:
Expiration; orLastLedgerSequenceand the current ledger sequence is greater than it.A terminal proposal stops accepting new signatures and exists in ledger state only until it is cleaned up.
Deletion Transactions:
TransactionProposalCancel,TransactionProposalSign, and — implicitly — any transaction of the target account that consumes the proposed transaction'sTicketSequence(see below).Deletion Conditions: The object is deleted when any one of the following occurs:
Owner(the proposer) may delete it, viaTransactionProposalCancel(resulttesSUCCESS).TransactionProposalCancel(resulttesSUCCESS, since deletion is that transaction's intended action).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).TicketSequence, the ledger looks uphash(<space key>, Account, <consumed ticket>)and, if a proposal exists there, deletes it and refunds theOwner's reserve. This catches both the proposal's own completed transaction running and the account spending that ticket on something else — either way the proposal can no longer execute, so it is cleaned up for free (§4.1). A proposal whoseTicketSequenceis never consumed is cleaned up on expiry instead.This removes only the leftover object. Signatures already copied off-ledger stay valid and submittable until the
TicketSequenceis consumed (§13.4).Account Deletion Blocker: Yes. A
TransactionProposalobject must be deleted before its owner account can be deleted.4.6. Invariants
Expirationis always present and non-zero.Transaction.Signersis unique byAccount, and the array is sorted byAccountwith at most 32 entries.Transaction.BatchSigners, if present, is unique byAccount, and the array is sorted byAccountwith at most 24 entries.Transaction.Signersis a signature that was cryptographically valid over the proposedTransaction(excluding itsSignersfield) at the time it was added.Transaction's signature fields change over the life of the proposal — its top-levelSigningPubKey/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. Example JSON
{ "LedgerEntryType": "TransactionProposal", "Flags": 0, "Owner": "rPROPOSER........................", "Expiration": 800000000, "Transaction": { "TransactionType": "Payment", "Account": "rTARGET..........................", "Destination": "rDEST............................", "Amount": "5000000000", "TicketSequence": 1201, "Fee": "10", "SigningPubKey": "", "Signers": [ { "Signer": { "Account": "rCEO............................", "SigningPubKey": "03AB...", "TxnSignature": "3045..." } } ] }, "OwnerNode": "0000000000000000", "PreviousTxnID": "F3B1...", "PreviousTxnLgrSeq": 12345678 }5. Transaction:
TransactionProposalCreateCreates a
TransactionProposalobject holding an unsigned proposed transaction, placing it in a pending state visible to all signers.5.1. Fields
TransactionTypeTransactionProposalCreateTransactionProposalCreatetransaction.AccountTransactionExpirationStandard common fields (
Fee,Sequence,Flags,Memos,SourceTag, signing fields) apply.MemosandSourceTagMAY be used to attach a reason code or reconciliation identifier to the proposal.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
Feeis not charged here; it is charged to the target account when the completed transaction is submitted.5.3. Failure Conditions
5.3.1. Data Verification
All Data Verification failures return a
tem-level error.Transactionis missing or is not a well-formed transaction of a known type (temMALFORMED).temcode 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.SigningPubKeyor includes aTxnSignature,Signers,CounterpartySignature,SponsorSignature, orBatchSignersfield (temBAD_SIGNER).TransactionProposalCreate,TransactionProposalSign, orTransactionProposalCancel; a pseudo-transaction (EnableAmendment,SetFee,UNLModify); or carries thetfInnerBatchTxnflag (temINVALID).TicketSequence, or specifiesSequenceinstead of or in addition toTicketSequence(temSEQ_AND_TICKET).Expirationis missing or zero (temMALFORMED).5.3.2. Protocol-Level Failures
Expirationis already at or before the parent ledger's close time (tecEXPIRED).LastLedgerSequencethat is already at or before the current ledger sequence (tecEXPIRED).TransactionProposalobject (tecINSUFFICIENT_RESERVE).Account) does not exist (tecNO_TARGET).SignerList(tecNO_PERMISSION).TransactionProposalwith the sameProposalIDalready exists — i.e. a live proposal (owned by anyone) already targets the same account with the sameTicketSequence(tecDUPLICATE, §4.1).5.4. State Changes
On Success (
tesSUCCESS):TransactionProposalledger object whoseOwneris the sendingAccount. The proposed transaction is stored with no signatures yet.Owner'sOwnerCount.5.5. Example JSON
{ "TransactionType": "TransactionProposalCreate", "Account": "rPROPOSER........................", "Fee": "10", "Sequence": 42, "Expiration": 800000000, "Transaction": { "TransactionType": "Payment", "Account": "rTARGET..........................", "Destination": "rDEST............................", "Amount": "5000000000", "TicketSequence": 1201, "Fee": "10", "SigningPubKey": "" } }6. Transaction:
TransactionProposalSignAppends one signature toward the proposed transaction to the proposal. A single, uniform contribution —
SigningFor+SigningPubKey+TxnSignature— supplies a signature for one account the proposed transaction requires authorization from. The ledger derives everything else from the proposed transaction and from who submits: where the signature is recorded, and whether it is a single- or multi-signature. The signature is validated exactly as it would be during standard signing, guaranteeing that the collected set remains submittable. If the proposal is already terminal, this transaction cannot record a signature and instead fails withtecEXPIRED, deleting the terminal proposal as a side effect (see §6.4).6.1. Fields
TransactionTypeTransactionProposalSignTransactionProposalSigntransaction.AccountSigningFordecides single- vs multi-sign (§6.1.2).ProposalIDTransactionProposalbeing signed.SigningForSigningPubKeyTxnSignatureSigningFor's signing data for the proposed transaction (§6.1.2).There are no per-role contribution objects. Every
TransactionProposalSignlooks the same — it names the account being authorized (SigningFor) and supplies one signature for it. The ledger works out the rest.6.1.1.
SigningFor— which account is being authorized, and where the signature landsSigningForanswers the question: whose approval is this signature providing? It must name an account that the proposed transaction needs a signature from. In other words,SigningFormust point to one of the proposed transaction's signature slots:Account, or itsDelegateif permission delegation is used;Counterparty, if that transaction type has one (for example, aLoanSet(XLS-66) lender; if omitted, this defaults to theLoanBroker.Owner, XLS-66 §3.8);Sponsor, if the transaction is sponsored (XLS-68);Batch, any account whose signature is needed for the batch, including each inner transaction's account and any additional account required by an inner transaction, such as itsDelegate,Counterparty, orSponsor. Each of these accounts is a batch participant.If
SigningFordoes not match one of these required accounts, the transaction fails withtecNO_PERMISSION. When it does match, the ledger records the signature in the location that corresponds to that account's role:SigningForis the transaction's…Account/DelegateTransaction.Signers(or top-level, see §6.1.2)CounterpartyTransaction.CounterpartySignatureSponsorTransaction.SponsorSignatureBatchparticipantTransaction.BatchSigners[SigningFor]If the same account fills more than one role, such as being both the
Counterpartyand theSponsor, the same contribution is recorded in every matching slot. Each slot is still validated independently. In most cases these roles are different accounts, so one contribution fills one slot.6.1.2. Single- vs multi-signature — derived from
AccountThe 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 the submitting
AccountandSigningFor:Account==SigningFor→ single-signature. The account is signing for itself using its master key or regular key.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.Account!=SigningFor→ multi-signature share. The submittingAccountis contributing one multi-signature share forSigningFor. The submittingAccountmust be inSigningFor's applicableSignerList. The ledger stores the contribution as a standardSignerentry ({Account, SigningPubKey, TxnSignature}) in the relevantSignersarray:Transaction.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
Feeis not charged here; it is charged to the target account when the completed transaction is submitted.6.3. Failure Conditions
6.3.1. Data Verification
All Data Verification failures return a
tem-level error.ProposalIDis missing or malformed (temMALFORMED).SigningFor,SigningPubKey, orTxnSignatureis missing (temMALFORMED).SigningFor's signing data for the proposed transaction (§6.1.2) (temBAD_SIGNATURE).6.3.2. Protocol-Level Failures
TransactionProposalobject exists with the givenProposalID(tecNO_ENTRY).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.SigningForis not an account the proposed transaction requires a signature from — it is not the transaction'sAccount/Delegate, itsCounterparty, itsSponsor, or (for aBatch) an account owning an inner transaction inRawTransactions(tecNO_PERMISSION).SigningPubKeyis notSigningFor's master or regular key; for multi-signing,Accountis not onSigningFor's applicableSignerList, orSigningPubKeyis not a valid key forAccount(tecNO_PERMISSION).Accountis already present in that destination, or a single-signature entry forSigningForalready exists (tecDUPLICATE). (The sameAccountmay still sign for a differentSigningFor.)SigningFor— a multi-signature share when a single-signature entry is already recorded, or vice versa (tecNO_PERMISSION).Signersarray, or would add aBatchSignerpast the 24-entryBatchSignerslimit (tecOVERSIZE).6.4. State Changes
On Success (
tesSUCCESS):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.Counterparty/Sponsorthe transaction requires, or, for aBatch, the outer account's quorum plus a satisfied authorization for every participant account — the proposal is complete: theTransactionfield is a valid signed transaction that anyone can copy and submit (see §6.5).On failure against a terminal proposal (
tecEXPIRED):tecresult is still applied to the ledger, the terminal proposal object is deleted and theOwner'sOwnerCountis decremented (releasing the reserve) as a side effect. This mirrors how transactions likeEscrowFinishandCheckCashreport a claimed-fee failure while cleaning up an expired object. 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:
TransactionProposalbyProposalID.Transactionverbatim — it already contains the collectedSigners(and, for aBatch,BatchSigners), sorted, and is a fully-formed signed transaction.submitAPI).The existing multi-sign (and, for a
Batch,BatchSigners) validation then checks the signatures against the applicable accounts' currentSignerList(s) and applies the transaction, charging itsFeeto the target account. No field of On-Chain Cosigner appears on the submitted transaction — it is an ordinary transaction. Applying it consumes the target account'sTicketSequence, which auto-deletes the proposal and refunds its reserve (§4.5).6.6. Example JSON
The
TransactionProposalSigntransaction is trivial —SigningForplus one signature. What matters is how it mutates theTransactionProposalobject, so each example shows the full object before and after every signature.6.6.1. Ordinary transaction — multi-sign shares accumulate to quorum
Setup. A
Paymentproposal for target accountrTARGET, whose applicableSignerListis{ rCEO: 4, rCFO: 3 }withSignerQuorum6. Freshly created, it holds no signatures:rCEOsigns.Account(rCEO) ≠SigningFor(rTARGET) → multi-sign:The object gains one
Transaction.Signersentry. Weight 4 < quorum 6, so it stays pending:rCFOsigns (same shape,SigningFor: rTARGET,Account: rCFO). The new share is inserted sorted byAccount, and weight 4 + 3 = 7 ≥ 6 → complete:The
Transactionfield is now a valid multi-signedPayment; anyone can copy it and submit it (§6.5).6.6.2. Ordinary transaction — single-sign with the account's own key
If
rTARGETinstead authorizes with its own key —Account==SigningFor(rTARGET) → single-sign — the signature fills the proposed transaction's top-levelSigningPubKey/TxnSignature(noSignersarray), and alone completes it:6.6.3. Auxiliary co-signature — a
LoanSetcounterpartySetup. A
LoanSetproposal: borrowerrBORROWER(target account) with the lenderrLENDERasCounterparty. The borrower's account is collected intoTransaction.Signers; the lender co-signs intoTransaction.CounterpartySignature. Suppose the borrower's quorum is already met and only the lender is outstanding:The lender single-signs (
SigningFor: rLENDER,Account: rLENDER). A newCounterpartySignaturefield appears, and every slot is now satisfied → complete:A multi-sign lender would instead accumulate into
CounterpartySignature.Signers— a nested array filling until the lender's own quorum is met, exactly likeTransaction.Signersabove. ASponsorSignature(for a sponsored transaction) behaves identically.6.6.4.
Batch— outer account plus participantsSetup. A multi-account
Batchby outer accountrOUTER, with inner transactions forrOUTER,rBOB, andrCAROL. Authorizations: the outer accountrOUTERintoTransaction.Signers; each other participant intoTransaction.BatchSigners[account].SignerLists:rOUTER = { rOUTERKEY: 1 }quorum 1;rBOBsigns with its own key;rCAROL = { rCAROLKEY: 1 }quorum 1.Three signatures arrive — one per account that must authorize:
After all three, the outer account's quorum is met and every participant is authorized → complete.
BatchSignersis sorted byAccount;rBOBis a single-signature entry,rCAROLa nested multi-sign one:7. Transaction:
TransactionProposalCancelDeletes a
TransactionProposalobject and releases the owner's reserve.7.1. Fields
TransactionTypeTransactionProposalCancelTransactionProposalCanceltransaction.AccountProposalIDTransactionProposalto cancel.7.2. Authorization
Owner, i.e. the proposer) may cancel.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. Failure Conditions
7.3.1. Data Verification
ProposalIDis missing or malformed (temMALFORMED).7.3.2. Protocol-Level Failures
TransactionProposalobject exists with the givenProposalID(tecNO_ENTRY).Accountis not theOwner(tecNO_PERMISSION).7.4. State Changes
On Success (
tesSUCCESS):TransactionProposalobject.OwnerCount(releasing the reserve).7.5. 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_proposalRPC for retrieving oneTransactionProposaland its computed status. (Listing the proposals an account owns is already covered byaccount_objectswith aTransactionProposaltype filter; no dedicated listing method is introduced.)8.1. RPC:
transaction_proposalReturns a
TransactionProposalby ID, or by the target account and proposed transaction ticket.8.1.1. Request Fields
proposal_idProposalID(§4.1). Required unlessaccountandticketare provided.accountticketto derive theProposalID. Required unlessproposal_idis provided.ticketTicketSequence. Used withaccountto derive theProposalID. Required unlessproposal_idis provided.ledger_hashledger_index"validated".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 the
SignerListitself (see the "Completion signalling" open question in §12):proposal_idTransactionProposal.proposalTransactionProposalledger object.signed_weightSignerList.quorumSignerQuorumthe collected weight must reach (the target account's applicable quorum).status"pending","complete", or"expired"(see below).statusis the single field a client switches on:pending— still collecting; more signatures can be added, and quorum is not yet met.complete— every signing requirement is satisfied (quorum, plus any requiredBatchSigners/auxiliary co-signatures), so the storedTransactionis ready to copy and submit (§6.5).expired— the proposal is terminal (§4.5): itsExpirationhas passed, or the proposed transaction'sLastLedgerSequencehas passed. It no longer accepts signatures and can be cleaned up by anyone.statusis evaluated terminal-first: a proposal that is terminal reportsexpiredeven if it had reached quorum earlier (the proposal object is dead and cleanable, though its already-collected signatures may still be independently submittable — see §13.4). Otherwise it reportscompleteif the requirements are met, elsepending.The computed fields are derived from live ledger state at the queried ledger and are not stored on the object.
8.1.3. Failure Conditions
proposal_idis missing or malformed, andaccount/ticketare not both present (invalidParams).proposalNotFound).8.1.4. Example Request
{ "command": "transaction_proposal", "account": "rTARGET..........................", "ticket": 1201, "ledger_index": "validated" }8.1.5. Example Response
{ "proposal_id": "C1A2B3D4E5F6...............................", "proposal": { "LedgerEntryType": "TransactionProposal", "Owner": "rPROPOSER........................", "Expiration": 800000000, "Transaction": { "TransactionType": "Payment", "Account": "rTARGET..........................", "TicketSequence": 1201, "...": "..." } }, "signed_weight": 3, "quorum": 6, "status": "pending", "ledger_index": 12345678, "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 useTicketSequence. The collection window is bounded by the proposal's ownExpiration; the proposed transaction'sLastLedgerSequence(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
SignerListwhen 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 liveSignerList, so the executed action always reflects the account's current authority model — including per-transaction-type lists resolved from the proposed transaction's type, exactly as in ordinary multi-sign under XLS-49 (Multiple Signer Lists).9.4. Why there is no execution transaction
Because signatures are collected directly into the proposed transaction's own
Signersfield, 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 insideTransactionProposalSign) 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'sTicketSequence, which auto-deletes the proposal (§4.5).10. Composability
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 (intoTransaction.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 submits oneTransactionProposalSignper 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.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 throughTransaction.Signers. This turns loan origination into a trustless, asynchronous flow with no synchronous coordination.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
Cosigneramendment. 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. The one addition to the common path is a cleanup check: when any account consumes aTicketSequence, the ledger removes a matchingTransactionProposalif one exists (§4.5). Accounts that do not use On-Chain Cosigner are not impacted.12. Open Questions
SignerListthemselves?TicketSequence), given that cancellation alone does not prevent submission of already-collected signatures (§13.4)?13. Security Considerations
13.1. Authority derives solely from the collected signatures
The completed transaction is authorized entirely by the multi-signatures collected on-ledger and validated against the applicable
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. ATransactionProposalSign, by contrast, must be posted by the signer itself — the submittingAccountis the key holder (its own key when signing single-signature, or theSignerListmember when contributing a multi-sign share forSigningFor) — which ties each collected signature to a deliberate on-ledger act by that signer.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
TransactionProposalSignis rejected unless the supplied signature is cryptographically valid over the immutable proposed transaction and the submitter is authorized for theSigningForaccount (its own key, or a member of its applicableSignerList). This prevents an attacker from polluting a proposal with junk entries and guarantees that a complete proposal will pass standard multi-sign 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. To positively prevent execution of a completed (or nearly-completed) proposal, the target account must consume the proposed transaction's
TicketSequencewith another transaction and/or rely on itsLastLedgerSequencewindow elapsing. Architects and wallets should surface this clearly.13.5. Stale signatures under SignerList changes
Because both the per-signature check and the final submission validate against the live
SignerList, removing a signer or raising the quorum while a proposal is pending is honored: a removed signer's contribution no longer counts toward quorum at submission. Conversely, lowering the quorum can make a previously-incomplete set sufficient. Modifying an account'sSignerListtherefore 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 aBatch— 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.Because anyone may propose against any account and there is one slot per
(target account, ticket)(§4.1), an attacker could squat a slot the real proposer wanted, blocking it withtecDUPLICATE. Each attempt costs a full reserve, and tickets give the honest proposer far more slots than an attacker could block.13.7. Fee accountability
The proposed transaction's fee is paid by the target account when the completed transaction is submitted, consistent with the target account being the party that authorized the action via its signers. Each
TransactionProposalCreate,TransactionProposalSign, andTransactionProposalCancelpays its own fee from its submitter.Appendix
Appendix A: FAQ
A.1: Who can create a proposal?
Any account. The proposer need not be a signer on the target account nor the target account itself. 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
Accountof the proposed transaction; the proposer is the account that submitsTransactionProposalCreate. This is what enables flows like a borrower proposing aLoanSetthat a lender then signs.A.3: How does a signer know what they are signing?
The full proposed transaction is stored, immutable, in the
Transactionfield of the on-ledger object. A signer (or their wallet) reads the object byProposalID, inspects the payload, signs exactly that payload, and submits the signature viaTransactionProposalSign. There is nothing to pass around off-chain.A.4: How is the proposed transaction actually executed?
There is no on-ledger execute step. Signatures accumulate inside the proposed transaction's own
Signersfield, so once quorum weight is reached theTransactionfield 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. To positively block execution, consume the proposed transaction's
TicketSequencewith another transaction or let itsLastLedgerSequenceelapse. 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 submitTransactionProposalCancelto 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'sTicketSequenceonly (§4.1), so there is exactly one proposal slot per(target account, ticket), shared across all proposers. Giving each proposed transaction a distinctTicketSequencelets many concurrent proposals coexist against the same target account without collisions.A.8: Why is the proposed transaction's fee paid by the target account and not the submitter?
The proposed transaction acts on behalf of the target account, authorized by that account's signers. Charging its fee to the target account keeps fee accountability with the party that authorized the action — it is, after all, an ordinary multi-signed transaction of the target account.
A.9: How does signing work when the proposed transaction is a multi-account Batch?
Each
TransactionProposalSignnames an account withSigningFor: the outer account (recorded intoTransaction.Signers) or a participant account (recorded into that participant's entry inTransaction.BatchSigners). A participant is authorized either single-signature (its own key, when the submitter equalsSigningFor) or multi-sign (a share per contributingSignerListmember). Because an XLS-56 batch signature binds the owning account (message = <batch data> + <owning account> + <signer account>), a signer authorized on several accounts must submit oneTransactionProposalSignper account, each with a distinct signature andSigningFor; the same signer key may therefore appear across several participants'BatchSigners. The proposal is complete once the outer account's quorum and every participant's authorization are satisfied; theTransactionfield is then a fully-signed Batch ready to submit. See §6.1.A.10: How is a transaction with a second signer — a
LoanSetcounterparty or a sponsor — handled?Such a transaction has more than one authorization slot: its own
Account(recorded intoTransaction.Signers) plus one auxiliary co-signature per required second party — theCounterpartyof aLoanSet(recorded intoTransaction.CounterpartySignature), theSponsorof a sponsored transaction (recorded intoTransaction.SponsorSignature). Each slot is filled by naming that party inSigningFor(§6.1); a single contribution supplies one party's signature (single-key or one multi-sign share), and the proposal is complete once every slot is satisfied. Because these fields are excluded from every party's signing data, the parties can sign in any order — and a single transaction may need several (a sponsoredLoanSetcollects the borrower, theCounterparty, and theSponsorindependently).A.11: Does this replace off-chain multi-sign?
No. Off-chain multi-sign and standard
SignerListSetcontinue to work unchanged. On-Chain Cosigner is an additive, opt-in coordination layer for accounts that want the ledger to be the meeting room.All reactions