Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions chainreg/blake2b_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chainreg

import (
"github.com/btcsuite/btcd/rpcclient"
"github.com/lightninglabs/neutrino"
"github.com/lightningnetwork/lnd/lncfg"
)

Expand All @@ -26,6 +27,41 @@ func chainIdentityStatusPath(cfg *Config) string {
func verifyBlake2bBackend(rpc *rpcclient.Client, cfg *Config,
quit <-chan struct{}) error {

return verifyBlake2bChain(rpc, cfg, quit)
}

// verifyBlake2bNeutrino runs the same check against the neutrino light
// client: once its headers reach the activation height, the header there must
// be a v2 header whose id this daemon reproduces, and on mainnet the pinned
// activation block. The status file is written as for bitcoind, so wrappers
// read the same state whichever backend runs.
func verifyBlake2bNeutrino(cs *neutrino.ChainService, cfg *Config,
quit <-chan struct{}) error {

return verifyBlake2bChain(&neutrinoIdentityRPC{
src: cs,
chain: bitcoindChainName(cfg.ActiveNetParams.Params),
}, cfg, quit)
}

// verifyBlake2bChain is the check itself, against whatever answers the
// calls it makes.
func verifyBlake2bChain(rpc blake2bChainRPC, cfg *Config,
quit <-chan struct{}) error {

if cfg.Shutdown != nil {
either := make(chan struct{})
stop := quit
go func() {
select {
case <-stop:
case <-cfg.Shutdown:
}
close(either)
}()
quit = either
}

params := cfg.ActiveNetParams
statusPath := chainIdentityStatusPath(cfg)

Expand Down
30 changes: 30 additions & 0 deletions chainreg/blake2b_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package chainreg
import (
"path/filepath"
"testing"
"time"

"github.com/lightningnetwork/lnd/lncfg"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -33,3 +34,32 @@ func TestChainIdentityStatusPathSelection(t *testing.T) {
ChainIdentityFileName),
chainIdentityStatusPath(cfg))
}

// TestChainIdentityWaitStopsOnShutdown pins that a daemon still waiting for
// the backend to reach the activation height stops when asked to.
func TestChainIdentityWaitStopsOnShutdown(t *testing.T) {
shutdown := make(chan struct{})
cfg := &Config{
Bitcoin: &lncfg.Chain{ChainDir: t.TempDir()},
ActiveNetParams: BitcoinMainNetParams,
Shutdown: shutdown,
}
// A backend that never gets there.
rpc := &neutrinoIdentityRPC{
src: &fakeNeutrino{height: 961000},
chain: "main",
}

done := make(chan error, 1)
go func() {
done <- verifyBlake2bChain(rpc, cfg, make(chan struct{}))
}()
close(shutdown)

select {
case err := <-done:
require.ErrorContains(t, err, "shutting down")
case <-time.After(10 * time.Second):
t.Fatal("the wait did not stop on shutdown")
}
}
123 changes: 123 additions & 0 deletions chainreg/blake2b_neutrino.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package chainreg

import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"sort"

"github.com/btcsuite/btcd/btcjson"
bitcoinCfg "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/neutrino/headerfs"
)

// NeutrinoChainParams returns the btcd parameters handed to the neutrino light
// client: the network's own, plus, where the activation block is pinned
// (mainnet), a checkpoint at the activation height with its hash. The light
// client checks BLAKE2b proof of work itself, but would follow any chain with
// the most work that passes those rules from the activation height on; the
// checkpoint ties it to the Bitcoin BLAKE2b chain's activation block. The
// shared parameters are left untouched: the checkpoint goes on a copy.
func NeutrinoChainParams(params BitcoinNetParams) *bitcoinCfg.Params {
p := *params.Params
if params.Blake2bActivationHash == nil ||
params.Blake2bActivationHeight == 0 {

return &p
}

height := int32(params.Blake2bActivationHeight)
checkpoints := make([]bitcoinCfg.Checkpoint, 0, len(p.Checkpoints)+1)
for _, c := range p.Checkpoints {
if c.Height != height {
checkpoints = append(checkpoints, c)
}
}
hash := *params.Blake2bActivationHash
checkpoints = append(checkpoints, bitcoinCfg.Checkpoint{
Height: height, Hash: &hash,
})
sort.Slice(checkpoints, func(i, j int) bool {
return checkpoints[i].Height < checkpoints[j].Height
})
p.Checkpoints = checkpoints

return &p
}

