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 }