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  }