Description
First off: SPV is worse security than fullnodes. Please use fullnodes if you can.
On the other hand, if you trust a particular cloud service provider, you can run a fullnode on a cloud service, and thus not spend a lot of bandwidth in your own local connection. You might then think to run lightningd
locally and point the --bitcoin
parameters at the cloud service fullnode, but this actually downloads every block in full and gets you as much bandwidth use as a locally-run bitcoind
with blocksonly
option. You could instead run an SPV server of some kind on your remote cloud service fullnode, and as long as the cloud service does not secretly replace your bitcoind
with minercoin2xd-unlimitedvision
, you should be fine. Then we would want support for SPV in a locally-run lightningd
to limit the bandwidth needed, and point it only at your trusted cloud service fullnode.
Here is my current plan:
When lightningd/bitcoind
object is initialized, it checks for the following two sets of APIs:
getrawblockbyheight
,getutxout
,estimatefees
,getchaininfo
,sendrawtransaction
- current 0.9.0 set.gettxesbyheight
,getutxobyscid
,estimatefees
,getchaininfo
,sendpsbttomempool
,checkspent
- next version of the backend plugin API.
gettxesbyheight
is given a height
to fetch, plus a receive_scriptpubkeys
an array of hex string scriptPubKey
s to check, and a spend_utxos
an array of objects with txid
, vout
, and scriptPubKey
fields. It returns a field found
as false
if not yet found, or if the block is found it returns found
field as true
and in addition returns the blockid
the hash of the block header, header
the hex string dump of the header, and txes
an array of hex string transactions in the block that match the given receive_scriptpubkeys
or spend_utxos
.
getutxobyscid
is given an scid
of BOLT-standard form BBBxTTxOO
. It returns a field found
, which is false
if the given SCID does not match an unspent SegWit v0 32-byte witness (P2WSH), or a SegWit v1 32-byte witness (Taproot). If true
it returns the scriptPubKey
of the output as well as the amount
.
sendpsbttomempool
is given a Base64 PSBT that contains a completely signed transaction, and a allowhighfees
parameter, and broadcasts it. We have to switch bitcoind_sendrawtx
to use struct bitcoin_tx
which conveniently contains a PSBT.
checkspent
is given an array of objects. Each object represents a txo with at least txid
and vout
and scriptPubKey
fields, and optional blockheight
, which is the known confirmation height of the txo. For each txo, it checks if the txo is spent, and if so, adds it to the spent
result, which is an array of objects with txid
, vout
, and spendheight
fields, with 'spendheight': 0
for TXOs that are spent in unconfirmed transactions.
Internally, we also have these changes to lightningd/bitcoind
functions:
bitcoind_can_getutxobyscid
. This accepts aconst struct bitcoind *
and returnstrue
if the bitcoin backend implementsgetutxobyscid
.- If it returns true, it is safe to call a new
bitcoind_getutxobyscid
andbitcoind_checkspent
, but not safe to callbitcoind_getfilteredblock
orbitcoind_getutxout
. - If it returns
false
, it is not safe to callbitcoind_getutxobyscid
orbitcoind_checkspent
but it is safe to callbitcoind_getfilteredblock
andbitcoind_getutxout
.
- If it returns true, it is safe to call a new
bitcoind_gettxesbyheight
. This is always callable.chaintopology
should use this API instead ofbitcoind_getrawblockbyheight
, since the latter is removed.- The only change in interface relative to
bitcoind_getrawblockheight
is thatbitcoind_gettxesbyheight
requires filter arguments. It still returns astruct block
albeit one with a possibly-incomplete set oftx
. - If the backend plugin is using the 0.9.0 interface this just calls into
getrawblockbyheight
and ignores the filter arguments.
- The only change in interface relative to
- As mentioned,
bitcoind_sendrawtx
should acceptstruct bitcoin_tx
instead ofconst char *hextx
.
Finally, we also add a new lightningd
command, checknewblock
, which triggers chaintopology
into checking for a new block. The backend plugin should call this when the block tip height changes. Normal bitcoin-cli
and BIP158 plugins will poll, though a BIP158 plugin could have a peer push a new block at it (not likely unless the peer is a miner). An Electrum plugin would subscribe to blockchain.headers.subscribe
. For now we also do polling ourselves in lightningd
to support older plugins that do not perform this call.
After we change lightningd
, we can then update bcli
to use the new interface. In order to check the new interface, gettxesbyheight
will also apply filtering to transactions in the block, and will not provide txes that do not match the given filters. getutxobyscid
could have a simple on-disk cache: a mapping of SCID to either a tuple of txid
and vout
, or Nothing. If an SCID exists in the on-disk cache, and it maps to nothing, it is known-invalid SCID, else if it maps to a txid
vout
the bcli
will go query bitcoin-cli getutxout
to see if it is still unspent. If the SCID does not exist in the on-disk cache, then we scan the block and update the cache, and we also iterate over all possible SCIDs (e.g. even absurd ones like BBBx16777215x65535) and map those that do not match a P2WSH or Taproot output to Nothing.
Original text:
First off: SPV is worse security than fullnodes. Please use fullnodes if you can.
Currently, the interface to bcli
inherently assumes the backend is a fullnode of some kind. This fullnode is always fully trusted to not lie, and we always download every block from the backend.
When doing block synchronization, what we can do is to have a txfilter
containing:
- UTXOs which, if spent, we want to know about.
- Addresses which, if spent from or received into, we want to know about.
Now, it is important that we keep track of UTXOs that we want to track spendedness of channels. However, each such UTXO has its own address as well.
Most SPV protocols (eg BIP158, Electrum) are address-centric. So, we should provide just addresses to the backend. The backend should inform us
So I propose for the bcli
getrawblockbyheight
:
- Add a new required parameter,
txfilter
, which is an array ofscriptPubKey
hexdumps. - It may return a result without a
block
field (but must still returnblockhash
). This is used to informlightningd
that the block at the given height does not involve any of the addresses in thetxfilter
. - It has to return a separate
blockheader
field regardless. This is needed so that chaintopology can recognize reorgs.
Then:
- For Electrum, we do
blockchain.scripthash.get_history
on every listedscriptPubKey
, and if any of them has a transaction whose confirmedheight
equals the height being queried, then we should download that block (elsewhere) and provide it to thelightningd
.- But we should be wary of reorgs. Thus, we should do a
blockchain.block.header
and save the header, thenblockchain.scripthash.get_history
for a single scriptpubkey, then ablockchain.block.header
and compare if it is the same as previous; if not the same, we should restart the querying again, since the alternate block might now contain the previousscriptpubkey
. This is not resilient against very fast ABA problems but hopefully those are rare.
- But we should be wary of reorgs. Thus, we should do a
- For BIP158, the plugin maintains a header chain, and we download the version
0x00
Neutrino filter for the block header hash at the requested height. Then we check if anything in ourtxfilter
matches the Neutrino filter.
Ideally, our fullnode-trusting bcli
should also apply the txfilter
to blocks it returns. This is to ensure that any bugs in maintaining the lightningd
-side txfilter
can be caught by our test suite without having to add SPV plugins of our own.
Unfortunately, to determine if an address is spent from, we would require a txindex
, or have the plugin maintain its own transaction index. Both are obviously undesirable.
However, in practice:
- We only care about addresses we receive from.
- We only care about UTXOs that somebody spends from.
So let me propose instead passing in two arguments to getrawblockbyheight
of bcli
:
txfilter_receive_addr
, an array ofscriptpubkey
s, which, if any of them receive funds in the given block, we should return the block.txfilter_spend_utxo
, an array of{'txid': 'xx', outnum: 42, scriptpubkey: 'xx'}
, which, if any of the given UTXOs are spent, we should return the block.
Then:
- The default fullnode-trusting
bcli
only needs to scan the requested block and every transaction:- If a transaction input has a
prevout
matching atxfilter_spend_utxo
entry, return the block. - If a transaction output has a
scriptPubKey
matching atxfilter_receive_addr
entry, return the block. - Otherwise do not return the block, just its header.
- If a transaction input has a
- The SPV interfaces, which are all address-based, just get the
scriptpubkey
from thetxfilter_spend_utxo
and append them to thetxfilter_receive_addr
.
This allows us to check in our test suite that lightningd
is maintaining its txfilter
correctly, using only the bcli
plugin, while allowing address-based SPV interfaces to be used.