github.com/decred/dcrlnd@v0.7.6/watchtower/lookout/lookout_test.go (about)

     1  package lookout_test
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"encoding/binary"
     7  	"io"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/decred/dcrd/chaincfg/v3"
    12  	"github.com/decred/dcrd/wire"
    13  	"github.com/decred/dcrlnd/chainntnfs"
    14  	"github.com/decred/dcrlnd/watchtower/blob"
    15  	"github.com/decred/dcrlnd/watchtower/lookout"
    16  	"github.com/decred/dcrlnd/watchtower/wtdb"
    17  	"github.com/decred/dcrlnd/watchtower/wtmock"
    18  	"github.com/decred/dcrlnd/watchtower/wtpolicy"
    19  )
    20  
    21  type mockPunisher struct {
    22  	matches chan *lookout.JusticeDescriptor
    23  }
    24  
    25  func (p *mockPunisher) Punish(
    26  	info *lookout.JusticeDescriptor, quit <-chan struct{}) error {
    27  
    28  	p.matches <- info
    29  	return nil
    30  }
    31  
    32  func makeArray33(i uint64) [33]byte {
    33  	var arr [33]byte
    34  	binary.BigEndian.PutUint64(arr[:], i)
    35  	return arr
    36  }
    37  
    38  func makePubKey(i uint64) [33]byte {
    39  	var arr [33]byte
    40  	arr[0] = 0x02
    41  	if i%2 == 1 {
    42  		arr[0] |= 0x01
    43  	}
    44  	binary.BigEndian.PutUint64(arr[1:], i)
    45  	return arr
    46  }
    47  
    48  func makeArray64(i uint64) [64]byte {
    49  	var arr [64]byte
    50  	binary.BigEndian.PutUint64(arr[:], i)
    51  	return arr
    52  }
    53  
    54  // makeRandomP2PKHPkScript makes a valid p2pkh pkscript using a random 20 byte
    55  // public key hash.
    56  func makeRandomP2PKHPkScript() []byte {
    57  	script := make([]byte, 25)
    58  	script[0] = 0x76
    59  	script[1] = 0xa9
    60  	script[2] = 0x14
    61  	if _, err := io.ReadFull(rand.Reader, script[3:23]); err != nil {
    62  		panic("cannot make addr")
    63  	}
    64  	script[23] = 0x88
    65  	script[24] = 0xac
    66  	return script
    67  }
    68  
    69  func TestLookoutBreachMatching(t *testing.T) {
    70  	db := wtmock.NewTowerDB()
    71  
    72  	// Initialize an mock backend to feed the lookout blocks.
    73  	backend := lookout.NewMockBackend()
    74  
    75  	// Initialize a punisher that will feed any successfully constructed
    76  	// justice descriptors across the matches channel.
    77  	matches := make(chan *lookout.JusticeDescriptor)
    78  	punisher := &mockPunisher{matches: matches}
    79  
    80  	// With the resources in place, initialize and start our watcher.
    81  	watcher := lookout.New(&lookout.Config{
    82  		BlockFetcher:   backend,
    83  		DB:             db,
    84  		EpochRegistrar: backend,
    85  		Punisher:       punisher,
    86  		NetParams:      chaincfg.RegNetParams(),
    87  	})
    88  	if err := watcher.Start(); err != nil {
    89  		t.Fatalf("unable to start watcher: %v", err)
    90  	}
    91  
    92  	rewardAndCommitType := blob.TypeFromFlags(
    93  		blob.FlagReward, blob.FlagCommitOutputs,
    94  	)
    95  
    96  	// Create two sessions, representing two distinct clients.
    97  	sessionInfo1 := &wtdb.SessionInfo{
    98  		ID: makeArray33(1),
    99  		Policy: wtpolicy.Policy{
   100  			TxPolicy: wtpolicy.TxPolicy{
   101  				BlobType:     rewardAndCommitType,
   102  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   103  			},
   104  			MaxUpdates: 10,
   105  		},
   106  		RewardAddress: makeRandomP2PKHPkScript(),
   107  	}
   108  	sessionInfo2 := &wtdb.SessionInfo{
   109  		ID: makeArray33(2),
   110  		Policy: wtpolicy.Policy{
   111  			TxPolicy: wtpolicy.TxPolicy{
   112  				BlobType:     rewardAndCommitType,
   113  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   114  			},
   115  			MaxUpdates: 10,
   116  		},
   117  		RewardAddress: makeRandomP2PKHPkScript(),
   118  	}
   119  
   120  	// Insert both sessions into the watchtower's database.
   121  	err := db.InsertSessionInfo(sessionInfo1)
   122  	if err != nil {
   123  		t.Fatalf("unable to insert session info: %v", err)
   124  	}
   125  	err = db.InsertSessionInfo(sessionInfo2)
   126  	if err != nil {
   127  		t.Fatalf("unable to insert session info: %v", err)
   128  	}
   129  
   130  	// Construct two distinct transactions, that will be used to test the
   131  	// breach hint matching.
   132  	tx := wire.NewMsgTx()
   133  	tx.Version = wire.TxVersion
   134  	hash1 := tx.TxHash()
   135  
   136  	tx2 := wire.NewMsgTx()
   137  	tx2.Version = wire.TxVersion + 1
   138  	hash2 := tx2.TxHash()
   139  
   140  	if bytes.Equal(hash1[:], hash2[:]) {
   141  		t.Fatalf("breach txids should be different")
   142  	}
   143  
   144  	// Construct a justice kit for each possible breach transaction.
   145  	blobType := blob.FlagCommitOutputs.Type()
   146  	blob1 := &blob.JusticeKit{
   147  		SweepAddress:     makeRandomP2PKHPkScript(),
   148  		BlobType:         blobType,
   149  		RevocationPubKey: makePubKey(1),
   150  		LocalDelayPubKey: makePubKey(1),
   151  		CSVDelay:         144,
   152  		CommitToLocalSig: makeArray64(1),
   153  	}
   154  	blob2 := &blob.JusticeKit{
   155  		SweepAddress:     makeRandomP2PKHPkScript(),
   156  		BlobType:         blobType,
   157  		RevocationPubKey: makePubKey(2),
   158  		LocalDelayPubKey: makePubKey(2),
   159  		CSVDelay:         144,
   160  		CommitToLocalSig: makeArray64(2),
   161  	}
   162  
   163  	key1 := blob.NewBreachKeyFromHash(&hash1)
   164  	key2 := blob.NewBreachKeyFromHash(&hash2)
   165  
   166  	// Encrypt the first justice kit under breach key one.
   167  	encBlob1, err := blob1.Encrypt(key1)
   168  	if err != nil {
   169  		t.Fatalf("unable to encrypt sweep detail 1: %v", err)
   170  	}
   171  
   172  	// Encrypt the second justice kit under breach key two.
   173  	encBlob2, err := blob2.Encrypt(key2)
   174  	if err != nil {
   175  		t.Fatalf("unable to encrypt sweep detail 2: %v", err)
   176  	}
   177  
   178  	// Add both state updates to the tower's database.
   179  	txBlob1 := &wtdb.SessionStateUpdate{
   180  		ID:            makeArray33(1),
   181  		Hint:          blob.NewBreachHintFromHash(&hash1),
   182  		EncryptedBlob: encBlob1,
   183  		SeqNum:        1,
   184  	}
   185  	txBlob2 := &wtdb.SessionStateUpdate{
   186  		ID:            makeArray33(2),
   187  		Hint:          blob.NewBreachHintFromHash(&hash2),
   188  		EncryptedBlob: encBlob2,
   189  		SeqNum:        1,
   190  	}
   191  	if _, err := db.InsertStateUpdate(txBlob1); err != nil {
   192  		t.Fatalf("unable to add tx to db: %v", err)
   193  	}
   194  	if _, err := db.InsertStateUpdate(txBlob2); err != nil {
   195  		t.Fatalf("unable to add tx to db: %v", err)
   196  	}
   197  
   198  	// Create a block containing the first transaction, connecting this
   199  	// block should match the first state update's breach hint.
   200  	block := &wire.MsgBlock{
   201  		Header: wire.BlockHeader{
   202  			Nonce: 1,
   203  		},
   204  		Transactions: []*wire.MsgTx{tx},
   205  	}
   206  	blockHash := block.BlockHash()
   207  	epoch := &chainntnfs.BlockEpoch{
   208  		Hash:   &blockHash,
   209  		Height: 1,
   210  	}
   211  
   212  	// Connect the block via our mock backend.
   213  	backend.ConnectEpoch(epoch, block)
   214  
   215  	// This should trigger dispatch of the justice kit for the first tx.
   216  	select {
   217  	case match := <-matches:
   218  		txid := match.BreachedCommitTx.TxHash()
   219  		if !bytes.Equal(txid[:], hash1[:]) {
   220  			t.Fatalf("matched breach did not match tx1's txid")
   221  		}
   222  	case <-time.After(5 * time.Second):
   223  		t.Fatalf("breach tx1 was not matched")
   224  	}
   225  
   226  	// Ensure that at most one txn was matched as a result of connecting the
   227  	// first block.
   228  	select {
   229  	case <-matches:
   230  		t.Fatalf("only one txn should have been matched")
   231  	case <-time.After(50 * time.Millisecond):
   232  	}
   233  
   234  	// Now, construct a second block containing the second breach
   235  	// transaction.
   236  	block2 := &wire.MsgBlock{
   237  		Header: wire.BlockHeader{
   238  			Nonce: 2,
   239  		},
   240  		Transactions: []*wire.MsgTx{tx2},
   241  	}
   242  	blockHash2 := block2.BlockHash()
   243  	epoch2 := &chainntnfs.BlockEpoch{
   244  		Hash:   &blockHash2,
   245  		Height: 2,
   246  	}
   247  
   248  	// Verify that the block hashes do no collide, otherwise the mock
   249  	// backend may not function properly.
   250  	if bytes.Equal(blockHash[:], blockHash2[:]) {
   251  		t.Fatalf("block hashes should be different")
   252  	}
   253  
   254  	// Connect the second block, such that the block is delivered via the
   255  	// epoch stream.
   256  	backend.ConnectEpoch(epoch2, block2)
   257  
   258  	// This should trigger dispatch of the justice kit for the second txn.
   259  	select {
   260  	case match := <-matches:
   261  		txid := match.BreachedCommitTx.TxHash()
   262  		if !bytes.Equal(txid[:], hash2[:]) {
   263  			t.Fatalf("received breach did not match tx2's txid")
   264  		}
   265  	case <-time.After(5 * time.Second):
   266  		t.Fatalf("tx was not matched")
   267  	}
   268  
   269  	// Ensure that at most one txn was matched as a result of connecting the
   270  	// second block.
   271  	select {
   272  	case <-matches:
   273  		t.Fatalf("only one txn should have been matched")
   274  	case <-time.After(50 * time.Millisecond):
   275  	}
   276  }