diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go new file mode 100644 index 00000000..d0c49e59 --- /dev/null +++ b/watchtower/lookout/justice_descriptor_test.go @@ -0,0 +1,348 @@ +// +build dev + +package lookout_test + +import ( + "reflect" + "testing" + "time" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/watchtower/blob" + "github.com/lightningnetwork/lnd/watchtower/lookout" + "github.com/lightningnetwork/lnd/watchtower/wtdb" +) + +const csvDelay uint32 = 144 + +var ( + revPrivBytes = []byte{ + 0x8f, 0x4b, 0x51, 0x83, 0xa9, 0x34, 0xbd, 0x5f, + 0x74, 0x6c, 0x9d, 0x5c, 0xae, 0x88, 0x2d, 0x31, + 0x06, 0x90, 0xdd, 0x8c, 0x9b, 0x31, 0xbc, 0xd1, + 0x78, 0x91, 0x88, 0x2a, 0xf9, 0x74, 0xa0, 0xef, + } + + toLocalPrivBytes = []byte{ + 0xde, 0x17, 0xc1, 0x2f, 0xdc, 0x1b, 0xc0, 0xc6, + 0x59, 0x5d, 0xf9, 0xc1, 0x3e, 0x89, 0xbc, 0x6f, + 0x01, 0x85, 0x45, 0x76, 0x26, 0xce, 0x9c, 0x55, + 0x3b, 0xc9, 0xec, 0x3d, 0xd8, 0x8b, 0xac, 0xa8, + } + + toRemotePrivBytes = []byte{ + 0x28, 0x59, 0x6f, 0x36, 0xb8, 0x9f, 0x19, 0x5d, + 0xcb, 0x07, 0x48, 0x8a, 0xe5, 0x89, 0x71, 0x74, + 0x70, 0x4c, 0xff, 0x1e, 0x9c, 0x00, 0x93, 0xbe, + 0xe2, 0x2e, 0x68, 0x08, 0x4c, 0xb4, 0x0f, 0x4f, + } +) + +type mockSigner struct { + index uint32 + keys map[keychain.KeyLocator]*btcec.PrivateKey +} + +func newMockSigner() *mockSigner { + return &mockSigner{ + keys: make(map[keychain.KeyLocator]*btcec.PrivateKey), + } +} + +func (s *mockSigner) SignOutputRaw(tx *wire.MsgTx, + signDesc *lnwallet.SignDescriptor) ([]byte, error) { + + witnessScript := signDesc.WitnessScript + amt := signDesc.Output.Value + + privKey, ok := s.keys[signDesc.KeyDesc.KeyLocator] + if !ok { + panic("cannot sign w/ unknown key") + } + + sig, err := txscript.RawTxInWitnessSignature( + tx, signDesc.SigHashes, signDesc.InputIndex, amt, + witnessScript, signDesc.HashType, privKey, + ) + if err != nil { + return nil, err + } + + return sig[:len(sig)-1], nil +} + +func (s *mockSigner) ComputeInputScript(tx *wire.MsgTx, + signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) { + return nil, nil +} + +func (s *mockSigner) addPrivKey(privKey *btcec.PrivateKey) keychain.KeyLocator { + keyLoc := keychain.KeyLocator{ + Index: s.index, + } + s.index++ + + s.keys[keyLoc] = privKey + + return keyLoc +} + +func TestJusticeDescriptor(t *testing.T) { + const ( + localAmount = btcutil.Amount(100000) + remoteAmount = btcutil.Amount(200000) + totalAmount = localAmount + remoteAmount + ) + + // Parse the key pairs for all keys used in the test. + revSK, revPK := btcec.PrivKeyFromBytes( + btcec.S256(), revPrivBytes, + ) + _, toLocalPK := btcec.PrivKeyFromBytes( + btcec.S256(), toLocalPrivBytes, + ) + toRemoteSK, toRemotePK := btcec.PrivKeyFromBytes( + btcec.S256(), toRemotePrivBytes, + ) + + // Create the signer, and add the revocation and to-remote privkeys. + signer := newMockSigner() + var ( + revKeyLoc = signer.addPrivKey(revSK) + toRemoteKeyLoc = signer.addPrivKey(toRemoteSK) + ) + + // Construct the to-local witness script. + toLocalScript, err := lnwallet.CommitScriptToSelf( + csvDelay, toLocalPK, revPK, + ) + if err != nil { + t.Fatalf("unable to create to-local script: %v", err) + } + + // Compute the to-local witness script hash. + toLocalScriptHash, err := lnwallet.WitnessScriptHash(toLocalScript) + if err != nil { + t.Fatalf("unable to create to-local witness script hash: %v", err) + } + + // Compute the to-remote witness script hash. + toRemoteScriptHash, err := lnwallet.CommitScriptUnencumbered(toRemotePK) + if err != nil { + t.Fatalf("unable to create to-remote script: %v", err) + } + + // Construct the breaching commitment txn, containing the to-local and + // to-remote outputs. We don't need any inputs for this test. + breachTxn := &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{}, + TxOut: []*wire.TxOut{ + { + Value: int64(localAmount), + PkScript: toLocalScriptHash, + }, + { + Value: int64(remoteAmount), + PkScript: toRemoteScriptHash, + }, + }, + } + breachTxID := breachTxn.TxHash() + + // Compute the weight estimate for our justice transaction. + var weightEstimate lnwallet.TxWeightEstimator + weightEstimate.AddP2WKHOutput() + weightEstimate.AddP2WKHOutput() + weightEstimate.AddWitnessInput(lnwallet.ToLocalPenaltyWitnessSize) + weightEstimate.AddWitnessInput(lnwallet.P2WKHWitnessSize) + txWeight := weightEstimate.Weight() + + // Create a session info so that simulate agreement of the sweep + // parameters that should be used in constructing the justice + // transaction. + sessionInfo := &wtdb.SessionInfo{ + SweepFeeRate: 2000, + RewardRate: 900000, + RewardAddress: makeAddrSlice(22), + } + + // Given the total input amount and the weight estimate, compute the + // amount that should be swept for the victim and the amount taken as a + // reward by the watchtower. + sweepAmt, rewardAmt, err := sessionInfo.ComputeSweepOutputs( + totalAmount, int64(txWeight), + ) + if err != nil { + t.Fatalf("unable to compute sweep outputs: %v", err) + } + + // Begin to assemble the justice kit, starting with the sweep address, + // pubkeys, and csv delay. + justiceKit := &blob.JusticeKit{ + SweepAddress: makeAddrSlice(22), + CSVDelay: csvDelay, + } + copy(justiceKit.RevocationPubKey[:], revPK.SerializeCompressed()) + copy(justiceKit.LocalDelayPubKey[:], toLocalPK.SerializeCompressed()) + copy(justiceKit.CommitToRemotePubKey[:], toRemotePK.SerializeCompressed()) + + // Create a transaction spending from the outputs of the breach + // transaction created earlier. The inputs are always ordered w/ + // to-local and then to-remote. The outputs are always added as the + // sweep address then reward address. + justiceTxn := &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: breachTxID, + Index: 0, + }, + }, + { + PreviousOutPoint: wire.OutPoint{ + Hash: breachTxID, + Index: 1, + }, + }, + }, + TxOut: []*wire.TxOut{ + { + + Value: int64(sweepAmt), + PkScript: justiceKit.SweepAddress, + }, + { + + Value: int64(rewardAmt), + PkScript: sessionInfo.RewardAddress, + }, + }, + } + + hashCache := txscript.NewTxSigHashes(justiceTxn) + + // Create the sign descriptor used to sign for the to-local input. + toLocalSignDesc := &lnwallet.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: revKeyLoc, + }, + WitnessScript: toLocalScript, + Output: breachTxn.TxOut[0], + SigHashes: hashCache, + InputIndex: 0, + HashType: txscript.SigHashAll, + } + + // Create the sign descriptor used to sign for the to-remote input. + toRemoteSignDesc := &lnwallet.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: toRemoteKeyLoc, + PubKey: toRemotePK, + }, + WitnessScript: toRemoteScriptHash, + Output: breachTxn.TxOut[1], + SigHashes: hashCache, + InputIndex: 1, + HashType: txscript.SigHashAll, + } + + // Verify that our test justice transaction is sane. + btx := btcutil.NewTx(justiceTxn) + if err := blockchain.CheckTransactionSanity(btx); err != nil { + t.Fatalf("justice txn is not sane: %v", err) + } + + // Compute a DER-encoded signature for the to-local input. + toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc) + if err != nil { + t.Fatalf("unable to sign to-local input: %v", err) + } + + // Compute the witness for the to-remote input. The first element is a + // DER-encoded signature under the to-remote pubkey. The sighash flag is + // also present, so we trim it. + toRemoteWitness, err := lnwallet.CommitSpendNoDelay( + signer, toRemoteSignDesc, justiceTxn, + ) + if err != nil { + t.Fatalf("unable to sign to-remote input: %v", err) + } + toRemoteSigRaw := toRemoteWitness[0][:len(toRemoteWitness[0])-1] + + // Convert the DER to-local sig into a fixed-size signature. + toLocalSig, err := lnwire.NewSigFromRawSignature(toLocalSigRaw) + if err != nil { + t.Fatalf("unable to parse to-local signature: %v", err) + } + + // Convert the DER to-remote sig into a fixed-size signature. + toRemoteSig, err := lnwire.NewSigFromRawSignature(toRemoteSigRaw) + if err != nil { + t.Fatalf("unable to parse to-remote signature: %v", err) + } + + // Complete our justice kit by copying the signatures into the payload. + copy(justiceKit.CommitToLocalSig[:], toLocalSig[:]) + copy(justiceKit.CommitToRemoteSig[:], toRemoteSig[:]) + + justiceDesc := &lookout.JusticeDescriptor{ + BreachedCommitTx: breachTxn, + SessionInfo: sessionInfo, + JusticeKit: justiceKit, + } + + // Construct a breach punisher that will feed published transactions + // over the buffered channel. + publications := make(chan *wire.MsgTx, 1) + punisher := lookout.NewBreachPunisher(&lookout.PunisherConfig{ + PublishTx: func(tx *wire.MsgTx) error { + publications <- tx + return nil + }, + }) + + // Exact retribution on the offender. If no error is returned, we expect + // the justice transaction to be published via the channel. + err = punisher.Punish(justiceDesc, nil) + if err != nil { + t.Fatalf("unable to punish breach: %v", err) + } + + // Retrieve the published justice transaction. + var wtJusticeTxn *wire.MsgTx + select { + case wtJusticeTxn = <-publications: + case <-time.After(50 * time.Millisecond): + t.Fatalf("punisher did not publish justice txn") + } + + // Construct the test's to-local witness. + justiceTxn.TxIn[0].Witness = make([][]byte, 3) + justiceTxn.TxIn[0].Witness[0] = append(toLocalSigRaw, + byte(txscript.SigHashAll)) + justiceTxn.TxIn[0].Witness[1] = []byte{1} + justiceTxn.TxIn[0].Witness[2] = toLocalScript + + // Construct the test's to-remote witness. + justiceTxn.TxIn[1].Witness = make([][]byte, 2) + justiceTxn.TxIn[1].Witness[0] = append(toRemoteSigRaw, + byte(txscript.SigHashAll)) + justiceTxn.TxIn[1].Witness[1] = toRemotePK.SerializeCompressed() + + // Assert that the watchtower derives the same justice txn. + if !reflect.DeepEqual(justiceTxn, wtJusticeTxn) { + t.Fatalf("expected justice txn: %v\ngot %v", + spew.Sdump(justiceTxn), + spew.Sdump(wtJusticeTxn)) + } +}