XRPL Multisig Prover
Source: contracts/xrpl-multisig-prover.
The XRPL Multisig Prover constructs fully-canonical XRPL transactions that the on-XRPL multisig account submits to deliver Axelar-to-XRPL traffic. On other Amplifier chains the prover only has to produce an execute_data blob that the destination chain's gateway will then verify and execute; on XRPL there is no destination contract to do that verification, so the prover's output is the on-chain transaction itself. The prover therefore also owns all the XRPL-specific bookkeeping that this implies: a pool of pre-allocated tickets for parallel out-of-order in-flight transactions, an XRP reserve budget for the multisig account, trust line management for receiving local IOUs, and the current/next verifier set view used to populate the multisig account's SignerList.
What XRPL-specific work this contract absorbs compared to the generic prover
| Concern | Generic Multisig Prover | XRPL Multisig Prover |
|---|---|---|
| ConstructProof / signing session lifecycle | Yes | Yes |
| UpdateVerifierSet / ConfirmVerifierSet | Yes (ConfirmVerifierSet from voting verifier) | Replaced by ConfirmProverMessage (verifier rotation rides a SignerListSet confirmation) |
| Output | Encoded calldata for destination chain's gateway | An XRPL canonical-serialized transaction (Payment / SignerListSet / TicketCreate / TrustSet) |
| Sequence management | N/A (no per-account nonce on Axelar side) | Yes: maintains AVAILABLE_TICKETS pool, NEXT_SEQUENCE_NUMBER, LAST_ASSIGNED_TICKET_NUMBER |
| Fee/reserve budget | N/A | Yes: FEE_RESERVE tracks the multisig account's XRP balance budget; ConfirmAddReservesMessage credits top-ups |
| Per-token plumbing | N/A | Yes: TrustSet execute variant for opening trust lines so the multisig account can receive a local-origin IOU |
| Proof status query | Returns full ProofResponse with execute_data | Returns unsigned_tx_hash plus Completed { execute_data: signed XRPL tx blob } |
Interface
pub struct InstantiateMsg {
pub admin_address: String,
pub governance_address: String,
pub gateway_address: String, // the xrpl-gateway
pub multisig_address: String, // the generic multisig contract
pub coordinator_address: String,
pub service_registry_address: String,
pub voting_verifier_address: String, // the xrpl-voting-verifier
pub signing_threshold: MajorityThreshold,
pub service_name: String,
pub chain_name: ChainName, // "xrpl"
pub verifier_set_diff_threshold: u32,
pub xrpl_multisig_address: XRPLAccountId,
// XRPL-specific fee + reserve configuration.
pub xrpl_transaction_fee: u64, // base fee per signer; final tx fee = fee * (1 + n_signers)
pub xrpl_base_reserve: u64, // protocol base reserve
pub xrpl_owner_reserve: u64, // per-owned-object reserve
pub initial_fee_reserve: u64, // bootstrap budget
// XRPL ticket pool bootstrap.
pub ticket_count_threshold: u32, // refill the pool when available count drops below this
pub available_tickets: Vec<u32>, // initial set of usable tickets
pub next_sequence_number: u32,
pub last_assigned_ticket_number: u32,
}
pub enum ExecuteMsg {
// Build an XRPL Payment for a single outgoing message and open a signing session.
// Permission: Any (the relayer calls this; gas is paid via accrued gas accounting).
ConstructProof {
cc_id: CrossChainId,
payload: HexBinary,
},
// Trigger a verifier set rotation: build an XRPL SignerListSet signed by the
// outgoing set. Permission: Elevated (admin or governance).
UpdateVerifierSet,
// Confirm that a prover-built XRPL transaction landed on the ledger.
// After the xrpl-voting-verifier confirms the corresponding ProverMessage poll, the
// relayer calls this to release the ticket, clear the payload, and (for SignerListSet)
// promote NextVerifierSet -> CurrentVerifierSet. Permission: Any.
ConfirmProverMessage { prover_message: XRPLProverMessage },
// Credit a verified XRPLAddReservesMessage to FEE_RESERVE. Permission: Any.
ConfirmAddReservesMessage { add_reserves_message: XRPLAddReservesMessage },
// Build an XRPL TicketCreate transaction. Permission: Any (the relayer calls when the
// pool drops below threshold; the contract decides internally how many to mint).
TicketCreate,
// Build an XRPL TrustSet transaction for the multisig account to be able to hold
// the given local-origin IOU. Permission: Elevated.
TrustSet { token_id: TokenId },
// Admin / parameter updates.
DisableExecution, // Elevated
EnableExecution, // Elevated
UpdateFeeReserve { new_fee_reserve: u64 }, // Elevated
UpdateSigningThreshold { // Governance
new_signing_threshold: MajorityThreshold,
},
UpdateXrplTransactionFee { new_transaction_fee: u64 }, // Elevated
UpdateXrplReserves { // Elevated
new_base_reserve: u64,
new_owner_reserve: u64,
},
UpdateAdmin { new_admin_address: String }, // Elevated
// Verifies a signature against an XRPL multi-sign digest (which includes the signer's
// own XRPL address). Used by the multisig contract during signature submission when
// sig_verifier is set. Permission: Any.
VerifySignature {
signature: HexBinary,
message: HexBinary,
public_key: HexBinary,
signer_address: String,
session_id: Uint64,
},
}
pub enum QueryMsg {
// Returns the unsigned tx hash and either ProofStatus::Pending or
// ProofStatus::Completed { execute_data: <signed XRPL tx blob> }.
Proof { multisig_session_id: Uint64 },
// Same XRPL-specific signature check as the execute variant, callable as a query.
VerifySignature { /* same args */ },
// The current active verifier set view (the one mirrored into the XRPL SignerList).
CurrentVerifierSet,
// The pending NextVerifierSet, if a rotation is in progress.
NextVerifierSet,
// Look up the in-flight multisig session for a specific cc_id.
MultisigSession { cc_id: CrossChainId },
// Recommends how many tickets the next TicketCreate should produce. 0 means no refill needed.
TicketCreate,
IsEnabled,
AvailableTickets,
NextSequenceNumber,
FeeReserve,
}
pub struct ProofResponse {
pub unsigned_tx_hash: HexTxHash,
pub status: ProofStatus,
}
pub enum ProofStatus {
Pending,
Completed { execute_data: HexBinary },
}
Proof construction sequence
sequenceDiagram
autonumber
participant Relayer
box LightYellow Axelar
participant XMP as xrpl-multisig-prover
participant XGW as xrpl-gateway
participant MS as Multisig
end
actor Signers
Relayer->>XMP: ConstructProof
XMP->>XGW: OutgoingMessages
XGW-->>XMP: stored router Message
XMP->>XMP: decode RECEIVE_FROM_HUB,<br/>allocate ticket from AVAILABLE_TICKETS,<br/>build XRPLPaymentTx canonical bytes
XMP->>MS: StartSigningSession with canonical bytes
MS-->>Signers: emit SigningStarted
MS-->>XMP: reply with session ID
XMP-->>Relayer: emit ProofUnderConstruction event
loop Collect signatures
Signers->>MS: SubmitSignature
end
MS-->>Relayer: emit SigningCompleted
Relayer->>XMP: Proof query with session ID
XMP->>MS: Multisig query
MS-->>XMP: state and signatures
XMP-->>Relayer: ProofResponse with signed XRPL tx blob
General steps
- The relayer calls
ConstructProof(cc_id, payload)after the ITS Hub has routed an inbound message into the XRPL Gateway'sOUTGOING_MESSAGESmap. - The prover queries the XRPL Gateway for the stored message, decodes
HubMessage::ReceiveFromHub(InterchainTransfer), and verifiesdatais empty (XRPL has no executable destination; the multisig prover rejects payloads). - It parses the destination as a classic XRPL address, converts the amount into the right
XRPLPaymentAmount(drops for XRP, or 54-bit-mantissa + exponent for an IOU), allocates a ticket fromAVAILABLE_TICKETS, and builds an XRPL Payment transaction whoseSequencefield is the allocated ticket. - The prover opens a signing session at the generic
multisigcontract, passing the XRPL canonical serialization as the message to sign. Each verifier signs a slightly different digest, because XRPL multi-signing requires appending the signer's own XRPL account id to the canonical bytes before the SHA-512-half hash. - Once quorum is reached, the relayer queries
Proof { multisig_session_id }and gets backProofStatus::Completed { execute_data: <signed XRPL tx blob> }. It then broadcasts the blob to the XRPL ledger. - The relayer's subscriber sees the multisig account's outbound transaction land on the ledger and reports it back via
VerifyMessageswith aProverMessage. Once the XRPL Voting Verifier confirms it, the relayer callsConfirmProverMessagewhich releases the ticket and clears the stored payload.
Update and confirm VerifierSet graph
graph TD
Relayer
subgraph Axelar
XMP[xrpl-multisig-prover]
SR[Service Registry]
XVV[xrpl-voting-verifier]
MS[Multisig]
Coord[Coordinator]
end
Relayer -- UpdateVerifierSet --> XMP
XMP -- ActiveVerifiers --> SR
XMP -- RegisterVerifierSet --> MS
XMP -- SetActiveVerifiers --> Coord
Relayer -- ConfirmProverMessage --> XMP
XMP -- MessagesStatus(ProverMessage) --> XVV
Update and confirm VerifierSet sequence
sequenceDiagram
autonumber
participant XRPL as XRPL ledger
participant Relayer
box LightYellow Axelar
participant XGW as xrpl-gateway
participant XMP as xrpl-multisig-prover
participant SR as Service Registry
participant MS as Multisig
participant XVV as xrpl-voting-verifier
end
actor Signers
Relayer->>XMP: UpdateVerifierSet
XMP->>SR: ActiveVerifiers
SR-->>XMP: candidate verifier set
Note over XMP: continue only if diff<br/>exceeds the configured threshold
XMP->>XMP: save as NextVerifierSet
XMP->>MS: StartSigningSession with SignerListSet bytes,<br/>signed by the current verifier set
loop Collect signatures
Signers->>MS: SubmitSignature
end
Relayer->>XMP: Proof query
XMP-->>Relayer: signed SignerListSet tx
Relayer->>XRPL: submit signed SignerListSet
XRPL-->>Relayer: tx confirmed in ledger
Relayer->>XGW: VerifyMessages with ProverMessage
Note over XVV: standard ProverMessage poll runs
Relayer->>XMP: ConfirmProverMessage
XMP->>XVV: MessagesStatus query
XVV-->>XMP: SucceededOnSourceChain
XMP->>XMP: promote NextVerifierSet to CurrentVerifierSet
XMP->>MS: RegisterVerifierSet
General steps
- The relayer (or governance) calls
UpdateVerifierSet. - The prover queries the Service Registry for the active verifier set for chain
xrpl. If the diff against the current set exceedsverifier_set_diff_threshold, the candidate becomes the prover'sNextVerifierSet. - The prover builds an XRPL
SignerListSettransaction that mirrors the new verifier set into the multisig account's on-XRPLSignerList, with weights scaled to fit XRPL'su16weight field, and a quorum derived from the prover'ssigning_threshold. The transaction is signed by the outgoing verifier set. - The relayer broadcasts the signed
SignerListSetto the XRPL ledger. The transaction inclusion is then reported back to Axelar as aProverMessageand confirmed through the standardVerifyMessagespoll machinery. ConfirmProverMessageis called after the poll reaches quorum. The prover promotesNextVerifierSettoCurrentVerifierSet, callsRegisterVerifierSeton the genericmultisigcontract so subsequent signing sessions reference the new set, and pushes the new set to the Coordinator viaSetActiveVerifiersfor cross-chain bookkeeping.
Why ConfirmProverMessage replaces the generic ConfirmVerifierSet
On the generic prover, after a rotateSigners calldata is broadcast to the destination chain, the destination's gateway emits a SignersRotated event which is verified by the destination chain's voting verifier. ConfirmVerifierSet reads that result and promotes the set. On XRPL there is no contract to emit an event: a SignerListSet transaction landing in the ledger is the event. The prover unifies this with the generic message-confirmation path by treating every prover-built outbound XRPL transaction (Payment, SignerListSet, TicketCreate, TrustSet) as a ProverMessage that the XRPL Voting Verifier polls on. ConfirmProverMessage then dispatches on the inner transaction type to release the ticket, promote the verifier set, refresh available tickets, or activate a trust line.
Tickets, sequence numbers, and fee reserve
See Tickets for the full treatment of how the prover manages XRPL sequence numbers, the ticket pool (assignment, reuse under contention, refill flow), and the FEE_RESERVE budget.