// neutrinoHeaderSource is what the chain-identity check reads from the light
// client: its best block, and the headers it validated.
type neutrinoHeaderSource interface {
BestBlock() (*headerfs.BlockStamp, error)
GetBlockHash(int64) (*chainhash.Hash, error)
GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error)
}

// neutrinoIdentityRPC lets the chain-identity check, written against a
// bitcoind backend, run against the light client: it answers the few calls
// the check makes from neutrino's validated headers. Anything else (the
// deployment info, say) is reported as not available, which the check treats
// as the node not saying.
type neutrinoIdentityRPC struct {
src neutrinoHeaderSource
chain string
}

// GetBlockChainInfo reports the light client's header height under the chain
// name the check expects, so the wait for the activation height works as it
// does with a node.
func (n *neutrinoIdentityRPC) GetBlockChainInfo() (
*btcjson.GetBlockChainInfoResult, error) {

best, err := n.src.BestBlock()
if err != nil {
return nil, err
}

return &btcjson.GetBlockChainInfoResult{
Chain: n.chain,
Blocks: best.Height,
Headers: best.Height,
}, nil
}

// GetBlockHash returns the hash of the light client's header at height.
func (n *neutrinoIdentityRPC) GetBlockHash(height int64) (*chainhash.Hash,
error) {

return n.src.GetBlockHash(height)
}

// RawRequest answers getblockheader (non-verbose) from the light client's
// headers.
func (n *neutrinoIdentityRPC) RawRequest(method string,
params []json.RawMessage) (json.RawMessage, error) {

if method != "getblockheader" || len(params) < 1 {
return nil, fmt.Errorf("%s is not available from the neutrino "+
"light client", method)
}

var hashStr string
if err := json.Unmarshal(params[0], &hashStr); err != nil {
return nil, err
}
hash, err := chainhash.NewHashFromStr(hashStr)
if err != nil {
return nil, err
}
header, err := n.src.GetBlockHeader(hash)
if err != nil {
return nil, err
}

var buf bytes.Buffer
if err := header.Serialize(&buf); err != nil {
return nil, err
}

return json.Marshal(hex.EncodeToString(buf.Bytes()))
}
136 changes: 136 additions & 0 deletions chainreg/blake2b_neutrino_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package chainreg

import (
"bytes"
"encoding/hex"
"errors"
"path/filepath"
"testing"

"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/neutrino/headerfs"
"github.com/stretchr/testify/require"
)

// fakeNeutrino serves the chain-identity check from a fixed set of headers,
// as the light client would from the ones it validated.
type fakeNeutrino struct {
height int32
byHash map[chainhash.Hash]*wire.BlockHeader
byHeight map[int64]chainhash.Hash
}

func newFakeNeutrino(t *testing.T, height int32, at int64,
hexStr string) *fakeNeutrino {

t.Helper()
raw, err := hex.DecodeString(hexStr)
require.NoError(t, err)
var h wire.BlockHeader
require.NoError(t, h.Deserialize(bytes.NewReader(raw)))
hash := h.BlockHash()
return &fakeNeutrino{
height: height,
byHash: map[chainhash.Hash]*wire.BlockHeader{hash: &h},
byHeight: map[int64]chainhash.Hash{at: hash},
}
}

func (f *fakeNeutrino) BestBlock() (*headerfs.BlockStamp, error) {
return &headerfs.BlockStamp{Height: f.height}, nil
}

func (f *fakeNeutrino) GetBlockHash(h int64) (*chainhash.Hash, error) {
hash, ok := f.byHeight[h]
if !ok {
return nil, errors.New("no header at that height")
}
return &hash, nil
}

func (f *fakeNeutrino) GetBlockHeader(hash *chainhash.Hash) (
*wire.BlockHeader, error) {

h, ok := f.byHash[*hash]
if !ok {
return nil, errors.New("unknown header")
}
return h, nil
}

