I've been planning to work on a plugin-based bitcoin backend for a while but
haven't gotten around to actually working on it. Since others are trying to
get started on this I thought I'd share some of my thoughts around how this
could be accomplished.
Current situation
Our current interactions with the bitcoin network is nicely encapsulated in
lightningd/bitcoin.c. From there we can see that the RPC methods we call are
the following:
estimatesmartfee used for fee estimation on a couple of different
horizons (1, 3, 6 blocks).
getblock (raw and hex encoded) used to retrieve a block after calling
getblockhash to get its hash given the height.
getblockhash to translate a height into a blockhash
gettxout to check whether a tx output is still unspent, i.e., in the UTXO.
getnetworkinfo used to check that bitcoind is a supported version only.
getblockchaininfo used to check if bitcoind is in IBD.
sendrawtransaction used to send a transaction to the network.
Backend Interface
I'd like to make the interface for backend a bit more coarse-grained, in order
to give the plugins more freedom as to how they accomplish their task. My go
to example is getblockhash + getblock which is always called in this order
and requires multiple roundtrips to bitcoind for a single logical operation.
Some of the methods above can be translated 1-to-1 into the plugin:
sendrawtransaction can become a simple hook, if only for the reason that
we don't want to expose it to the JSON-RPC users. In addition with
prioritized and chained hooks we can have a failover chain that tries one
plugin after the other in case of failure.
Other methods can be combined into more abstract methods:
getblockbyheight is a combination of getblockhash and getblock which
either returns the hash and the block at a given height or an error if
there is no block (yet). In addition we could give a tiny hint as to the
context in which the query is done (scan) to tell the plugin whether we
are in a linear scan to catch up with the blockchain head. That'd allow the
plugin to pre-fetch the next few blocks, effectively pipelining the sync.
getchaininfo can subsume the calls to getnetworkinfo and
getblockchaininfo because they are mostly done on startup, and
effectively contain the same information. A version number for the set of
capabilities exposed by the backend plugin would still be in order to guard
against using an outdated backend with a newer version that expects some
things to be available.
getoutputbyscid is a combination of getblockhash, getblock
(verbosity=1) and gettxout used when verifying a channel's outpoint
from its announcement. This is one of the most costly calls we have since
it threads through 3 RPC calls and is called for every channel we learn
about (it's cached later, but on new nodes this is the heavy hitter). We
now optimized this to use filtered blocks, but it's still in use by some
parts of the machinery.
getfilteredblock is an alternative to getoutputbyscid issue. It gets
the entire block, filters out transactions that are not interesting (don't
have unspent outputs or don't have P2WSH outputs) and returns them to be
processed. We would probably implement either this bulk interface or
getoutputbyscid. The latter would allow plugins to implement something
smart, but comes at the cost of more hook calls.
And finally we can make some things notifications:
fee could give a bulk update on all horizons we are interested in and
push a fee update from the plugin to lightningd. That means we could also
respond to things happening on the network that could affect fees (new
block being found), or we could implement fee smoothing at the plugin level.
block and tx notifications could alert us about incoming blocks (header
and height) or transactions we might be interested in. The latter would
mean we need to tell the plugin about addresses and outputs we are
interested in, but it'd be only an optimization anyway since we'll catch
them during the regular processing of blocks.
Hook vs JSON-RPC passthrough
Most of the above methods could be implemented either as JSON-RPC methods
(with passthrough to the user RPC interface) or as hooks. JSON-RPC methods are
nice since they allow the user to query the same backend that lightningd
uses internally (facilitating debugging), however they may end up cluttering
the RPC interface with semi-related things. Hooks on the other hand are
internal only and with the planned chaining of hooks we can have multiple
plugins with different priorities providing transparent failover (something
that with JSON-RPC is not planned right now), the downside is that the
failover isn't implemented yet and the backend is not accessible for
debugging.
I've been planning to work on a plugin-based bitcoin backend for a while but
haven't gotten around to actually working on it. Since others are trying to
get started on this I thought I'd share some of my thoughts around how this
could be accomplished.
Current situation
Our current interactions with the bitcoin network is nicely encapsulated in
lightningd/bitcoin.c. From there we can see that the RPC methods we call arethe following:
estimatesmartfeeused for fee estimation on a couple of differenthorizons (1, 3, 6 blocks).
getblock(raw and hex encoded) used to retrieve a block after callinggetblockhashto get its hash given the height.getblockhashto translate a height into a blockhashgettxoutto check whether a tx output is still unspent, i.e., in the UTXO.getnetworkinfoused to check thatbitcoindis a supported version only.getblockchaininfoused to check ifbitcoindis in IBD.sendrawtransactionused to send a transaction to the network.Backend Interface
I'd like to make the interface for backend a bit more coarse-grained, in order
to give the plugins more freedom as to how they accomplish their task. My go
to example is
getblockhash+getblockwhich is always called in this orderand requires multiple roundtrips to
bitcoindfor a single logical operation.Some of the methods above can be translated 1-to-1 into the plugin:
sendrawtransactioncan become a simple hook, if only for the reason thatwe don't want to expose it to the JSON-RPC users. In addition with
prioritized and chained hooks we can have a failover chain that tries one
plugin after the other in case of failure.
Other methods can be combined into more abstract methods:
getblockbyheightis a combination ofgetblockhashandgetblockwhicheither returns the hash and the block at a given height or an error if
there is no block (yet). In addition we could give a tiny hint as to the
context in which the query is done (
scan) to tell the plugin whether weare in a linear scan to catch up with the blockchain head. That'd allow the
plugin to pre-fetch the next few blocks, effectively pipelining the sync.
getchaininfocan subsume the calls togetnetworkinfoandgetblockchaininfobecause they are mostly done on startup, andeffectively contain the same information. A version number for the set of
capabilities exposed by the backend plugin would still be in order to guard
against using an outdated backend with a newer version that expects some
things to be available.
getoutputbyscidis a combination ofgetblockhash,getblock(
verbosity=1) andgettxoutused when verifying a channel's outpointfrom its announcement. This is one of the most costly calls we have since
it threads through 3 RPC calls and is called for every channel we learn
about (it's cached later, but on new nodes this is the heavy hitter). We
now optimized this to use filtered blocks, but it's still in use by some
parts of the machinery.
getfilteredblockis an alternative togetoutputbyscidissue. It getsthe entire block, filters out transactions that are not interesting (don't
have unspent outputs or don't have P2WSH outputs) and returns them to be
processed. We would probably implement either this bulk interface or
getoutputbyscid. The latter would allow plugins to implement somethingsmart, but comes at the cost of more hook calls.
And finally we can make some things notifications:
feecould give a bulk update on all horizons we are interested in andpush a fee update from the plugin to
lightningd. That means we could alsorespond to things happening on the network that could affect fees (new
block being found), or we could implement fee smoothing at the plugin level.
blockandtxnotifications could alert us about incoming blocks (headerand height) or transactions we might be interested in. The latter would
mean we need to tell the plugin about addresses and outputs we are
interested in, but it'd be only an optimization anyway since we'll catch
them during the regular processing of blocks.
Hook vs JSON-RPC passthrough
Most of the above methods could be implemented either as JSON-RPC methods
(with passthrough to the user RPC interface) or as hooks. JSON-RPC methods are
nice since they allow the user to query the same backend that
lightningduses internally (facilitating debugging), however they may end up cluttering
the RPC interface with semi-related things. Hooks on the other hand are
internal only and with the planned chaining of hooks we can have multiple
plugins with different priorities providing transparent failover (something
that with JSON-RPC is not planned right now), the downside is that the
failover isn't implemented yet and the backend is not accessible for
debugging.