diff --git a/blockmanager.go b/blockmanager.go index e6f2cbf77..3f3e6fa8f 100644 --- a/blockmanager.go +++ b/blockmanager.go @@ -21,6 +21,7 @@ import ( "github.com/btcsuite/btcutil/gcs" "github.com/btcsuite/btcutil/gcs/builder" "github.com/lightninglabs/neutrino/blockntfns" + "github.com/lightninglabs/neutrino/chainsync" "github.com/lightninglabs/neutrino/headerfs" "github.com/lightninglabs/neutrino/headerlist" ) @@ -1236,6 +1237,37 @@ func (b *blockManager) resolveConflict( store *headerfs.FilterHeaderStore, fType wire.FilterType) ( []*chainhash.Hash, error) { + // First check the served checkpoints against the hardcoded ones. + for peer, cp := range checkpoints { + for i, header := range cp { + height := uint32((i + 1) * wire.CFCheckptInterval) + err := chainsync.ControlCFHeader( + b.server.chainParams, fType, height, header, + ) + if err == chainsync.ErrCheckpointMismatch { + log.Warnf("Banning peer=%v since served "+ + "checkpoints didn't match our "+ + "checkpoint at height %d", peer, height) + + sp := b.server.PeerByAddr(peer) + if sp != nil { + b.server.BanPeer(sp) + sp.Disconnect() + } + delete(checkpoints, peer) + break + } else if err != nil { + return nil, err + } + } + } + + if len(checkpoints) == 0 { + return nil, fmt.Errorf("no peer is serving good cfheader " + + "checkpoints") + } + + // Check if the remaining checkpoints are sane. heightDiff, err := checkCFCheckptSanity(checkpoints, store) if err != nil { return nil, err diff --git a/chainsync/filtercontrol.go b/chainsync/filtercontrol.go new file mode 100644 index 000000000..b423a37fb --- /dev/null +++ b/chainsync/filtercontrol.go @@ -0,0 +1,56 @@ +package chainsync + +import ( + "fmt" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +// ErrCheckpointMismatch is returned if given filter headers don't pass our +// control check. +var ErrCheckpointMismatch = fmt.Errorf("checkpoint doesn't match") + +// filterHeaderCheckpoints holds a mapping from heights to filter headers for +// various heights. We use them to check whether peers are serving us the +// expected filter headers. +var filterHeaderCheckpoints = map[wire.BitcoinNet]map[uint32]*chainhash.Hash{ + // Mainnet filter header checkpoints. + chaincfg.MainNetParams.Net: map[uint32]*chainhash.Hash{}, + + // Testnet filter header checkpoints. + chaincfg.TestNet3Params.Net: map[uint32]*chainhash.Hash{}, +} + +// ControlCFHeader controls the given filter header against our list of +// checkpoints. It returns ErrCheckpointMismatch if we have a checkpoint at the +// given height, and it doesn't match. +func ControlCFHeader(params chaincfg.Params, fType wire.FilterType, + height uint32, filterHeader *chainhash.Hash) error { + + if fType != wire.GCSFilterRegular { + return fmt.Errorf("unsupported filter type %v", fType) + } + + control, ok := filterHeaderCheckpoints[params.Net] + if !ok { + return nil + } + + hash, ok := control[height] + if !ok { + return nil + } + + if *filterHeader != *hash { + return ErrCheckpointMismatch + } + + return nil +} + +func hashFromStr(hexStr string) *chainhash.Hash { + hash, _ := chainhash.NewHashFromStr(hexStr) + return hash +} diff --git a/chainsync/filtercontrol_test.go b/chainsync/filtercontrol_test.go new file mode 100644 index 000000000..4a090087a --- /dev/null +++ b/chainsync/filtercontrol_test.go @@ -0,0 +1,52 @@ +package chainsync + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +func TestControlCFHeader(t *testing.T) { + t.Parallel() + + // We'll modify our backing list of checkpoints for this test. + height := uint32(999) + header := hashFromStr( + "4a242283a406a7c089f671bb8df7671e5d5e9ba577cea1047d30a7f4919df193", + ) + filterHeaderCheckpoints = map[wire.BitcoinNet]map[uint32]*chainhash.Hash{ + chaincfg.MainNetParams.Net: map[uint32]*chainhash.Hash{ + height: header, + }, + } + + // Expect the control at height to succeed. + err := ControlCFHeader( + chaincfg.MainNetParams, wire.GCSFilterRegular, height, header, + ) + if err != nil { + t.Fatalf("error checking height: %v", err) + } + + // Pass an invalid header, this should return an error. + header = hashFromStr( + "000000000006a7c089f671bb8df7671e5d5e9ba577cea1047d30a7f4919df193", + ) + err = ControlCFHeader( + chaincfg.MainNetParams, wire.GCSFilterRegular, height, header, + ) + if err != ErrCheckpointMismatch { + t.Fatalf("expected ErrCheckpointMismatch, got %v", err) + } + + // Finally, control an unknown height. This should also pass since we + // don't have the checkpoint stored. + err = ControlCFHeader( + chaincfg.MainNetParams, wire.GCSFilterRegular, 99, header, + ) + if err != nil { + t.Fatalf("error checking height: %v", err) + } +}