I’ve spoken to Pat out-of-band and insertion of the PubKey is already supported in the bch_signTransaction
API (obvious but didn’t occur to me at the time, use-case was a bit different - see Supporting Script Population below), so this might be unnecessary (and, as Pat explained, some might view it as contentious?) There are a use-cases where it is required, but we might be able to handle these use-cases more elegantly. Will try to outline these (along with some other feedback/considerations) below.
NOTE: Some ideas below might be bad/unworkable. For those that sound acceptable, I’m happy to try to do the work to PoC these in Pat’s Cashonize Wallet fork.
Background on the Placeholder Approach
The current spec is very simple in that it supports using zero-byte placeholders within encoded transactions to populate inputs (pubKey, signature). This allows it to work with native transaction encoding formats, which (should) maximize compatibility across existing BCH libraries and wallets. Essentially, forEach input:
- Replace 65 zero-byte OP_PUSH with signature.
- Replace 33 zero-byte OP_PUSH with pubKey.
This allows us to work with both LibAuth and CashScript (and other libs and wallets) easily as we are using BCH’s Native Transaction Primitives.
If the following is viable and we retain this placeholder approach, I don’t think it is worth supporting LibAuth’s TransactionBCH
payload in spec and are better off just using the Encoded Transaction Primitive. This will make WC compatibility much simpler/easier for non-JS wallets to implement - e.g. Electrum Cash, @tom’s Flowee Pay, Bitcoin.com, etc). From a LibAuth point of view, it is only an additional encodeTransaction(transactionBCH)
to get it into this form too. More broadly, the more that we can retain use of standard primitives, the easier it will be for Wallets to integrate.
Placeholder Limitations and Potential Solutions
In terms of the placeholder approach, the big limitation I’ve noted with use-cases I’m working with is that it does not (currently) allow for parameters (or flags) to be specified. For example, it does not currently support a Wallet signing a dataSig and nor does it support partially signed transactions (I do not actually have a use-case right now for this, but suspect there will be some).
We might be able to keep the current placeholder approach and still support these by (perhaps) doing something like the following:
Supporting DataSig
NOT VIABLE: See Wallet Connect V2 support for BitcoinCash - #5 by bitcoincashautist
DataSig’s are 64 bytes long. We could therefore look for an OP_PUSH 64 bytes with 32 leading zero-bytes. The remaining 32 bytes could then represent the SHA256 (32 byte) message hash. I’m a mathlet (so verification of my figures here would be good), but I think the chance of an organic DataSig leading with 32 zero bytes would be: 1 / (2 ^ 256) = 1 / 1.157920892373162e77
.
I’m unsure whether ending up with an organic leading 32 zero byte dataSig is feasible in practice, if someone could chime in here, that would be most helpful.
Support Partially Signed Transactions (sigHash flags)
Signatures are 65 bytes long. We could therefore look for an OP_PUSH 65 bytes with 64 leading zero-bytes. The remaining 1 bytes could then represent the sigHash flags for signing ( Bitcoin Cash Protocol Documentation ). I think the chance of an organic signature leading with 64 zero bytes would be: 1 / (2 ^ 512) = 1 / 1.340780792994260e154
.
(Again, I’m a Mathlet, so would appreciate someone chiming in on feasibility of actually organically producing a 64 zero-byte leading signature. If the 32 zero-byte above is safe, then this certainly should be too?)
Supporting Script Population
There are situations where we may want to build lockscripts without actually signing a transaction - but these might have a User’s PubKey (or a DataSig) as a pre-requisite. Imagine the following CashScript contract:
contract Contract (
// ...
pubkey userPubKey,
datasig userSignedMessage,
) { ... }
Currently (using Cashonize), there is a workaround in that we can pass in a full transaction with the desired Locking Script as an Unlocking Script input (that we never broadcast and has spoofed parameters) to bch_signTransaction
and then extract the resulting populated Unlocking Script from the returned SignedTransaction. This feels a bit dirty though - and it’s probably a bit incidental (implementation-specific) that it works.
We might want another interface for this sort of case (e.g. bch_signScript
- but I don’t know if this name necessarily reflects what it always does?).
Batching Requests
We may want to combine the bch_signTransaction
into a bch_batch
interface that supports both transactions and populating bytecode with the necessary parameters.
@mainnet_pat has already suggested this (as bch_batchSignTransaction
), but it might be advantageous if we allow this to batch any method? E.g.
[
{
method: "bch_signScript",
payload: { ... }
},
{
method: "bch_signTransaction",
payload: { ... }
}
]
From a Wallet UX point-of-view, these can be presented/approved one-at-a-time. IMO, the real benefit of this is that it can on some painful UX on Mobile Devices whereby many mobile devices/browers will kill the WebSocket (and WalletConnect) connection when the tab is non-visible. Without it, (I think?) the user would have to flick back-and-forth between App Tab and Wallet for each step required (so that the app can re-instantiate WC connection and send the next request). It might also enhance security in that a user approves a batch of actions before responding to wallet - if any are declined, the entire batch is disregarded, preserving privacy (you either get all or none).
An additional benefit (for a future iteration of the protocol) is that we might also be able to instantiate a Wallet Connect backend flow that performs a single set of batch operations and then immediately disconnects (@sahid.miller called out a valid concern here where a backend session can persist indefinitely until the wallet explicitly terminates it: Telegram: Contact @bch_wc2 ).
There might be an elegant way to “chain” the results of this batch call so that the result of the previous operations are available to those that follow, but this might be over-ambitious for now.
Contract in sourceOutputs
Apologies, I haven’t looked much at the contract portion of this, but it looks like this might just be to provide extra “humanizing” context to the Wallet UI for approval of a bch_signTransaction
?
If that’s the case, I think the approach that Pat’s suggested here might work better:
... [we can lookup] contract information by looking up a redeem bytecode (stripped of contract function and constructor arguments) from a specialized contract registry akin to 4byte.directory.
If it’s only use is humanizing some of the contract parameters, leaving this out of spec (or making-optional) and having wallets look up the artifact from unlocking bytecode as a “best practice” would probably simplify many implementations. (Perhaps this could work similar to BCMR?)
locking and unlocking bytecodes can be obtained from Fulcrum and contract information by looking up a redeem bytecode
I’m inbetween on this. Personally, just to simplify wallet implementations (and shift the heavy juggling away from the wallet and to the app), I think it’s better if the App provides these to the wallet. Otherwise, I think we’ll find weird bugs in wallets that might make assumptions (e.g. all inputs are P2PKH) that kill compatibility? (i.e. an app’s WC flow works on one wallet, but mysteriously fails on another - BIP70/JPP implementations were like this).
Not really sure if what I’ve got above is feasible in practice, just spit-balling on how we might be able to simplify this part of the spec for wallets.
Final Thoughts
I don’t think we should (or need to) aim for perfection with this first iteration, but should strive to make it versatile just so that interesting use-cases can be realized (and demonstrated). We can work on improving the protocol/interfaces for better UX once we’ve got a more concrete idea of the many use-cases that are available and common contract patterns (imo, we’re still discovering these). As a stop-gap between now and then, I think what Pat’s built (with a few amendments) would work very well.
In general, from my point of view, I think that we should:
- Aim to keep the spec/implementation simple and try to use existing BCH primitives. This will allow easier integration for other wallets (working with BIP70/JPP, this has been a nightmare). It also minimizes technical debt for when the next iteration is built and we still want to retain support for this version.
- Try to make it so that we can support the placeholders that many contracts will likely require in the long-run (dataSig, sigHash flags).
- Support basic batching. This one isn’t really that important imo and I’m not sure if it will really help UX that much. But, if it can be done simply, it might be worth it (though many contracts will probably still require multiple batched steps? E.g. mint tokens → construct contracts). I mention it above more-so because I think the idea of being able to batch any method gives us the most flexibility (as opposed to specific methods for specific batch ops).