github.com/decred/dcrlnd@v0.7.6/watchtower/lookout/justice_descriptor_test.go (about) 1 package lookout_test 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/decred/dcrd/blockchain/standalone/v2" 8 "github.com/decred/dcrd/chaincfg/v3" 9 "github.com/decred/dcrd/dcrec/secp256k1/v4" 10 "github.com/decred/dcrd/dcrutil/v4" 11 "github.com/decred/dcrd/dcrutil/v4/txsort" 12 "github.com/decred/dcrd/txscript/v4" 13 "github.com/decred/dcrd/wire" 14 "github.com/decred/dcrlnd/input" 15 "github.com/decred/dcrlnd/keychain" 16 "github.com/decred/dcrlnd/lnwire" 17 "github.com/decred/dcrlnd/watchtower/blob" 18 "github.com/decred/dcrlnd/watchtower/lookout" 19 "github.com/decred/dcrlnd/watchtower/wtdb" 20 "github.com/decred/dcrlnd/watchtower/wtmock" 21 "github.com/decred/dcrlnd/watchtower/wtpolicy" 22 "github.com/stretchr/testify/require" 23 ) 24 25 const csvDelay uint32 = 144 26 27 var ( 28 revPrivBytes = []byte{ 29 0x8f, 0x4b, 0x51, 0x83, 0xa9, 0x34, 0xbd, 0x5f, 30 0x74, 0x6c, 0x9d, 0x5c, 0xae, 0x88, 0x2d, 0x31, 31 0x06, 0x90, 0xdd, 0x8c, 0x9b, 0x31, 0xbc, 0xd1, 32 0x78, 0x91, 0x88, 0x2a, 0xf9, 0x74, 0xa0, 0xef, 33 } 34 35 toLocalPrivBytes = []byte{ 36 0xde, 0x17, 0xc1, 0x2f, 0xdc, 0x1b, 0xc0, 0xc6, 37 0x59, 0x5d, 0xf9, 0xc1, 0x3e, 0x89, 0xbc, 0x6f, 38 0x01, 0x85, 0x45, 0x76, 0x26, 0xce, 0x9c, 0x55, 39 0x3b, 0xc9, 0xec, 0x3d, 0xd8, 0x8b, 0xac, 0xa8, 40 } 41 42 toRemotePrivBytes = []byte{ 43 0x28, 0x59, 0x6f, 0x36, 0xb8, 0x9f, 0x19, 0x5d, 44 0xcb, 0x07, 0x48, 0x8a, 0xe5, 0x89, 0x71, 0x74, 45 0x70, 0x4c, 0xff, 0x1e, 0x9c, 0x00, 0x93, 0xbe, 46 0xe2, 0x2e, 0x68, 0x08, 0x4c, 0xb4, 0x0f, 0x4f, 47 } 48 49 rewardCommitType = blob.TypeFromFlags( 50 blob.FlagReward, blob.FlagCommitOutputs, 51 ) 52 53 altruistCommitType = blob.FlagCommitOutputs.Type() 54 55 altruistAnchorCommitType = blob.TypeAltruistAnchorCommit 56 ) 57 58 // TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the 59 // correct justice transaction for different blob types. 60 func TestJusticeDescriptor(t *testing.T) { 61 tests := []struct { 62 name string 63 blobType blob.Type 64 }{ 65 { 66 name: "reward and commit type", 67 blobType: rewardCommitType, 68 }, 69 { 70 name: "altruist and commit type", 71 blobType: altruistCommitType, 72 }, 73 { 74 name: "altruist anchor commit type", 75 blobType: altruistAnchorCommitType, 76 }, 77 } 78 79 for _, test := range tests { 80 t.Run(test.name, func(t *testing.T) { 81 testJusticeDescriptor(t, test.blobType) 82 }) 83 } 84 } 85 86 func privKeyFromBytes(b []byte) (*secp256k1.PrivateKey, *secp256k1.PublicKey) { 87 p := secp256k1.PrivKeyFromBytes(b) 88 return p, p.PubKey() 89 } 90 91 func testJusticeDescriptor(t *testing.T, blobType blob.Type) { 92 isAnchorChannel := blobType.IsAnchorChannel() 93 94 const ( 95 localAmount = dcrutil.Amount(100000) 96 remoteAmount = dcrutil.Amount(200000) 97 totalAmount = localAmount + remoteAmount 98 ) 99 netParams := chaincfg.RegNetParams() 100 101 // Parse the key pairs for all keys used in the test. 102 revSK, revPK := privKeyFromBytes(revPrivBytes) 103 _, toLocalPK := privKeyFromBytes(toLocalPrivBytes) 104 toRemoteSK, toRemotePK := privKeyFromBytes(toRemotePrivBytes) 105 106 // Create the signer, and add the revocation and to-remote privkeys. 107 signer := wtmock.NewMockSigner() 108 var ( 109 revKeyLoc = signer.AddPrivKey(revSK) 110 toRemoteKeyLoc = signer.AddPrivKey(toRemoteSK) 111 ) 112 113 // Construct the to-local witness script. 114 toLocalScript, err := input.CommitScriptToSelf( 115 csvDelay, toLocalPK, revPK, 116 ) 117 require.Nil(t, err) 118 119 // Compute the to-local witness script hash. 120 toLocalScriptHash, err := input.ScriptHashPkScript(toLocalScript) 121 require.Nil(t, err) 122 123 // Compute the to-remote redeem script, pk script, and sequence 124 // numbers. 125 // 126 // NOTE: This is pretty subtle. 127 // 128 // The actual redeem script (i.e. last push of the SigScript) for a 129 // p2pkh output is just the pubkey, but the witness sighash calculation 130 // injects the classic p2kh script: OP_DUP OP_HASH160 <pubkey-hash160> 131 // OP_EQUALVERIFY OP_CHECKSIG. When signing for p2pkh we don't pass the 132 // raw pubkey as the redeem script to the sign descriptor (since that's 133 // also not a valid script). Instead we give it the _p2pkh redeem 134 // script_ of the standard form (OP_DUP, ...) from which pubkey-hash160 135 // is extracted during sighash calculation. 136 // 137 // On the other hand, signing for the anchor P2SH to-remote outputs 138 // requires the sign descriptor to contain the redeem script verbatim. 139 // This difference in behavior forces us to use a distinct 140 // toRemoteSigningScript to handle both cases. 141 var ( 142 toRemoteSequence uint32 143 toRemoteRedeemScript []byte 144 toRemoteScriptHash []byte 145 toRemoteSigningScript []byte 146 ) 147 if isAnchorChannel { 148 toRemoteSequence = 1 149 toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed( 150 toRemotePK, 151 ) 152 require.Nil(t, err) 153 154 toRemoteScriptHash, err = input.ScriptHashPkScript( 155 toRemoteRedeemScript, 156 ) 157 require.Nil(t, err) 158 159 // As it should be. 160 toRemoteSigningScript = toRemoteRedeemScript 161 } else { 162 toRemoteRedeemScript = toRemotePK.SerializeCompressed() 163 toRemoteScriptHash, err = input.CommitScriptUnencumbered( 164 toRemotePK, 165 ) 166 require.Nil(t, err) 167 168 // NOTE: This is the _pkscript_. 169 toRemoteSigningScript = toRemoteScriptHash 170 } 171 172 // Construct the breaching commitment txn, containing the to-local and 173 // to-remote outputs. We don't need any inputs for this test. 174 breachTxn := &wire.MsgTx{ 175 Version: 2, 176 TxIn: []*wire.TxIn{}, 177 TxOut: []*wire.TxOut{ 178 { 179 Value: int64(localAmount), 180 PkScript: toLocalScriptHash, 181 }, 182 { 183 Value: int64(remoteAmount), 184 PkScript: toRemoteScriptHash, 185 }, 186 }, 187 } 188 breachTxID := breachTxn.TxHash() 189 190 // Compute the size estimate for our justice transaction. 191 var sizeEstimate input.TxSizeEstimator 192 sizeEstimate.AddCustomInput(input.ToLocalPenaltySigScriptSize) 193 194 if isAnchorChannel { 195 sizeEstimate.AddCustomInput(input.ToRemoteConfirmedWitnessSize) 196 } else { 197 sizeEstimate.AddP2PKHInput() 198 } 199 sizeEstimate.AddP2PKHOutput() 200 if blobType.Has(blob.FlagReward) { 201 sizeEstimate.AddP2PKHOutput() 202 } 203 txSize := sizeEstimate.Size() 204 205 // Create a session info so that simulate agreement of the sweep 206 // parameters that should be used in constructing the justice 207 // transaction. 208 policy := wtpolicy.Policy{ 209 TxPolicy: wtpolicy.TxPolicy{ 210 BlobType: blobType, 211 SweepFeeRate: 2000, 212 RewardRate: 900000, 213 }, 214 } 215 sessionInfo := &wtdb.SessionInfo{ 216 Policy: policy, 217 RewardAddress: makeRandomP2PKHPkScript(), 218 } 219 220 // Begin to assemble the justice kit, starting with the sweep address, 221 // pubkeys, and csv delay. 222 justiceKit := &blob.JusticeKit{ 223 BlobType: blobType, 224 SweepAddress: makeRandomP2PKHPkScript(), 225 CSVDelay: csvDelay, 226 } 227 copy(justiceKit.RevocationPubKey[:], revPK.SerializeCompressed()) 228 copy(justiceKit.LocalDelayPubKey[:], toLocalPK.SerializeCompressed()) 229 copy(justiceKit.CommitToRemotePubKey[:], toRemotePK.SerializeCompressed()) 230 231 // Create a transaction spending from the outputs of the breach 232 // transaction created earlier. The inputs are always ordered w/ 233 // to-local and then to-remote. The outputs are always added as the 234 // sweep address then reward address. 235 justiceTxn := &wire.MsgTx{ 236 Version: 2, 237 TxIn: []*wire.TxIn{ 238 { 239 PreviousOutPoint: wire.OutPoint{ 240 Hash: breachTxID, 241 Index: 0, 242 }, 243 ValueIn: breachTxn.TxOut[0].Value, 244 }, 245 { 246 PreviousOutPoint: wire.OutPoint{ 247 Hash: breachTxID, 248 Index: 1, 249 }, 250 ValueIn: breachTxn.TxOut[1].Value, 251 Sequence: toRemoteSequence, 252 }, 253 }, 254 } 255 256 outputs, err := policy.ComputeJusticeTxOuts( 257 totalAmount, txSize, justiceKit.SweepAddress, 258 sessionInfo.RewardAddress, 259 ) 260 require.Nil(t, err) 261 262 // Attach the txouts and BIP69 sort the resulting transaction. 263 justiceTxn.TxOut = outputs 264 txsort.InPlaceSort(justiceTxn) 265 266 // Create the sign descriptor used to sign for the to-local input. 267 toLocalSignDesc := &input.SignDescriptor{ 268 KeyDesc: keychain.KeyDescriptor{ 269 KeyLocator: revKeyLoc, 270 }, 271 WitnessScript: toLocalScript, 272 Output: breachTxn.TxOut[0], 273 InputIndex: 0, 274 HashType: txscript.SigHashAll, 275 } 276 277 // Create the sign descriptor used to sign for the to-remote input. 278 toRemoteSignDesc := &input.SignDescriptor{ 279 KeyDesc: keychain.KeyDescriptor{ 280 KeyLocator: toRemoteKeyLoc, 281 PubKey: toRemotePK, 282 }, 283 WitnessScript: toRemoteSigningScript, 284 Output: breachTxn.TxOut[1], 285 InputIndex: 1, 286 HashType: txscript.SigHashAll, 287 } 288 289 // Verify that our test justice transaction is sane. 290 err = standalone.CheckTransactionSanity(justiceTxn, uint64(netParams.MaxTxSize)) 291 require.Nil(t, err) 292 293 // Compute a DER-encoded signature for the to-local input. 294 toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc) 295 require.Nil(t, err) 296 297 // Compute the witness for the to-remote input. The first element is a 298 // DER-encoded signature under the to-remote pubkey. The sighash flag is 299 // also present, so we trim it. 300 toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc) 301 require.Nil(t, err) 302 303 // Convert the DER to-local sig into a fixed-size signature. 304 toLocalSig, err := lnwire.NewSigFromSignature(toLocalSigRaw) 305 require.Nil(t, err) 306 307 // Convert the DER to-remote sig into a fixed-size signature. 308 toRemoteSig, err := lnwire.NewSigFromSignature(toRemoteSigRaw) 309 require.Nil(t, err) 310 311 // Complete our justice kit by copying the signatures into the payload. 312 copy(justiceKit.CommitToLocalSig[:], toLocalSig[:]) 313 copy(justiceKit.CommitToRemoteSig[:], toRemoteSig[:]) 314 315 justiceDesc := &lookout.JusticeDescriptor{ 316 BreachedCommitTx: breachTxn, 317 SessionInfo: sessionInfo, 318 JusticeKit: justiceKit, 319 NetParams: netParams, 320 } 321 322 // Construct a breach punisher that will feed published transactions 323 // over the buffered channel. 324 publications := make(chan *wire.MsgTx, 1) 325 punisher := lookout.NewBreachPunisher(&lookout.PunisherConfig{ 326 PublishTx: func(tx *wire.MsgTx, _ string) error { 327 publications <- tx 328 return nil 329 }, 330 }) 331 332 // Exact retribution on the offender. If no error is returned, we expect 333 // the justice transaction to be published via the channel. 334 err = punisher.Punish(justiceDesc, nil) 335 require.Nil(t, err) 336 337 // Retrieve the published justice transaction. 338 var wtJusticeTxn *wire.MsgTx 339 select { 340 case wtJusticeTxn = <-publications: 341 case <-time.After(50 * time.Millisecond): 342 t.Fatalf("punisher did not publish justice txn") 343 } 344 345 // Construct the test's to-local witness. 346 wstack0 := make([][]byte, 3) 347 wstack0[0] = append(toLocalSigRaw.Serialize(), byte(txscript.SigHashAll)) 348 wstack0[1] = []byte{1} 349 wstack0[2] = toLocalScript 350 justiceTxn.TxIn[0].SignatureScript, err = input.WitnessStackToSigScript(wstack0) 351 if err != nil { 352 t.Fatalf("error assembling wstack0: %v", err) 353 } 354 355 // Construct the test's to-remote witness. 356 wstack1 := make([][]byte, 2) 357 wstack1[0] = append(toRemoteSigRaw.Serialize(), byte(txscript.SigHashAll)) 358 wstack1[1] = toRemoteRedeemScript 359 justiceTxn.TxIn[1].SignatureScript, err = input.WitnessStackToSigScript(wstack1) 360 if err != nil { 361 t.Fatalf("error assembling wstack1: %v", err) 362 } 363 364 // Assert that the watchtower derives the same justice txn. 365 require.Equal(t, justiceTxn, wtJusticeTxn) 366 }