Shared-spending Inter-Protocol Framework
A high-quality general Bitcoin-management (a.k.a. “wallet”) implementation can implement the following software interface as a framework by which additional code can implement protocols that are capable of sufficient flexibility to allow arbitrarily adding new transaction inputs and new transaction outputs.
As a concrete example, any protocol that uses Private Key Handover as described in the main text above can integrate with the below-described inter-protocol framework in order to create batched, RBFed transactions.
The framework maintains a (possibly empty) set of requested commands. A “command” in this context is either (1) a request to spend an input before some timeout or (2) a request to fund some address before some timeout.
In particular, end-users can simply call fund_output one-at-a-time and the framework will, if feasible, batch the outputs into a single transaction, i.e. instead of requiring end-user code to call into a send_multi-equivalent command (so that end-user code has to decide to batch before calling into the Bitcoin-management framework), the end-user code calls multiple fund_output commands, and the framework automatically batches them, using RBF to replace any broadcasted transactions.
The framework implicitly assumes that all inputs are some SegWit version, and thus have a non-malleable txid independent of signing. The framework simply bans non-SegWit inputs outright.
The framework has two entry points: fund_output and spend_input. Both accept one or more callbacks, which the framework uses to coordinate with the actual protocols.
fund_output(amount, timeout, requesterAddressCallback, requesterReadyCallback, requesterDoneCallback)- Tell the framework that it has to make a transaction that sends out anamount, targeting the transaction to be confirmed before thetimeoutblockheight.- The
requesterAddressCallbackis called whenever the framework has decided on some number of owned UTXOs, plus any pending spend-an-input requests, and a feerate to use. It is given the feerate the framework has decided on. It should return either a destination address for the amount, or a request to cancel itself and remove its request to fund the output.- For example, consider the LN BOLT
openv1protocol, where this might be used to initiate a newopen_channel, and thus generate a new address from the repliedaccept_channel(i.e. this callback would sendopen_channeland wait for a correspondingaccept_channel). - The framework may call this at any time, as long as the request has not been cancelled, for example if it decides to RBF.
- For example, consider the LN BOLT
- The
requesterReadyCallbackis called whenever the framework has already calledrequesterAddressCallbackon allfund_outputcommands, and has created, but not signed, the transaction. It is given the stabletxidof the transaction and the output index of that transaction where it is created, as well as the corresponding address. It returns either anok, or a request to cancel the transaction and remove its request to fund the output.- For example, consider the LN BOLT
openv1protocol, where this would be used to sendfunding_createdand wait for afunding_signed(and if it times out waiting forfunding_signed, to cancel the request instead of continuing). - The framework will not sign and broadcast the transaction until after all
requesterReadyCallbacks returnok. If any do not returnok, the framework will remove that output and then restart its loop starting with callingrequesterAddressCallbackon all unremoved output requests.
- For example, consider the LN BOLT
- The
requesterDoneCallbackis called when some transaction that has funded the output has been confirmed (possibly to some depth determined by policy). This is given the specific txout and address.- For example, consider the LN BOLT
openv1protocol, where this would be used to sendfunding_locked/channel_ready.
- For example, consider the LN BOLT
- The
spend_input(txin, amount, timeout, requesterSignCallback)- Tell the framework that some specific transactiontxin(a stabletxid+outnum) must be spent before the giventimeoutblockheight.amountis given here but depending on implementation it may be possible to remove it from the interface (in principle a full UTXO-map would store theamountand spending conditions, but that is not trivially exposed by e.g. bitcoin-core).- If the framework currently has no
fund_outputrequests, the funds (minus fees) should go to an address that the framework wallet unilaterally controls. - The
requesterSignCallbackis called whenever the framework has already decided to sign and broadcast some transaction that includes this input. It is given a PSBT, and it must return a PSBT with the corresponding input having the required witness filled in.- For example, consider the LN BOLT
revocationprotocol, where the only requirement is to sign with therevocationpubkey, and the transaction can spend to anything.
- For example, consider the LN BOLT
- If the framework currently has no
For a simple “send to an address” base usecase, you can call fund_output with the amount to be sent, with trivial callbacks where requesterAddressCallback returns the target address, requesterReadyCallback trivially returns ok, and requesterDoneCallback notifies the user UI. The magic here is that if the human operator performs multiple “send to an address” commands in sequence, the wallet framework automatically batches them (it can delay its decision loop to wait for new commands to reduce the number of times it RBFs, but it can always RBF newer transactions for each new “send to an address” request). In addition, in case of a sudden surge in fees, the wallet framework can automatically RBF without further human operator input; further, the human operator can specify some maximum feerate, and then requesterAddressCallback can fail the request automatically if the feerate is higher than the maximum set by the human operator.
For the proposed “Private Key Handover”, any protocol that implements Private Key Handover proposed in the original post can call into spend_input, and provide a requesterSignCallback that uses the handed-over private key to sign the input it is claiming.
Of note is that obviously the framework needs to continue operating across restarts. Thus, the actual callbacks would not be some kind of in-memory representation of a function; instead, you would need to pre-register some set of protocols that can use this framework, with some kind of per-protocol name or identifier that is stable across restarts (for example, it can be a versioned string, such as simple_send_to_addr_v1). This registration for protocols that would call into fund_output could provide the function addresses for all the callbacks needed, so that the actual fund_output call accepts only the registered name.
The framework would store the pending requests on persistent storage, and on restart, would load and look for any pending requests, and check the stored protocol names against the pre-registered protocols (and abort the restart if the in-storage string does not match any pre-registered protocols). This make the framework a little harder to use, but makes it robust against restarts; the framework can record exactly where in its processing it has completed (e.g. if it has called some callback for some particular request). This may require that individual requests also have an identifier (such as a UUID) across restarts.