Confidential batches
Use ordered batches when several confidential operations must use the state produced by earlier operations in the same transaction. Start with the builders guide for standalone operations.
Build a batch
BuildBatch assembles several confidential operations into one XLS-56 Batch that the
ledger applies in order. It exists because calling the standalone builders in a row cannot
produce one: each of them reads the ledger, and inside a Batch the ledger does not yet show
what an earlier inner leaves behind, so every proof after the first would bind a balance and
a version the transaction will no longer find when it applies.
batch, err := builder.BuildBatch(client, builder.BuildBatchParams{
Account: senderAddress,
Operations: []builder.BatchOperation{
builder.SendOp{BuildSendParams: builder.BuildSendParams{
Account: senderAddress,
Destination: receiverAddress,
IssuanceID: issuanceID,
Amount: 30,
SenderPrivKey: senderKey.PrivKeyHex,
SenderPubKey: senderKey.PubKeyHex,
BalanceRange: elgamal.AmountRange{Low: 0, High: 1_000},
}},
builder.MergeInboxOp{BuildMergeInboxParams: builder.BuildMergeInboxParams{
Account: receiverAddress,
IssuanceID: issuanceID,
}},
},
})
Each of the five confidential operations wraps the parameters of the standalone builder it
mirrors, so an inner reads the same as the call it replaces: ConvertOp, ConvertBackOp,
SendOp, MergeInboxOp, and ClawbackOp. TransactionOp carries a ready-made ordinary
transaction, which the assembler only shapes as an inner. Each operation can be passed as a
value or as a non-nil pointer.
The assembler owns five things:
- Up-front validation. Every operation's inputs are checked before the first ledger query,
by the same validator and with the same sentinels as the standalone builder it mirrors,
including the
TxOptionsrules. An invalid later operation costs no ledger read and no proof for the operations before it. - One validated ledger. Every
MPTokenandMPTokenIssuancetheBatchtouches is read from a single snapshot, pinned by hash after the first read, so no inner's proof mixes state from two ledgers. - Predicted state. A map keyed by the decoded holder
AccountIDand the issuance ID carries the spending and inbox ciphertexts, the issuer and auditor mirror balances, the holder keys, the balance versions, and the public amounts. After each inner it is advanced by exactly what the transactor does, including the re-randomization the network applies to a send's credited ciphertexts and the canonical encrypted zero it writes when it resets a balance: a merge resets the inbox, a holder's first convert starts its spending balance at zero, and a clawback resets every balance of its holder.elgamal.EncryptCanonicalZeroderives that ciphertext from the key, the holder account, and the issuance the same way the network does, so a later inner can spend from a reset balance within the sameBatch. As in the standalone builders, an open-ledger version change rejects the build withErrStaleBalanceVersiononly for a holder whose version a send or convert-back proof binds. - Final nonces. Each inner's
Sequence, or theTicketSequenceit spends instead, is resolved before any proof is generated, because a confidential context hash commits to the nonce and no later autofill can repair a proof. An account's inners take consecutive sequences; the outerBatchaccount's start one past the sequence theBatchitself spends, or at its current sequence when theBatchspends aTicket. ATicketCreateinner moves its account's later sequences past everyTicketit creates. - Inner shape. Every inner carries
tfInnerBatchTxn, a zeroFee, an emptySigningPubKey, and no signature of its own.
Fee and LastLedgerSequence are left unset, so the returned Batch goes through the
client's own autofill, which prices a Batch by summing its inners and charges each
confidential inner the multiplier the network applies. Autofill cannot disturb a proof: every
nonce the proofs bind is already set, and autofill assigns only nonces that are missing.
Signing stays with the caller. Each participating account signs with
wallet.SignMultiBatch, several signatures are merged with wallet.CombineBatchSigners, and
the outer account signs the Batch itself:
flat := batch.Flatten()
if err := client.AutofillMultisigned(&flat, 1); err != nil {
return err
}
if err := wallet.SignMultiBatch(receiverWallet, &flat, nil); err != nil {
return err
}
response, err := client.SubmitTxAndWait(flat, &rpctypes.SubmitOptions{Wallet: &senderWallet})
if err != nil {
return err
}
if response.Meta.TransactionResult != "tesSUCCESS" {
return fmt.Errorf("transaction result: %s", response.Meta.TransactionResult)
}
This fragment uses rpctypes for xrpl/rpc/types. For WebSocket, use xrpl/websocket/types instead. senderWallet and receiverWallet are the account-signing wallets, not the encryption keypairs.
Batch limits
The assembler refuses to emit a proof it can already tell the network will reject. Each refusal has its own sentinel:
ErrBatchOperationCount: aBatchholds between two and eight inners. The check runs before any ledger read, so an impossible size costs nothing.ErrBatchModeNotSupported: onlytfAllOrNothing, the default, is supported. Under any other mode an inner can be skipped or fail while later inners still apply, and every prediction after it would describe a ledger that never happened.ErrBatchMissingOperation: an operation is nil, as an interface or as a pointer, or aTransactionOpcarries no transaction.ErrBatchInnerNotSupported: aTransactionOpof a type the assembler does not accept.IsSupportedInnerTransactionTypereports the allowlist:AccountSet,SetRegularKey,SignerListSet,TicketCreate,TrustSet,DepositPreauth,DelegateSet,CredentialCreate,CredentialAccept, andCredentialDelete. Anything that could change a confidential balance, anMPToken's existence or authorization, or an issuance is kept out, because the assembler would have to predict its effect to keep the later proofs valid:MPTokenAuthorizecreates and deletes theMPTokenthe predictions are keyed by,MPTokenIssuanceSetcan lock an issuance or change its keys, andPaymentandClawbackcan move the public MPT a convert is funded from. Submit those before or after theBatch.ErrBatchInnerSequenceSet: a confidential operation set its ownSequence. The assembler derives every inner sequence from the operation's position, so a caller-set one describes an order it cannot honor. ATicketSequenceis accepted, and the proof binds it in place of the sequence.ErrBatchInnerSequenceMismatch: aTransactionOpcarries aSequencethat is not the one its position in theBatchrequires for its account, such as the outerBatch's own sequence, one past an allocated inner, or one aTicketCreateearlier in theBatchturned into aTicket. This holds for every account, so a caller-set sequence of an account other than the outer one is checked against that account's current sequence.ErrConflictingNonce: an inner, confidential or ready-made, sets bothSequenceandTicketSequence. The network requires exactly one.ErrBatchDuplicateNonce: two inners of one account spend the same sequence orTicket, or an inner spends theTicketthe outerBatchitself spends. The network rejects an all-or-nothingBatchthat repeats a nonce.
Everything the standalone builders reject, a Batch inner rejects too, with the same
sentinel: the issuance capability checks, the locked and authorized preflights, the key
mismatches, and the balance bounds. The bounds are checked against the running state rather
than the pre-batch ledger, so a convert earlier in the same Batch funds a later convert-back
and widens the decryption bound a later spend searches under.
For protocol fields and authorization, see Batch. For nonce and proof-state restrictions, see transaction options.