// TestNeutrinoChainParamsCheckpoint pins that the light client gets a
// checkpoint at the mainnet activation block, on a copy, and nothing extra on
// a network without a pinned activation hash.
func TestNeutrinoChainParamsCheckpoint(t *testing.T) {
before := len(chaincfg.MainNetParams.Checkpoints)

p := NeutrinoChainParams(BitcoinMainNetParams)
require.Len(t, chaincfg.MainNetParams.Checkpoints, before,
"shared parameters changed")
require.Len(t, p.Checkpoints, before+1)

last := p.Checkpoints[len(p.Checkpoints)-1]
require.EqualValues(t, Blake2bMainnetActivationHeight, last.Height)
require.Equal(t, *Blake2bMainnetActivationHash, *last.Hash)
for i := 1; i < len(p.Checkpoints); i++ {
require.Less(
t, p.Checkpoints[i-1].Height, p.Checkpoints[i].Height,
)
}

t4 := NeutrinoChainParams(BitcoinTestNet4Params)
require.Equal(t, chaincfg.TestNet4Params.Checkpoints, t4.Checkpoints)
}

// TestNeutrinoChainIdentity runs the chain-identity check against the light
// client's headers: the real activation header confirms mainnet and writes
// the status file; the last SHA256d header in its place is refused.
func TestNeutrinoChainIdentity(t *testing.T) {
t.Run("activation header confirms", func(t *testing.T) {
status := filepath.Join(t.TempDir(), ChainIdentityFileName)
rpc := &neutrinoIdentityRPC{
src: newFakeNeutrino(
t, 976700, 961640, activationHeaderHex,
),
chain: "main",
}
c := newTestChecker(t, rpc, BitcoinMainNetParams, status)
height, err := c.run(make(chan struct{}))
require.NoError(t, err)
require.EqualValues(t, 961640, height)

st, err := ReadChainIdentityStatus(status)
require.NoError(t, err)
require.Equal(t, ChainIdentityConfirmed, st.State)
require.Equal(
t, Blake2bMainnetActivationHash.String(),
st.ActivationHash,
)
})

t.Run("classic header refused", func(t *testing.T) {
status := filepath.Join(t.TempDir(), ChainIdentityFileName)
rpc := &neutrinoIdentityRPC{
src: newFakeNeutrino(
t, 976700, 961640, lastSHA256HeaderHex,
),
chain: "main",
}
c := newTestChecker(t, rpc, BitcoinMainNetParams, status)
_, err := c.run(make(chan struct{}))
var wrong *ErrWrongChain
require.ErrorAs(t, err, &wrong)

st, err := ReadChainIdentityStatus(status)
require.NoError(t, err)
require.Equal(t, ChainIdentityRefused, st.State)
})

t.Run("other calls are not available", func(t *testing.T) {
rpc := &neutrinoIdentityRPC{src: &fakeNeutrino{}, chain: "main"}
_, err := rpc.RawRequest("getdeploymentinfo", nil)
require.Error(t, err)
})
}
17 changes: 17 additions & 0 deletions chainreg/chainregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ type Config struct {
// chain. It is expected to stop the daemon.
OnChainMismatch func(error)

// Shutdown, when set, ends the chain-identity check's wait for the
// activation height: a daemon still waiting there (a node that never
// gets there, a light client with only SHA256d peers) must stop when
// asked to.
Shutdown <-chan struct{}

// WalletUnlockParams are the parameters that were used for unlocking
// the main wallet.
WalletUnlockParams *walletunlocker.WalletUnlockParams
Expand Down Expand Up @@ -297,6 +303,17 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) {
cfg.ActiveNetParams.Params, cfg.NeutrinoCS,
)

// The light client checks BLAKE2b proof of work, and on
// mainnet a checkpoint ties it to the activation block; this
// confirms the chain it ended up on as the bitcoind path does
// (waiting for the activation height, then re-checking), and
// writes the same status file.
if err := verifyBlake2bNeutrino(
cfg.NeutrinoCS, cfg, chainIdentityQuit,
); err != nil {
return nil, nil, err
}

// Get our best block as a health check.
cc.HealthCheck = func() error {
_, _, err := cc.ChainSource.GetBestBlock()
Expand Down
Loading
Loading