Title: Atomic/Batch Transactions Author: Mayukha Vadari Affiliation: Ripple
The XRP Ledger has a robust set of built-in features, enabling fast and efficient transactions without the need for complex smart contracts on every step. However, a key limitation exists: multiple transactions cannot be executed atomically. This means that if a complex operation requires several transactions, a failure in one can leave the system in an incomplete or unexpected state. Imagine building a house: you wouldn't want to lay the foundation and build the walls, and then discover you can't afford the roof, leaving you with an unfinished and unusable structure.
This document proposes a design to allow multiple transactions to be packaged together and executed as a single unit. It's like laying the foundation, building the walls, and raising the roof all in one single secure step, leveraging the existing strengths of the XRP Ledger. If you're unable to afford the roof, you won't even bother laying the foundation.
This eliminates the risk of partial completion and unexpected outcomes, fostering a more reliable and predictable user experience for complex operations. By introducing these batch transactions, developers gain the ability to design innovative features and applications that were previously hindered by the lack of native smart contracts for conditional workflows. This empowers them to harness the full potential of the XRP Ledger's built-in features while ensuring robust execution of complex processes.
Some use-cases that may be enabled by batch transactions include (but are certainly not limited to):
- All or nothing: Mint an NFT and create an offer for it in one transaction. If the offer creation fails, the NFT mint is reverted as well.
- Trying out a few offers: Submit multiple offers with different amounts of slippage, but only one will succeed.
- Platform fees: Package platform fees within the transaction itself, simplifying the process.
- Trustless swaps (multi-account): Trustless token/NFT swaps between multiple accounts.
- Flash loans: Borrowing funds, using them, and returning them all in the same transaction.
- Withdrawing accounts (multi-account): Attempt a withdrawal from your checking account, and if that fails, withdraw from your savings account instead.
This spec adds one new transaction: Batch
. It also adds an addition to the common fields of all transactions. It will not require any new ledger objects, nor modifications to existing ledger objects. It will require an amendment, tentatively titled featureBatch
.
The rough idea of this design is that users can include "sub-transactions" inside Batch
, and these transactions are processed atomically. The design also supports transactions from different accounts in the same Batch
wrapper transaction.
- Inner transaction: the sub-transactions included in the
Batch
transaction, that are executed atomically. - Outer transaction: the wrapper
Batch
transaction itself. - Batch mode or mode: the "mode" of batch processing that the transaction uses. See section 2.2 for more details.
Field Name | Required? | JSON Type | Internal Type | Description |
---|---|---|---|---|
Flags |
✔️ | number |
UInt32 |
A bit-map of boolean flags enabled for this transaction. These flags represent the batch mode of the transaction. |
RawTransactions |
✔️ | array |
STArray |
The list of inner transactions that will be applied. |
BatchSigners |
array |
STArray |
An array of objects that represent signatures for a multi-account Batch transaction, signifying authorization of this transaction. |
The Flags
field represents the batch mode of the transaction. Exactly one must be specified in a Batch
transaction.
This spec supports four modes:
ALLORNOTHING
ortfAllOrNothing
(with a value of0x00010000
)ONLYONE
ortfOnlyOne
(with a value of0x00020000
)UNTILFAILURE
ortfUntilFailure
(with a value of0x00040000
)INDEPENDENT
ortfIndependent
(with a value of0x00080000
)
A transaction will be considered a failure if it receives any result that is not tesSUCCESS
.
All or nothing. All transactions must succeed for any of them to succeed.
The first transaction to succeed will be the only one to succeed; all other transactions either failed or were never tried.
While this can sort of be done by submitting multiple transactions with the same sequence number, there is no guarantee that the transactions are processed in the same order they are sent.
All transactions will be applied until the first failure, and all transactions after the first failure will not be applied.
All transactions will be applied, regardless of failure.
RawTransactions
contains the list of transactions that will be applied. There can be up to 8 transactions included. These transactions can come from one account or multiple accounts.
Each inner transaction:
- Must contain the
tfInnerBatchTxn
flag (see section 3 for details). - Must not have a fee. It must use a fee value of
"0"
. - Must not be signed (the global transaction is already signed by all relevant parties). They must instead have an empty string (
""
) in theSigningPubKey
field, and theTxnSignature
field must be omitted.
Note: the 8 transaction limit can be relaxed in the future.
This field operates similarly to multi-signing on the XRPL. It is only needed if multiple accounts' transactions are included in the Batch
transaction; otherwise, the normal transaction signature provides the same security guarantees.
This field must be provided if more than one account has inner transactions included in the Batch
. In that case, this field must contain signatures from all accounts whose inner transactions are included, excluding the account signing the outer transaction (if applicable).
Each object in this array contains the following fields:
FieldName | Required? | JSON Type | Internal Type |
---|---|---|---|
Account |
✔️ | string |
STAccount |
SigningPubKey |
string |
STBlob |
|
TxnSignature |
string |
STBlob |
|
Signers |
array |
STArray |
Either the SigningPubKey
and TxnSignature
fields must be included, or the Signers
field.
This is an account that has at least one inner transaction.
These fields are included if the account is signing with a single signature (as opposed to multi-sign). They sign the Flags
field and the hashes of the transactions in RawTransactions
.
This field is included if the account is signing with multi-sign (as opposed to a single signature). It operates equivalently to the Signers
field used in standard transaction multi-sign. This field holds the signatures for the Flags
field and the hashes of the transactions in RawTransactions
.
The fee for the outer transaction is:
n
is the number of signatures included in the outer transaction)
In other words, the fee is twice the base fee (a total of 20 drops when there is no fee escalation), plus the sum of the transaction fees of all the inner transactions (which incorporates factors like higher fees for AMMCreate
or EscrowFinish
), plus an additional base fee amount for each additional signature in the transaction (e.g. from BatchSigners
).
The fees for the individual inner transactions are paid here instead of in the inner transaction itself, to ensure that fee escalation is calculated on the total cost of the transaction instead of just the overhead.
The inner transactions will be committed separately to the ledger and will therefore have separate metadata. This is to ensure better backwards compatibility for legacy systems, so they can support Batch
transactions without needing any changes to their systems.
For example, a ledger that only has one Batch
transaction containing 2 inner transactions would look like this:
[
OuterTransaction,
InnerTransaction1,
InnerTransaction2
]
Each outer transaction will only contain the metadata for its sequence and fee processing, not for the inner transaction processing. The error code will also only be based on the outer transaction processing (e.g. sequence and fee), and it will return a tesSUCCESS
error even if the inner transaction processing fails.
Each inner transaction will contain the metadata for its own processing. Only the inner transactions that were actually committed to the ledger will be included. This makes it easier for legacy systems to still be able to process Batch
transactions as if they were normal.
There will also be a pointer back to the parent outer transaction (ParentBatchID
).
This standard doesn't add any new field to the transaction common fields, but it does add another global transaction flag:
Flag Name | Value |
---|---|
tfInnerBatchTxn |
0x40000000 |
This flag should only be used if a transaction is an inner transaction in a Batch
transaction. This signifies that the transaction shouldn't be signed. Any normal transaction that includes this flag should be rejected.
Regardless of how many accounts' transactions are included in a Batch
transaction, all accounts need to sign the collection of transactions.
In the single account case, this is obvious; the single account must approve all of the transactions it is submitting. No other accounts are involved, so this is a pretty straightforward case.
The multi-account case is a bit more complicated and is best illustrated with an example. Let's say Alice and Bob are conducting a trustless swap via a multi-account Batch
, with Alice providing 1000 XRP and Bob providing 1000 USD. Bob is going to submit the Batch
transaction, so Alice must provide her part of the swap to him.
If Alice provides a fully autofilled and signed transaction to Bob, Bob could submit Alice's transaction on the ledger without submitting his and receive the 1000 XRP without losing his 1000 USD. Therefore, the inner transactions must be unsigned.
If Alice just signs her part of the Batch
transaction, Bob could modify his transaction to only provide 1 USD instead, thereby getting his 1000 XRP at a much cheaper rate. Therefore, the entire Batch
transaction (and all its inner transactions) must be signed by all parties.
An inner batch transaction is a very special case. It doesn't include a signature or a fee (since those are both included in the outer transaction). Therefore, they must be handled very carefully to ensure that someone can't somehow directly submit an inner Batch
transaction without it being included in an outer transaction.
Namely:
- Inner transactions may not be broadcast (and won't be accepted if they happen to be broadcast, e.g. from a malicious node). They must be generated from the
Batch
outer transaction instead. - Inner transactions may not be directly submitted via the
submit
RPC.
In this example, the user is creating an offer while trading on a DEX UI, and the second transaction is a platform fee.
The inner transactions are not signed, and the BatchSigners
field is not needed on the outer transaction, since there is only one account involved.
BatchSigners
field is not needed on the outer transaction, since there is only one account involved.{
TransactionType: "Batch",
Account: "rUserBSM7T3b6nHX3Jjua62wgX9unH8s9b",
Flags: 0x00010000,
RawTransactions: [
{
RawTransaction: {
TransactionType: "OfferCreate",
Flags: 1073741824,
Account: "rUserBSM7T3b6nHX3Jjua62wgX9unH8s9b",
TakerGets: "6000000",
TakerPays: {
currency: "GKO",
issuer: "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
value: "2"
},
Sequence: 4,
Fee: "0",
SigningPubKey: ""
}
},
{
RawTransaction: {
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUserBSM7T3b6nHX3Jjua62wgX9unH8s9b",
Destination: "rDEXfrontEnd23E44wKL3S6dj9FaXv",
Amount: "1000",
Sequence: 5,
Fee: "0",
SigningPubKey: ""
}
}
],
Sequence: 3,
Fee: "40",
SigningPubKey: "022D40673B44C82DEE1DDB8B9BB53DCCE4F97B27404DB850F068DD91D685E337EA",
TxnSignature: "3045022100EC5D367FAE2B461679AD446FBBE7BA260506579AF4ED5EFC3EC25F4DD1885B38022018C2327DB281743B12553C7A6DC0E45B07D3FC6983F261D7BCB474D89A0EC5B8"
}
This example shows what the ledger will look like after the transaction is confirmed.
Note that the inner transactions are committed as normal transactions.
[
{
TransactionType: "Batch",
Account: "rUserBSM7T3b6nHX3Jjua62wgX9unH8s9b",
Flags: 0x00010000,
RawTransactions: [
{
RawTransaction: {
TransactionType: "OfferCreate",
Flags: 1073741824,
Account: "rUserBSM7T3b6nHX3Jjua62wgX9unH8s9b",
TakerGets: "6000000",
TakerPays: {
currency: "GKO",
issuer: "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
value: "2"
},
Sequence: 4,
Fee: "0",
SigningPubKey: ""
}
},
{
RawTransaction: {
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUserBSM7T3b6nHX3Jjua62wgX9unH8s9b",
Destination: "rDEXfrontEnd23E44wKL3S6dj9FaXv",
Amount: "1000",
Sequence: 5,
Fee: "0",
SigningPubKey: ""
}
}
],
Sequence: 3,
Fee: "40",
SigningPubKey: "022D40673B44C82DEE1DDB8B9BB53DCCE4F97B27404DB850F068DD91D685E337EA",
TxnSignature: "3045022100EC5D367FAE2B461679AD446FBBE7BA260506579AF4ED5EFC3EC25F4DD1885B38022018C2327DB281743B12553C7A6DC0E45B07D3FC6983F261D7BCB474D89A0EC5B8"
},
{
TransactionType: "OfferCreate",
Flags: 1073741824,
Account: "rUserBSM7T3b6nHX3Jjua62wgX9unH8s9b",
TakerGets: "6000000",
TakerPays: {
currency: "GKO",
issuer: "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
value: "2"
},
Sequence: 4,
Fee: "0",
SigningPubKey: ""
},
{
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUserBSM7T3b6nHX3Jjua62wgX9unH8s9b",
Destination: "rDEXfrontEnd23E44wKL3S6dj9FaXv",
Amount: "1000",
Sequence: 5,
Fee: "0",
SigningPubKey: ""
}
]
In this example, two users are atomically swapping their tokens, XRP for GKO.
The inner transactions are still not signed, but the BatchSigners
field is needed on the outer transaction, since there are two accounts' inner transactions in this Batch
transaction.
BatchSigners
field is needed on the outer transaction, since there are two accounts' inner transactions in this Batch
transaction.{
TransactionType: "Batch",
Account: "rUser1fcu9RJa5W1ncAuEgLJF2oJC6",
Flags: 0x00010000,
RawTransactions: [
{
RawTransaction: {
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUser1fcu9RJa5W1ncAuEgLJF2oJC6",
Destination: "rUser2fDds782Bd6eK15RDnGMtxf7m",
Amount: "6000000",
Sequence: 5,
Fee: "0",
SigningPubKey: ""
}
},
{
RawTransaction: {
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUser2fDds782Bd6eK15RDnGMtxf7m",
Destination: "rUser1fcu9RJa5W1ncAuEgLJF2oJC6",
Amount: {
currency: "GKO",
issuer: "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
value: "2"
},
Sequence: 20,
Fee: "0",
SigningPubKey: ""
}
}
],
BatchSigners: [
{
BatchSigner: {
Account: "rUser2fDds782Bd6eK15RDnGMtxf7m",
SigningPubKey: "03C6AE25CD44323D52D28D7DE95598E6ABF953EECC9ABF767F13C21D421C034FAB",
TxnSignature: "304502210083DF12FA60E2E743643889195DC42C10F62F0DE0A362330C32BBEC4D3881EECD022010579A01E052C4E587E70E5601D2F3846984DB9B16B9EBA05BAD7B51F912B899"
}
},
],
Sequence: 4,
Fee: "60",
SigningPubKey: "03072BBE5F93D4906FC31A690A2C269F2B9A56D60DA9C2C6C0D88FB51B644C6F94",
TxnSignature: "30440220702ABC11419AD4940969CC32EB4D1BFDBFCA651F064F30D6E1646D74FBFC493902204E5B451B447B0F69904127F04FE71634BD825A8970B9467871DA89EEC4B021F8"
}
This example shows what the ledger will look like after the transaction is confirmed.
Note that the inner transactions are committed as normal transactions.
[
{
TransactionType: "Batch",
Account: "rUser1fcu9RJa5W1ncAuEgLJF2oJC6",
Flags: 0x00010000,
RawTransactions: [
{
RawTransaction: {
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUser1fcu9RJa5W1ncAuEgLJF2oJC6",
Destination: "rUser2fDds782Bd6eK15RDnGMtxf7m",
Amount: "6000000",
Sequence: 5,
Fee: "0",
SigningPubKey: ""
}
},
{
RawTransaction: {
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUser2fDds782Bd6eK15RDnGMtxf7m",
Destination: "rUser1fcu9RJa5W1ncAuEgLJF2oJC6",
Amount: {
currency: "GKO",
issuer: "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
value: "2"
},
Sequence: 20,
Fee: "0",
SigningPubKey: ""
}
}
],
BatchSigners: [
{
BatchSigner: {
Account: "rUser2fDds782Bd6eK15RDnGMtxf7m",
SigningPubKey: "03C6AE25CD44323D52D28D7DE95598E6ABF953EECC9ABF767F13C21D421C034FAB",
TxnSignature: "304502210083DF12FA60E2E743643889195DC42C10F62F0DE0A362330C32BBEC4D3881EECD022010579A01E052C4E587E70E5601D2F3846984DB9B16B9EBA05BAD7B51F912B899"
}
},
],
Sequence: 4,
Fee: "60",
SigningPubKey: "03072BBE5F93D4906FC31A690A2C269F2B9A56D60DA9C2C6C0D88FB51B644C6F94",
TxnSignature: "30440220702ABC11419AD4940969CC32EB4D1BFDBFCA651F064F30D6E1646D74FBFC493902204E5B451B447B0F69904127F04FE71634BD825A8970B9467871DA89EEC4B021F8"
},
{
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUser1fcu9RJa5W1ncAuEgLJF2oJC6",
Destination: "rUser2fDds782Bd6eK15RDnGMtxf7m",
Amount: "6000000",
Sequence: 5,
Fee: "0",
SigningPubKey: ""
},
{
TransactionType: "Payment",
Flags: 1073741824,
Account: "rUser2fDds782Bd6eK15RDnGMtxf7m",
Destination: "rUser1fcu9RJa5W1ncAuEgLJF2oJC6",
Amount: {
currency: "GKO",
issuer: "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
value: "2"
},
Sequence: 20,
Fee: "0",
SigningPubKey: ""
}
]
A.1: What if I want a more complex tree of transactions that are AND/XORed together? Can I nest Batch
transactions?
The original version of this spec supported nesting Batch
transactions. However, upon further analysis, that was deemed a bit too complicated for an initial version of Batch
, as most of the benefit from this new feature does not require nested transactions. Based on user and community need, this could be added as part of a V2.
Yes, just as they would if they were individually submitted.
That is definitely a concern. Ways to mitigate this are still being investigated. Some potential answers:
- Charge for more extensive path usage
- Have higher fees for
Batch
transactions - Submit the
Batch
transactions at the end of the ledger
The only way to directly tell that this fails is because unless there is a tec
error, no inner transaction would be validated. Some debug tools will be provided (such as parsing the rippled debug logs), and ideas to improve this in the future (such as a separate database to store Batch
outputs) are being discussed.
A.5: Can another account sign/pay for the outer transaction if they don't have any of the inner transactions?
If there are multiple parties in the inner transactions, yes. Otherwise, no. This is because in a single party Batch
transaction, the inner transaction's "signature" (which signals approval of the transactions by the accounts submitting them) is provided by the normal transaction signing fields (SigningPubKey
and TxnSignature
).
Right now, if you submit a series of transactions with consecutive sequence numbers without the use of tickets or Batch
, then if one fails in the middle, all subsequent transactions will also fail due to incorrect sequence numbers (since the one that failed would have the next sequence numbers).
The difference between the UNTILFAILURE
mode and this existing behavior is that right now, the subsequent transactions will only fail with a non-tec
error code. If the failed transaction receives an error code starting with tec
, then a fee is claimed and a sequence number is consumed, and the subsequent transactions will still be processed as usual.
A.7: How is the INDEPENDENT
mode any different than existing behavior with tickets?
Tickets require temporarily having a reserve for all the tickets you want to create, but an INDEPENDENT
mode Batch
transaction doesn't, for the low cost of 10 extra drops.
On the flip side, tickets are still needed for other use-cases, such as needing to coordinate multiple signers or needing out-of-order transactions.
A.8: Is it possible for inner transactions to end up in a different ledger than the outer transaction?
No, because the inner transactions skip the transaction queue. They are already effectively processed by the queue via the outer transaction. Inner transactions will also be excluded from consensus for the same reason.
A.9: How does this work in conjunction with XLS-75d? If I give an account powers over the Batch
transaction, can it effectively run all transaction types?
No, OnBehalfOf
will not be allowed on Batch
transactions. Instead, the inner transactions must include the OnBehalfOf
field.
A.10: What if I want some error code types to be allowed to proceed, just as tesSUCCESS
would, in e.g. an ALLORNOTHING
case?
This was deemed unnecessary. If you have a need for this, please provide example use-cases.
The primary use of sequence numbers is to prevent hash collisions - two otherwise-identical transactions must have different sequence numbers (or different TicketSequence
values).
In addition, some objects, such as offers and escrows, use the sequence number of the creation transaction as a part of their ledger entry ID generation, to ensure uniqueness of the IDs.
For this reason, inner transactions must include sequence numbers.
That is not supported. This also allows fee escalation to be calculated on the total cost of the transaction, instead of just on the overhead, and ensures that even if all of the transactions fail, the transaction fees are still charged.
Yes, as long as you use the master key of that account. Setting a regular key/signer list in the middle of the Batch
won't work, since that key isn't valid at the time of processing the outer transaction (when the signatures are checked).
A.14: How will transaction simulation work with Batch?
Some extra processing will be needed for that. As a result, Batch transactions likely won't be able to be simulated at first.