syntax = "proto3"; package spark_internal; import "common.proto"; import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "spark.proto"; import "validate/validate.proto"; option go_package = "github.com/lightsparkdev/spark/proto/spark_internal"; service SparkInternalService { rpc mark_keyshares_as_used(MarkKeysharesAsUsedRequest) returns (google.protobuf.Empty) {} rpc mark_keyshare_for_deposit_address(MarkKeyshareForDepositAddressRequest) returns (MarkKeyshareForDepositAddressResponse) {} rpc reserve_entity_dkg_key(ReserveEntityDkgKeyRequest) returns (google.protobuf.Empty) {} rpc finalize_tree_creation(FinalizeTreeCreationRequest) returns (google.protobuf.Empty) {} rpc frost_round1(FrostRound1Request) returns (FrostRound1Response) {} rpc frost_round2(FrostRound2Request) returns (FrostRound2Response) {} rpc finalize_transfer(FinalizeTransferRequest) returns (google.protobuf.Empty) {} rpc finalize_refresh_timelock(FinalizeRefreshTimelockRequest) returns (google.protobuf.Empty) { option deprecated = true; } rpc finalize_extend_leaf(FinalizeExtendLeafRequest) returns (google.protobuf.Empty) { option deprecated = true; } rpc finalize_renew_refund_timelock(FinalizeRenewRefundTimelockRequest) returns (google.protobuf.Empty) {} rpc finalize_renew_node_timelock(FinalizeRenewNodeTimelockRequest) returns (google.protobuf.Empty) {} rpc node_available_for_renew(NodeAvailableForRenewRequest) returns (google.protobuf.Empty) {} rpc initiate_preimage_swap(spark.InitiatePreimageSwapRequest) returns (InitiatePreimageSwapResponse) {} rpc initiate_preimage_swap_v2(InitiatePreimageSwapRequest) returns (InitiatePreimageSwapResponse) {} rpc update_preimage_request(UpdatePreimageRequestRequest) returns (google.protobuf.Empty) {} rpc store_preimage_share(spark.StorePreimageShareV2Request) returns (google.protobuf.Empty) {} rpc prepare_tree_address(PrepareTreeAddressRequest) returns (PrepareTreeAddressResponse) {} rpc initiate_transfer(InitiateTransferRequest) returns (google.protobuf.Empty) {} rpc initiate_transfer_v2(InitiateTransferV2Request) returns (google.protobuf.Empty) {} rpc deliver_sender_key_tweak(DeliverSenderKeyTweakRequest) returns (google.protobuf.Empty) {} rpc initiate_cooperative_exit(InitiateCooperativeExitRequest) returns (google.protobuf.Empty) {} rpc initiate_settle_receiver_key_tweak(InitiateSettleReceiverKeyTweakRequest) returns (google.protobuf.Empty) {} rpc settle_receiver_key_tweak(SettleReceiverKeyTweakRequest) returns (google.protobuf.Empty) {} rpc settle_sender_key_tweak(SettleSenderKeyTweakRequest) returns (google.protobuf.Empty) {} // Create UTXO swap record to claim UTXO by SSP in the static deposit flow rpc create_static_deposit_utxo_swap(CreateStaticDepositUtxoSwapRequest) returns (CreateStaticDepositUtxoSwapResponse) {} // Create UTXO swap record to refund UTXO to the user in the static deposit flow rpc create_static_deposit_utxo_refund(CreateStaticDepositUtxoRefundRequest) returns (CreateStaticDepositUtxoRefundResponse) {} // Create instant static deposit UTXO swap record across all SOs rpc create_instant_static_deposit_utxo_swap(CreateInstantStaticDepositUtxoSwapRequest) returns (CreateInstantStaticDepositUtxoSwapResponse) {} // Save utxo for an instant static deposit across all SOs rpc save_utxo_for_instant_static_deposit(SaveUtxoForInstantStaticDepositRequest) returns (SaveUtxoForInstantStaticDepositResponse) {} // Link transfer edge to utxo swap on non-coordinator SOs (used by instant static deposits) rpc link_utxo_swap_transfer(LinkUtxoSwapTransferRequest) returns (LinkUtxoSwapTransferResponse) {} // Internal method to cancel a swap for other SOs if one of them failed to ack it rpc rollback_utxo_swap(RollbackUtxoSwapRequest) returns (RollbackUtxoSwapResponse) {} // Internal method to cancel an instant UTXO swap for other SOs if one of them failed to ack it rpc rollback_instant_utxo_swap(RollbackInstantUtxoSwapRequest) returns (RollbackInstantUtxoSwapResponse) {} // Internal method to mark a swap as COMPLETE in all SOs rpc utxo_swap_completed(UtxoSwapCompletedRequest) returns (UtxoSwapCompletedResponse) {} rpc query_leaf_signing_pubkeys(QueryLeafSigningPubkeysRequest) returns (QueryLeafSigningPubkeysResponse) {} rpc resolve_leaf_investigation(ResolveLeafInvestigationRequest) returns (google.protobuf.Empty) {} rpc fix_keyshare(FixKeyshareRequest) returns (google.protobuf.Empty) {} rpc fix_keyshare_round1(FixKeyshareRound1Request) returns (FixKeyshareRound1Response) {} rpc fix_keyshare_round2(FixKeyshareRound2Request) returns (FixKeyshareRound2Response) {} rpc get_transfers(GetTransfersRequest) returns (GetTransfersResponse) {} // Generate proofs of possession for a static deposit address. // The client can use them to validate that all SOs know about this address. // The coordinator can use them to validate if an address was created correctly. rpc generate_static_deposit_address_proofs(GenerateStaticDepositAddressProofsRequest) returns (GenerateStaticDepositAddressProofsResponse) {} // This method fixes bad leaves by querying a designated "good" SO for its // leaves, and copying it over to this SO's DB. rpc sync_node(SyncNodeRequest) returns (google.protobuf.Empty) {} // Query nodes by ID for operator-to-operator reconciliation. Unlike the // public SparkService.query_nodes, this internal variant bypasses the // per-wallet privacy filter: the filter exists to stop end users reading // nodes for wallets they cannot access, which has no bearing on trusted, // IP-restricted SO-to-SO sync. Without this, syncing leaves owned by a // privacy-enabled wallet returns zero nodes ("expected N, got 0"). rpc query_nodes(spark.QueryNodesRequest) returns (spark.QueryNodesResponse) {} // ConsensusPrepare is the generic prepare RPC for the 2PC consensus engine. // The coordinator fans this out to all participants during Execute. // Each participant dispatches to the appropriate FlowHandler.Prepare based on op_type. rpc consensus_prepare(ConsensusPrepareRequest) returns (ConsensusPrepareResponse) {} // ConsensusQueryOutcome lets a participant ask the coordinator what the // final outcome of a 2PC execution was. Used by the flow-execution // reconciliation background task to resolve stuck participant rows. // Looks up the coordinator's FlowExecution row by id and returns its // status + decision payload. rpc consensus_query_outcome(ConsensusQueryOutcomeRequest) returns (ConsensusQueryOutcomeResponse) {} } message MarkKeysharesAsUsedRequest { repeated string keyshare_id = 1; } message MarkKeyshareForDepositAddressRequest { string keyshare_id = 1; string address = 2; bytes owner_identity_public_key = 3; bytes owner_signing_public_key = 4; // Static deposit address flag optional bool is_static = 5; } message MarkKeyshareForDepositAddressResponse { bytes address_signature = 1; } message FrostRound1Request { // DEPRECATED: This field is ignored when random_nonce_count is set. // Prefer setting random_nonce_count directly. repeated string keyshare_ids = 1 [deprecated = true]; // DEPRECATED: Not used by the handler. Commitments are independent of long-term public keys. map public_keys = 2 [deprecated = true]; // DEPRECATED: This field is ignored when random_nonce_count is set. Prefer random_nonce_count. uint32 count = 3 [deprecated = true]; // If this is set to non-zero, the keyshare_ids will be ignored and the random keys will be used. uint32 random_nonce_count = 4; } message FrostRound1Response { repeated common.SigningCommitment signing_commitments = 1; } message SigningJob { // The unique identifier for the signing job. string job_id = 1 [(validate.rules).string.uuid = true]; // The message to sign. bytes message = 2; // The secret package from DKG round 3. string keyshare_id = 3; // The verifying key. This should be the total public key for both user and StatechainEntity. bytes verifying_key = 4; // The commitments for all participants of the statechain. map commitments = 5; // The commitments for all participants of the user. common.SigningCommitment user_commitments = 6; // The adaptor for the message. bytes adaptor_public_key = 7; } message FrostRound2Request { repeated SigningJob signing_jobs = 1; } message FrostRound2Response { map results = 1; } message FinalizeTreeCreationRequest { repeated TreeNode nodes = 1; spark.Network network = 2; } message FinalizeTransferRequest { string transfer_id = 1; repeated TreeNode nodes = 2; google.protobuf.Timestamp timestamp = 3; } message FinalizeRefreshTimelockRequest { repeated TreeNode nodes = 1; } message FinalizeExtendLeafRequest { TreeNode node = 1; } message FinalizeRenewRefundTimelockRequest { TreeNode node = 1; } message FinalizeRenewNodeTimelockRequest { TreeNode split_node = 1; TreeNode node = 2; } message NodeAvailableForRenewRequest { string node_id = 1; } message TreeNode { string id = 1; uint64 value = 2; bytes verifying_pubkey = 3; bytes owner_identity_pubkey = 4; bytes owner_signing_pubkey = 5; bytes raw_tx = 6; bytes raw_refund_tx = 7; string tree_id = 8; optional string parent_node_id = 9; string signing_keyshare_id = 10; uint32 vout = 11; uint32 refund_timelock = 12; bytes direct_tx = 13; bytes direct_refund_tx = 14; bytes direct_from_cpfp_refund_tx = 15; } message InitiatePreimageSwapRequest { spark.InitiatePreimageSwapRequest request = 1; map cpfp_refund_signatures = 2; map direct_refund_signatures = 3; map direct_from_cpfp_refund_signatures = 4; } message InitiatePreimageSwapResponse { bytes preimage_share = 1; } message PrepareTreeAddressNode { string signing_keyshare_id = 1; bytes user_public_key = 2; repeated PrepareTreeAddressNode children = 3; } message PrepareTreeAddressRequest { string target_keyshare_id = 1; PrepareTreeAddressNode node = 2; // TODO: This should be removed once we have a proper auth process. bytes user_identity_public_key = 3; spark.Network network = 4; } message PrepareTreeAddressResponse { map signatures = 1; } message InitiateTransferLeaf { string leaf_id = 1; bytes raw_refund_tx = 2; bytes direct_refund_tx = 3; bytes direct_from_cpfp_refund_tx = 4; } message InitiateTransferRequest { string transfer_id = 1; bytes sender_identity_public_key = 2; bytes receiver_identity_public_key = 3; google.protobuf.Timestamp expiry_time = 4; repeated InitiateTransferLeaf leaves = 5; map sender_key_tweak_proofs = 6; spark.TransferType type = 7; spark.TransferPackage transfer_package = 8; // The finalized signatures for the cpfp refund transactions. map refund_signatures = 9; // The finalized signatures for the direct refund transactions. map direct_refund_signatures = 10; // The finalized signatures for the direct from cpfp refund transactions. map direct_from_cpfp_refund_signatures = 11; // The invoice this transfer pays. string spark_invoice = 12; // Adaptor public keys to verify adapted signatures for tree node refund transactions. spark.AdaptorPublicKeyPackage adaptor_public_keys = 13; // Primary transfer ID of a counter transfer in Swap V3 flow. Transfers are // required to be connected so their keys can be tweaked at the same time. // For non-SwapV3 flows this field will be an empty string. string primary_transfer_id = 14; } // Per-sender data within an InitiateTransferV2Request. message InitiateTransferSenderPackage { bytes sender_identity_public_key = 1; spark.TransferPackage transfer_package = 2; // leaf_id → receiver_identity_public_key for each leaf in this sender's package. map receiver_identity_public_keys = 3; // Finalized CPFP refund signatures (leaf_id → signature). map refund_signatures = 4; // Finalized direct refund signatures (leaf_id → signature). map direct_refund_signatures = 5; // Finalized direct-from-CPFP refund signatures (leaf_id → signature). map direct_from_cpfp_refund_signatures = 6; } // Internal SO-to-SO request to create one Transfer with multiple senders and receivers. // Receiving SOs must accept all sender packages atomically (all or nothing). message InitiateTransferV2Request { string transfer_id = 1; // One entry per sender. Leaf IDs must be disjoint across entries. repeated InitiateTransferSenderPackage sender_packages = 2; // FROST key-tweak proofs keyed by leaf_id (across all senders). map sender_key_tweak_proofs = 3; google.protobuf.Timestamp expiry_time = 4; } message DeliverSenderKeyTweakRequest { string transfer_id = 1; bytes sender_identity_public_key = 2; spark.TransferPackage transfer_package = 3; // Cross-SO check: non-coordinator decrypted proofs must match these. map sender_key_tweak_proofs = 4; } message InitiateCooperativeExitRequest { InitiateTransferRequest transfer = 1; string exit_id = 2; bytes exit_txid = 3; bytes connector_tx = 4; } message UpdatePreimageRequestRequest { string preimage_request_id = 1; bytes preimage = 2; bytes identity_public_key = 3; } message InitiateSettleReceiverKeyTweakRequest { string transfer_id = 1; map key_tweak_proofs = 2; map user_public_keys = 3; // Full ECIES-encrypted key tweak package (SO identifier -> ciphertext). // Each SO verifies the user signature against this full map, then decrypts its own portion. map encrypted_claim_key_tweak_package = 4; // User signature over the encrypted claim key tweak package. bytes claim_signature = 5; bytes receiver_identity_public_key = 6; } enum SettleKeyTweakAction { NONE = 0; COMMIT = 1; ROLLBACK = 2; } message SettleReceiverKeyTweakRequest { string transfer_id = 1; SettleKeyTweakAction action = 3; bytes receiver_identity_public_key = 4; reserved 2; } message SettleSenderKeyTweakRequest { string transfer_id = 1; SettleKeyTweakAction action = 2; } message ReserveInstantStaticDepositUtxoSwapRequest { spark.UTXO on_chain_utxo = 1; // SSP signature of a message consenting to transfer a specific amount of funds // in exchange for a specific UTXO. bytes ssp_signature = 2; // User signature of a message consenting to exchange a specific UTXO for a fund // transfer from a specific SSP. bytes user_signature = 3; // Transfer package to send from SSP to User. spark.StartTransferRequest transfer = 4; // Destination address for the static deposit UTXO. string destination_address = 5; // UTXO output value in satoshis. int64 value_sats = 6; // credit amount in satoshis. int64 credit_amount_sats = 7; // secondary credit amount in satoshis. int64 secondary_credit_amount_sats = 8; // Deprecated: the SO no longer reads or stores this field. Will be removed once the SSP stops sending it. google.protobuf.Timestamp expiry_time = 9 [deprecated = true]; // Optional: requested secondary transfer ID for secondary credit amount. string requested_secondary_transfer_id = 10; } message CreateInstantStaticDepositUtxoSwapRequest { ReserveInstantStaticDepositUtxoSwapRequest request = 1; bytes signature = 2; bytes coordinator_public_key = 3; } message CreateInstantStaticDepositUtxoSwapResponse { string swap_id = 1; } message SaveUtxoForInstantStaticDepositRequest { spark.UTXO on_chain_utxo = 1; string utxo_swap_id = 2; bytes signature = 3; bytes coordinator_public_key = 4; string transfer_id = 5; } message SaveUtxoForInstantStaticDepositResponse {} message InitiateStaticDepositUtxoSwapRequest { spark.UTXO on_chain_utxo = 1; // SSP signature of a message consenting to transfer a specific amount of funds // in exchange for a specific UTXO. bytes ssp_signature = 2; // User signature of a message consenting to exchange a specific UTXO for a fund // transfer from a specific SSP. bytes user_signature = 3; // Transfer package to send from SSP to User. spark.StartTransferRequest transfer = 4; // Transaction constructed by the SSP spending the UTXO to an address it owns. spark.SigningJob spend_tx_signing_job = 5; // Optional: which hash variant was used to create user_signature. spark.HashVariant hash_variant = 6; // Optional: minimum confirmations required. Defaults to 3 if unset/0. Min 1. optional uint32 confirmation_threshold = 7; } message CreateStaticDepositUtxoSwapRequest { InitiateStaticDepositUtxoSwapRequest request = 1; bytes signature = 2; bytes coordinator_public_key = 3; } message CreateStaticDepositUtxoSwapResponse { string UtxoDepositAddress = 1; } message CreateStaticDepositUtxoRefundRequest { spark.InitiateStaticDepositUtxoRefundRequest request = 1; bytes signature = 2; bytes coordinator_public_key = 3; } message CreateStaticDepositUtxoRefundResponse { string UtxoDepositAddress = 1; } message RollbackUtxoSwapRequest { spark.UTXO on_chain_utxo = 1; bytes signature = 2; bytes coordinator_public_key = 3; // Optional: the confirmation threshold the swap was originally created with. // When set, the receiving operator re-verifies the UTXO at this threshold // instead of falling back to the network default. Defaults to nil for // legacy callers; receiving handler falls back to existing behavior. optional uint32 confirmation_threshold = 4; } message RollbackUtxoSwapResponse { } message RollbackInstantUtxoSwapRequest { spark.UTXO on_chain_utxo = 1; bytes signature = 2; bytes coordinator_public_key = 3; repeated spark.UtxoSwapStatus rollback_from_statuses = 4 [(validate.rules).repeated = { min_items: 1, items: {enum: {const: 1}} }]; spark.UtxoSwapStatus rollback_to_status = 5 [(validate.rules).enum = {const: 3}]; } message RollbackInstantUtxoSwapResponse { } message UtxoSwapCompletedRequest { spark.UTXO on_chain_utxo = 1; bytes signature = 2; bytes coordinator_public_key = 3; // Optional: the confirmation threshold the swap was originally created with. // When set, the receiving operator re-verifies the UTXO at this threshold // instead of falling back to the network default. Defaults to nil for // legacy callers; receiving handler falls back to existing behavior. optional uint32 confirmation_threshold = 4; } message UtxoSwapCompletedResponse { } message LinkUtxoSwapTransferRequest { string transfer_id = 1; bytes signature = 2; bytes coordinator_public_key = 3; } message LinkUtxoSwapTransferResponse {} message QueryLeafSigningPubkeysRequest { repeated string leaf_ids = 1; } message QueryLeafSigningPubkeysResponse { map signing_pubkeys = 1; } message ResolveLeafInvestigationRequest { repeated string lost_leaf_ids = 1; repeated string available_leaf_ids = 2; } // ProvidePreimagePrepareRequest is the Prepare payload for the // CONSENSUS_OPERATION_TYPE_PROVIDE_PREIMAGE 2PC flow. Wraps the public // ProvidePreimageRequest plus the coordinator-derived key_tweak_proofs map // so every participant can independently validate the preimage + proofs // and write its preimage_request row in one Prepare pass. message ProvidePreimagePrepareRequest { spark.ProvidePreimageRequest original_request = 1; // Key tweak proofs derived by the coordinator from its TransferLeaf rows. // Participants cross-check against their own stored tweaks via // validateKeyTweakProofs. map key_tweak_proofs = 2; } // ProvidePreimageCommitRequest is the Commit payload for the // CONSENSUS_OPERATION_TYPE_PROVIDE_PREIMAGE 2PC flow. Carries the transfer id // + key tweak proofs so each participant can re-validate proofs and apply // the sender key tweaks via CommitSenderKeyTweaks during Commit. message ProvidePreimageCommitRequest { string transfer_id = 1; map key_tweak_proofs = 2; } // ProvidePreimageRollbackRequest is the Rollback payload for the // CONSENSUS_OPERATION_TYPE_PROVIDE_PREIMAGE 2PC flow. Rollback is a no-op // for this flow (preimage storage is intentionally irreversible; sender // key tweaks only apply in Commit). The message carries transfer_id only // for logging / future use. message ProvidePreimageRollbackRequest { string transfer_id = 1; } // InitiatePreimageSwapPrepareRequest is the Prepare payload for the // CONSENSUS_OPERATION_TYPE_INITIATE_PREIMAGE_SWAP 2PC flow. It wraps the public // InitiatePreimageSwapRequest verbatim. Every SO runs the full preimage-swap // validation (ValidateGetPreimageRequest, ValidateTransferPackage, // buildHTLCRefundMaps), persists the transfer + preimage_request + // user_signed_transaction rows, and — for REASON_SEND with a transfer package — // produces FROST round-2 signature shares over the HTLC refund txs. The // expiry-override / receive-no-expiry mutation is applied by the coordinator // onto original_request before fan-out, so every SO agrees on the same expiry. // Field name mirrors the sibling 2PC PrepareRequest messages // (SendTransferPrepareRequest, ProvidePreimagePrepareRequest, CoopExitPrepareRequest). message InitiatePreimageSwapPrepareRequest { spark.InitiatePreimageSwapRequest original_request = 1; } // InitiatePreimageSwapPrepareResponse is each SO's Prepare result. The two // fields are populated on disjoint flow paths: frost_response carries this SO's // FROST round-2 signature shares over the HTLC refund txs (REASON_SEND with a // transfer package; nil when this SO is outside the signing set or on a receive // flow), while preimage_share carries this SO's share of the payment preimage // (non-HODL REASON_RECEIVE only, where the coordinator recovers the preimage // from a threshold of shares in BuildCommitPayload). message InitiatePreimageSwapPrepareResponse { FrostRound2Response frost_response = 1; bytes preimage_share = 2; } // InitiatePreimageSwapCommitRequest is the 2PC commit payload the coordinator // builds in BuildCommitPayload. Field population is mutually exclusive by flow: // leaf_signatures for REASON_SEND (aggregated HTLC refund signatures every SO // applies via UpdateTransferLeavesSignatures); preimage + key_tweak_proofs for // the non-HODL REASON_RECEIVE path (the recovered preimage to persist plus the // proofs used to settle sender key tweaks). All empty for HODL receive / // send-without-package, where the state written in Prepare is already terminal // for this flow. message InitiatePreimageSwapCommitRequest { string transfer_id = 1; repeated SendTransferLeafSignatures leaf_signatures = 2; bytes preimage = 3; map key_tweak_proofs = 4; } // InitiatePreimageSwapRollbackRequest is the 2PC rollback payload. Each SO // cancels the transfer it wrote in Prepare (transfer -> RETURNED, leaves // unlocked) via executeCancelTransfer. Idempotent: a never-created transfer or // an already-terminal one is a no-op. message InitiatePreimageSwapRollbackRequest { string transfer_id = 1; } // StaticDepositUtxoRefundPrepareRequest is the Prepare payload for the // CONSENSUS_OPERATION_TYPE_STATIC_DEPOSIT_UTXO_REFUND 2PC flow. Wraps the public // InitiateStaticDepositUtxoRefundRequest plus the FROST round-1 commitments the // coordinator collected (via GetSigningCommitments) before invoking the engine, // keyed by operator identifier. Every SO validates the request and creates the // UtxoSwap (CREATED) row; SOs whose identifier appears in signing_commitments // also produce their round-2 share over the refund-tx sighash. Collecting // round-1 on the coordinator keeps the public RPC a single call (the client // supplies only its own nonce, as today). // // The coordinator identity recorded on each SO's UtxoSwap row // (coordinator_identity_public_key) is deliberately NOT carried here, so it can // never be a self-declared payload value. Each SO instead derives it from the // engine's coordinator_index (ConsensusPrepareRequest), resolved against the // static operator config, so the stored key is always a real signing // operator's. message StaticDepositUtxoRefundPrepareRequest { spark.InitiateStaticDepositUtxoRefundRequest original_request = 1; map signing_commitments = 2; } // StaticDepositUtxoRefundCommitRequest is the 2PC commit payload. Each SO marks // its UtxoSwap for the utxo COMPLETED (the signing result is coordinator-local, // matching legacy). on_chain_utxo locates the swap row. message StaticDepositUtxoRefundCommitRequest { spark.UTXO on_chain_utxo = 1; } // StaticDepositUtxoRefundRollbackRequest is the 2PC rollback payload. Each SO // cancels (CancelUtxoSwap -> CANCELLED) the swap it created in Prepare; REFUND // swaps skip the SP-3261 transfer-sent guard. Idempotent: NotFound / already // terminal is a no-op. // // on_chain_utxo alone does not discriminate between attempts: once a cancelled // swap frees the (utxo, status != CANCELLED) unique slot, a retry can create a // new swap for the same UTXO, and a redelivered rollback from the earlier // attempt would otherwise cancel the newer swap. Redelivery safety therefore // comes from the engine, not this payload: consensus commit/rollback dispatch // is fenced on the participant FlowExecution row (keyed by the gossip // envelope's flow_execution_id) and skips the handler once that row is // terminal. Handler effects and the row's terminal transition commit in the // same request transaction, so a terminal row proves this flow's effect was // already applied. message StaticDepositUtxoRefundRollbackRequest { spark.UTXO on_chain_utxo = 1; } // StaticDepositUtxoSwapPrepareRequest is the prepare op for the // CONSENSUS_OPERATION_TYPE_STATIC_DEPOSIT_UTXO_SWAP 2PC flow (the fixed-amount // static deposit claim). Wraps the internal-mirror // InitiateStaticDepositUtxoSwapRequest plus the FROST round-1 commitments the // coordinator collected (via GetSigningCommitments) for the SPEND TX before // invoking the engine, keyed by operator identifier. Every SO validates the // request, creates the UtxoSwap (CREATED) row, and creates the // Transfer/TransferLeaf rows (SenderKeyTweakPending) by delegating to the // send-transfer prepare; SOs whose identifier appears in a signing set also // produce round-2 shares (leaf refund txs and/or the spend tx). Collecting // round-1 on the coordinator keeps the public RPC a single call. // // The coordinator identity recorded on each SO's UtxoSwap row // (coordinator_identity_public_key) is deliberately NOT carried here, so it can // never be a self-declared payload value. Each SO instead derives it from the // engine's coordinator_index (ConsensusPrepareRequest), resolved against the // static operator config, so the stored key is always a real signing // operator's. message StaticDepositUtxoSwapPrepareRequest { InitiateStaticDepositUtxoSwapRequest original_request = 1; map spend_tx_signing_commitments = 2; } // StaticDepositUtxoSwapCommitRequest is the 2PC commit payload: the embedded // send-transfer commit (aggregated leaf refund signatures + transfer id) plus // the utxo locating the swap row. Participants apply the transfer commit FIRST // (transfer -> SENDER_KEY_TWEAKED), then complete the swap — CompleteUtxoSwap // on a non-refund swap requires the associated transfer to be sent, so this // ordering is load-bearing, not stylistic. message StaticDepositUtxoSwapCommitRequest { spark.UTXO on_chain_utxo = 1; SendTransferCommitRequest transfer_commit = 2; } // StaticDepositUtxoSwapRollbackRequest is the 2PC rollback payload. Each SO // locates its own UtxoSwap row by on_chain_utxo, rolls back the transfer that // row names FIRST (rollbackSendTransfer -> RETURNED), then cancels the swap // (CancelUtxoSwap -> CANCELLED); this ordering satisfies the SP-3261 guard, // which refuses to cancel a swap whose transfer is sent and fails closed when // the transfer state is unreadable. // // The transfer id is deliberately NOT carried in this payload: each // participant derives it from its own swap row (requested_transfer_id, written // by its own Prepare), so a buggy or compromised coordinator cannot name an // unrelated transfer and have it rolled back. Prepare creates the swap row // strictly before the transfer, so no active swap row means no transfer to // roll back. Idempotent: a missing/terminal swap is a no-op. // // on_chain_utxo alone does not discriminate between attempts: once a // cancelled swap frees the (utxo, status != CANCELLED) unique slot, a retry // can create a new swap for the same UTXO, and a redelivered rollback from the // earlier attempt would otherwise cancel the newer swap. Redelivery safety // comes from the engine, not this payload: consensus commit/rollback dispatch // is fenced on the participant FlowExecution row and skips the handler once // that row is terminal. message StaticDepositUtxoSwapRollbackRequest { spark.UTXO on_chain_utxo = 1; } message ReserveEntityDkgKeyRequest { string keyshare_id = 1; } message FixKeyshareRequest { string bad_keyshare_id = 1; string bad_operator_id = 2; repeated string good_operator_ids = 3; } message FixKeyshareRound1Request { string bad_keyshare_id = 1; string bad_operator_id = 2; repeated string good_operator_ids = 3; } message FixKeyshareRound1Response { repeated bytes message = 1; } message FixKeyshareRound2Request { string bad_keyshare_id = 1; string bad_operator_id = 2; repeated string good_operator_ids = 3; repeated bytes message = 4; } message FixKeyshareRound2Response { bytes message = 1; } message GetTransfersRequest { repeated string transfer_ids = 1; } message GetTransfersResponse { repeated spark.Transfer transfers = 1; } message GenerateStaticDepositAddressProofsRequest { string keyshare_id = 1; string address = 2; bytes owner_identity_public_key = 3; } message GenerateStaticDepositAddressProofsResponse { bytes address_signature = 1; } message SyncNodeRequest { repeated string node_ids = 1; string operator_id = 2; } // DepositTreePrepareRequest is the 2PC prepare payload for consensus deposit tree finalization. // Each SO independently validates the deposit address, constructs signing jobs, // and produces FROST signature shares. message DepositTreePrepareRequest { spark.FinalizeDepositTreeCreationRequest original_request = 1; } // StorePreimageSharePrepareRequest is the 2PC prepare payload for consensus // preimage share storage. Each SO validates, decrypts, and writes its share // during Prepare. Commit and Rollback are no-ops. message StorePreimageSharePrepareRequest { spark.StorePreimageShareV2Request original_request = 1; } // SendTransferPrepareRequest is the 2PC prepare payload for consensus // send-transfer (v3). Each SO validates the transfer package, decrypts its // slice of the key tweak package, persists the transfer + transfer_leaf rows // in a locked-but-not-yet-tweaked state, and produces FROST signature shares // over the refund transactions. message SendTransferPrepareRequest { spark.StartTransferV3Request original_request = 1; // Optional spark invoice this transfer pays. Carried out-of-band from // original_request because the public StartTransferV3Request has no // invoice field; it is set only when a StartTransferV2 request routed // through the consensus engine carried a spark_invoice. Every SO // validates it in Prepare and persists it on the transfer. string spark_invoice = 2; } // SendTransferCommitRequest is the 2PC commit payload built by the coordinator // in BuildCommitPayload. It carries the aggregated refund signatures per leaf // so every SO can apply identical state during Commit without re-aggregating. message SendTransferCommitRequest { string transfer_id = 1; repeated SendTransferLeafSignatures leaf_signatures = 2; } // SendTransferLeafSignatures carries the aggregated FROST signatures for a // single leaf's refund tx variants. message SendTransferLeafSignatures { string leaf_id = 1; bytes refund_signature = 2; bytes direct_refund_signature = 3; bytes direct_from_cpfp_refund_signature = 4; } // SendTransferRollbackRequest is the 2PC rollback payload. SOs delete the // transfer + transfer_leaf rows they wrote in Prepare and unlock any leaves // they locked. message SendTransferRollbackRequest { string transfer_id = 1; } // ClaimTransferPrepareRequest is the 2PC prepare payload for consensus // claim-transfer. Each SO validates the claim package, validates and decrypts // its slice of the key tweak package, persists the receiver-side state in a // locked-but-not-yet-applied form, and produces FROST signature shares over // the receiver's refund transactions (cpfp/direct/direct-from-cpfp variants). message ClaimTransferPrepareRequest { spark.ClaimTransferRequest original_request = 1; } // ClaimTransferCommitRequest is the 2PC commit payload built by the coordinator // in BuildCommitPayload. It carries the aggregated refund signatures per leaf // so every SO can apply identical state during Commit (key tweaks + finalized // refund transactions) without re-aggregating. // // receiver_identity_public_key carries the claimer's identity (the // OwnerIdentityPublicKey from the original ClaimTransferRequest). Required for // MIMO transfers where transferEnt.ReceiverIdentityPubkey may point to a // different receiver than the one that initiated the claim — participants must // load the same receiver row the coordinator did. For non-MIMO transfers this // is redundant with transferEnt.ReceiverIdentityPubkey but harmless. message ClaimTransferCommitRequest { string transfer_id = 1; repeated ClaimTransferLeafSignatures leaf_signatures = 2; bytes receiver_identity_public_key = 3; } // ClaimTransferLeafSignatures carries the aggregated FROST signatures for a // single leaf's receiver-side refund tx variants. message ClaimTransferLeafSignatures { string leaf_id = 1; bytes refund_signature = 2; bytes direct_refund_signature = 3; bytes direct_from_cpfp_refund_signature = 4; } // ClaimTransferRollbackRequest is the 2PC rollback payload. SOs revert any // receiver-side state they wrote in Prepare (key tweak proofs, locked // transfer/receiver status) so the transfer remains claimable on retry. // // receiver_identity_public_key carries the claimer's identity. Required for // MIMO transfers where transferEnt.ReceiverIdentityPubkey may point to a // different receiver than the one that initiated the claim — without this, // rollback would revert the wrong receiver's row and leave the actual claimer // permanently stuck. For non-MIMO transfers this is redundant with // transferEnt.ReceiverIdentityPubkey but harmless. message ClaimTransferRollbackRequest { string transfer_id = 1; bytes receiver_identity_public_key = 2; } // CoopExitPrepareRequest is the 2PC prepare payload for consensus cooperative // exit. Each SO validates the exit_txid<->connector_tx binding and the transfer // package, decrypts its slice of the key tweak package, persists the transfer + // transfer_leaf + cooperative_exit rows at SENDER_INITIATED (refund txs stored // unsigned, key tweaks stored but not applied — a pre-commit state the chain // watcher leaves untouched), and produces FROST signature shares over the // connector-spending refund transactions. message CoopExitPrepareRequest { spark.CooperativeExitRequest original_request = 1; } // CoopExitCommitRequest is the 2PC commit payload built by the coordinator in // BuildCommitPayload. It carries the aggregated refund signatures per leaf so // every SO applies identical state during Commit. connector_tx is replicated // here because it is request-only (no ent column persists it) yet every SO's // Commit needs the connector prevouts to verify the multi-input refund // signatures. Reuses SendTransferLeafSignatures — coop exit aggregates the same // three refund variants. Commit applies the signatures and promotes the // transfer to SENDER_KEY_TWEAK_PENDING; key tweaks remain stored for the chain // watcher to apply after on-chain confirmation. message CoopExitCommitRequest { string transfer_id = 1; repeated SendTransferLeafSignatures leaf_signatures = 2; bytes connector_tx = 3; } // CoopExitRollbackRequest is the 2PC rollback payload. SOs mark the transfer // RETURNED, unlock the leaves, and delete the cooperative_exit row so the chain // watcher no longer considers this exit pending. message CoopExitRollbackRequest { string transfer_id = 1; } message ConsensusPrepareRequest { // Corresponds to gossip.ConsensusOperationType. Defined as int32 to avoid // a circular proto import (gossip.proto already imports spark_internal.proto). int32 op_type = 1; // Domain-specific proto message passed to FlowHandler.Prepare. google.protobuf.Any operation = 2; // UUID (as string) of the coordinator's FlowExecution row for this 2PC // execution. Participants use this as the id of their own FlowExecution // row (each SO has an independent database, so the same UUID is safely // reused across roles). Empty means the caller is a pre-upgrade // coordinator; participants skip writing a FlowExecution row. string flow_execution_id = 3; // SigningOperator.ID of the coordinator. Participants store this on their // FlowExecution row as coordinator_index so the reconciliation task knows // which operator to query via ConsensusQueryOutcome. The authz // interceptor already IP-restricts callers to operator addresses, so the // worst a misbehaving caller can do is set a wrong index — at which point // reconciliation queries the wrong operator, which has no record and // responds OUTCOME_UNSPECIFIED. uint32 coordinator_index = 4; } message ConsensusPrepareResponse { // Domain-specific result from FlowHandler.Prepare (e.g., signature shares). google.protobuf.Any result = 1; } message ConsensusQueryOutcomeRequest { // UUID (as string) of the FlowExecution row to query. Same value the // coordinator originally sent in ConsensusPrepareRequest.flow_execution_id. string flow_execution_id = 1; } message ConsensusQueryOutcomeResponse { enum Outcome { // Coordinator has no record of this flow_execution_id. // Reconciliation callers treat this as data loss requiring // operator attention; it is not a normal-operation outcome // because the coordinator writes the row before fan-out and // retains it through the terminal transition. OUTCOME_UNSPECIFIED = 0; OUTCOME_IN_FLIGHT = 1; OUTCOME_COMMITTED = 2; OUTCOME_ROLLED_BACK = 3; } Outcome outcome = 1; // Mirrors gossip.ConsensusOperationType on the matched row. int32 op_type = 2; // Populated when outcome is COMMITTED or ROLLED_BACK. Domain-specific // commit/rollback proto (the same bytes the coordinator sends as the // ConsensusCommit / ConsensusRollback gossip payload). google.protobuf.Any decision_payload = 3; }