diff --git a/cmd/lncli/wtclient.go b/cmd/lncli/wtclient.go index c08601c324..068fc3005f 100644 --- a/cmd/lncli/wtclient.go +++ b/cmd/lncli/wtclient.go @@ -284,8 +284,14 @@ var policyCommand = cli.Command{ "policy. (default)", }, cli.BoolFlag{ - Name: "anchor", - Usage: "Retrieve the anchor tower client's current policy.", + Name: "anchor", + Usage: "Retrieve the anchor tower client's current " + + "policy.", + }, + cli.BoolFlag{ + Name: "taproot", + Usage: "Retrieve the taproot tower client's current " + + "policy.", }, }, } @@ -305,6 +311,8 @@ func policy(ctx *cli.Context) error { policyType = wtclientrpc.PolicyType_ANCHOR case ctx.Bool("legacy"): policyType = wtclientrpc.PolicyType_LEGACY + case ctx.Bool("taproot"): + policyType = wtclientrpc.PolicyType_TAPROOT // For backwards compatibility with original rpc behavior. default: diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md index dea62a1036..098fbd585e 100644 --- a/docs/release-notes/release-notes-0.18.0.md +++ b/docs/release-notes/release-notes-0.18.0.md @@ -85,7 +85,13 @@ control to [handle pathfinding errors](https://github.com/lightningnetwork/lnd/pull/8095) for blinded paths are also included. * A new config value, - [http-header-timeout](https://github.com/lightningnetwork/lnd/pull/7715), is added so users can specify the amount of time the http server will wait for a request to complete before closing the connection. The default value is 5 seconds. + [http-header-timeout](https://github.com/lightningnetwork/lnd/pull/7715), is + added so users can specify the amount of time the http server will wait for a + request to complete before closing the connection. The default value is 5 + seconds. +* Update [watchtowers to be Taproot + ready](https://github.com/lightningnetwork/lnd/pull/7733) + * [`routerrpc.usestatusinitiated` is introduced](https://github.com/lightningnetwork/lnd/pull/8177) to signal that @@ -190,7 +196,7 @@ * [Add a watchtower tower client multiplexer](https://github.com/lightningnetwork/lnd/pull/7702) to manage tower clients of different types. - + * [Introduce CommitmentType and JusticeKit interface](https://github.com/lightningnetwork/lnd/pull/7736) to simplify the code. diff --git a/input/script_utils.go b/input/script_utils.go index d907737b84..fde0c6f5e9 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -2124,27 +2124,14 @@ func NewLocalCommitScriptTree(csvTimeout uint32, // First, we'll need to construct the tapLeaf that'll be our delay CSV // clause. - builder := txscript.NewScriptBuilder() - builder.AddData(schnorr.SerializePubKey(selfKey)) - builder.AddOp(txscript.OP_CHECKSIG) - builder.AddInt64(int64(csvTimeout)) - builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) - builder.AddOp(txscript.OP_DROP) - - delayScript, err := builder.Script() + delayScript, err := TaprootLocalCommitDelayScript(csvTimeout, selfKey) if err != nil { return nil, err } // Next, we'll need to construct the revocation path, which is just a // simple checksig script. - builder = txscript.NewScriptBuilder() - builder.AddData(schnorr.SerializePubKey(selfKey)) - builder.AddOp(txscript.OP_DROP) - builder.AddData(schnorr.SerializePubKey(revokeKey)) - builder.AddOp(txscript.OP_CHECKSIG) - - revokeScript, err := builder.Script() + revokeScript, err := TaprootLocalCommitRevokeScript(selfKey, revokeKey) if err != nil { return nil, err } @@ -2176,6 +2163,35 @@ func NewLocalCommitScriptTree(csvTimeout uint32, }, nil } +// TaprootLocalCommitDelayScript builds the tap leaf with the CSV delay script +// for the to-local output. +func TaprootLocalCommitDelayScript(csvTimeout uint32, + selfKey *btcec.PublicKey) ([]byte, error) { + + builder := txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(selfKey)) + builder.AddOp(txscript.OP_CHECKSIG) + builder.AddInt64(int64(csvTimeout)) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + + return builder.Script() +} + +// TaprootLocalCommitRevokeScript builds the tap leaf with the revocation path +// for the to-local output. +func TaprootLocalCommitRevokeScript(selfKey, revokeKey *btcec.PublicKey) ( + []byte, error) { + + builder := txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(selfKey)) + builder.AddOp(txscript.OP_DROP) + builder.AddData(schnorr.SerializePubKey(revokeKey)) + builder.AddOp(txscript.OP_CHECKSIG) + + return builder.Script() +} + // TaprootCommitScriptToSelf creates the taproot witness program that commits // to the revocation (script path) and delay path (script path) in a single // taproot output key. Both the delay script and the revocation script are part diff --git a/itest/lnd_revocation_test.go b/itest/lnd_revocation_test.go index 1b6a5eac40..d02e60d7e3 100644 --- a/itest/lnd_revocation_test.go +++ b/itest/lnd_revocation_test.go @@ -715,30 +715,24 @@ func testRevokedCloseRetributionRemoteHodl(ht *lntest.HarnessTest) { // asserts that Willy responds by broadcasting the justice transaction on // Carol's behalf sweeping her funds without a reward. func testRevokedCloseRetributionAltruistWatchtower(ht *lntest.HarnessTest) { - testCases := []struct { - name string - anchors bool - }{{ - name: "anchors", - anchors: true, - }, { - name: "legacy", - anchors: false, - }} - - for _, tc := range testCases { - tc := tc + for _, commitType := range []lnrpc.CommitmentType{ + lnrpc.CommitmentType_LEGACY, + lnrpc.CommitmentType_ANCHORS, + lnrpc.CommitmentType_SIMPLE_TAPROOT, + } { + testName := fmt.Sprintf("%v", commitType.String()) + ct := commitType testFunc := func(ht *lntest.HarnessTest) { testRevokedCloseRetributionAltruistWatchtowerCase( - ht, tc.anchors, + ht, ct, ) } - success := ht.Run(tc.name, func(tt *testing.T) { + success := ht.Run(testName, func(tt *testing.T) { st := ht.Subtest(tt) st.RunTestCase(&lntest.TestCase{ - Name: tc.name, + Name: testName, TestFunc: testFunc, }) }) @@ -756,7 +750,7 @@ func testRevokedCloseRetributionAltruistWatchtower(ht *lntest.HarnessTest) { } func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, - anchors bool) { + commitType lnrpc.CommitmentType) { const ( chanAmt = funding.MaxBtcFundingAmount @@ -767,18 +761,19 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, // Since we'd like to test some multi-hop failure scenarios, we'll // introduce another node into our test network: Carol. - carolArgs := []string{"--hodl.exit-settle"} - if anchors { - carolArgs = append(carolArgs, "--protocol.anchors") - } + carolArgs := lntest.NodeArgsForCommitType(commitType) + carolArgs = append(carolArgs, "--hodl.exit-settle") + carol := ht.NewNode("Carol", carolArgs) // Willy the watchtower will protect Dave from Carol's breach. He will // remain online in order to punish Carol on Dave's behalf, since the // breach will happen while Dave is offline. willy := ht.NewNode( - "Willy", []string{"--watchtower.active", - "--watchtower.externalip=" + externalIP}, + "Willy", []string{ + "--watchtower.active", + "--watchtower.externalip=" + externalIP, + }, ) willyInfo := willy.RPC.GetInfoWatchtower() @@ -801,13 +796,8 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, // Dave will be the breached party. We set --nolisten to ensure Carol // won't be able to connect to him and trigger the channel data // protection logic automatically. - daveArgs := []string{ - "--nolisten", - "--wtclient.active", - } - if anchors { - daveArgs = append(daveArgs, "--protocol.anchors") - } + daveArgs := lntest.NodeArgsForCommitType(commitType) + daveArgs = append(daveArgs, "--nolisten", "--wtclient.active") dave := ht.NewNode("Dave", daveArgs) addTowerReq := &wtclientrpc.AddTowerRequest{ @@ -833,8 +823,10 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, // closure by Carol, we'll first open up a channel between them with a // 0.5 BTC value. params := lntest.OpenChannelParams{ - Amt: 3 * (chanAmt / 4), - PushAmt: chanAmt / 4, + Amt: 3 * (chanAmt / 4), + PushAmt: chanAmt / 4, + CommitmentType: commitType, + Private: true, } chanPoint := ht.OpenChannel(dave, carol, params) @@ -956,7 +948,7 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, willyBalResp := willy.RPC.WalletBalance() if willyBalResp.ConfirmedBalance != 0 { - return fmt.Errorf("Expected Willy to have no funds "+ + return fmt.Errorf("expected Willy to have no funds "+ "after justice transaction was mined, found %v", willyBalResp) } @@ -994,7 +986,7 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, ht.AssertNumPendingForceClose(dave, 0) // If this is an anchor channel, Dave would sweep the anchor. - if anchors { + if lntest.CommitTypeHasAnchors(commitType) { ht.MineBlocksAndAssertNumTxes(1, 1) } diff --git a/lnrpc/wtclientrpc/wtclient.go b/lnrpc/wtclientrpc/wtclient.go index bf7f9a1da8..4a68e22a03 100644 --- a/lnrpc/wtclientrpc/wtclient.go +++ b/lnrpc/wtclientrpc/wtclient.go @@ -273,9 +273,9 @@ func (c *WatchtowerClient) ListTowers(ctx context.Context, // for the legacy client to the existing tower. rpcTowers := make(map[wtdb.TowerID]*Tower) for blobType, towers := range towersPerBlobType { - policyType := PolicyType_LEGACY - if blobType.IsAnchorChannel() { - policyType = PolicyType_ANCHOR + policyType, err := blobTypeToPolicyType(blobType) + if err != nil { + return nil, err } for _, tower := range towers { @@ -331,9 +331,9 @@ func (c *WatchtowerClient) GetTowerInfo(ctx context.Context, var resTower *Tower for blobType, tower := range towersPerBlobType { - policyType := PolicyType_LEGACY - if blobType.IsAnchorChannel() { - policyType = PolicyType_ANCHOR + policyType, err := blobTypeToPolicyType(blobType) + if err != nil { + return nil, err } rpcTower := marshallTower( @@ -437,15 +437,9 @@ func (c *WatchtowerClient) Policy(ctx context.Context, return nil, err } - var blobType blob.Type - switch req.PolicyType { - case PolicyType_LEGACY: - blobType = blob.TypeAltruistCommit - case PolicyType_ANCHOR: - blobType = blob.TypeAltruistAnchorCommit - default: - return nil, fmt.Errorf("unknown policy type: %v", - req.PolicyType) + blobType, err := policyTypeToBlobType(req.PolicyType) + if err != nil { + return nil, err } policy, err := c.cfg.ClientMgr.Policy(blobType) @@ -525,3 +519,35 @@ func marshallTower(tower *wtclient.RegisteredTower, policyType PolicyType, return rpcTower } + +func blobTypeToPolicyType(t blob.Type) (PolicyType, error) { + switch t { + case blob.TypeAltruistTaprootCommit: + return PolicyType_TAPROOT, nil + + case blob.TypeAltruistAnchorCommit: + return PolicyType_ANCHOR, nil + + case blob.TypeAltruistCommit: + return PolicyType_LEGACY, nil + + default: + return 0, fmt.Errorf("unknown blob type: %s", t) + } +} + +func policyTypeToBlobType(t PolicyType) (blob.Type, error) { + switch t { + case PolicyType_TAPROOT: + return blob.TypeAltruistTaprootCommit, nil + + case PolicyType_ANCHOR: + return blob.TypeAltruistAnchorCommit, nil + + case PolicyType_LEGACY: + return blob.TypeAltruistCommit, nil + + default: + return 0, fmt.Errorf("unknown policy type: %s", t) + } +} diff --git a/lnrpc/wtclientrpc/wtclient.pb.go b/lnrpc/wtclientrpc/wtclient.pb.go index 62d1f65900..063b5f5340 100644 --- a/lnrpc/wtclientrpc/wtclient.pb.go +++ b/lnrpc/wtclientrpc/wtclient.pb.go @@ -27,6 +27,8 @@ const ( PolicyType_LEGACY PolicyType = 0 // Selects the policy from the anchor tower client. PolicyType_ANCHOR PolicyType = 1 + // Selects the policy from the taproot tower client. + PolicyType_TAPROOT PolicyType = 2 ) // Enum value maps for PolicyType. @@ -34,10 +36,12 @@ var ( PolicyType_name = map[int32]string{ 0: "LEGACY", 1: "ANCHOR", + 2: "TAPROOT", } PolicyType_value = map[string]int32{ - "LEGACY": 0, - "ANCHOR": 1, + "LEGACY": 0, + "ANCHOR": 1, + "TAPROOT": 2, } ) @@ -1069,42 +1073,43 @@ var file_wtclientrpc_wtclient_proto_rawDesc = []byte{ 0x65, 0x65, 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x42, 0x79, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x13, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x76, 0x62, 0x79, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x73, 0x77, 0x65, 0x65, - 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x56, 0x62, 0x79, 0x74, 0x65, 0x2a, 0x24, 0x0a, 0x0a, + 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x56, 0x62, 0x79, 0x74, 0x65, 0x2a, 0x31, 0x0a, 0x0a, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52, - 0x10, 0x01, 0x32, 0xc5, 0x03, 0x0a, 0x10, 0x57, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, - 0x72, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x54, 0x6f, - 0x77, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x41, 0x64, 0x64, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1d, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x41, 0x64, 0x64, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x50, 0x0a, 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x12, - 0x1f, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, - 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x20, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, - 0x12, 0x1e, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x44, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e, 0x66, - 0x6f, 0x12, 0x20, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x47, 0x65, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, - 0x12, 0x19, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x77, 0x74, - 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x12, 0x1a, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, - 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2f, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x10, 0x02, 0x32, + 0xc5, 0x03, 0x0a, 0x10, 0x57, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x54, 0x6f, 0x77, 0x65, 0x72, + 0x12, 0x1c, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x64, 0x64, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, + 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, + 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, + 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x77, + 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, + 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6d, 0x6f, + 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4d, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x12, 0x1e, 0x2e, + 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, + 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, + 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x20, + 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, + 0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x12, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x54, + 0x6f, 0x77, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x19, 0x2e, + 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1a, + 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x77, 0x74, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2f, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/lnrpc/wtclientrpc/wtclient.proto b/lnrpc/wtclientrpc/wtclient.proto index b152279c96..b236e01585 100644 --- a/lnrpc/wtclientrpc/wtclient.proto +++ b/lnrpc/wtclientrpc/wtclient.proto @@ -219,6 +219,9 @@ enum PolicyType { // Selects the policy from the anchor tower client. ANCHOR = 1; + + // Selects the policy from the taproot tower client. + TAPROOT = 2; } message PolicyRequest { diff --git a/lnrpc/wtclientrpc/wtclient.swagger.json b/lnrpc/wtclientrpc/wtclient.swagger.json index 1d0468dd68..36dc87981a 100644 --- a/lnrpc/wtclientrpc/wtclient.swagger.json +++ b/lnrpc/wtclientrpc/wtclient.swagger.json @@ -154,13 +154,14 @@ "parameters": [ { "name": "policy_type", - "description": "The client type from which to retrieve the active offering policy.\n\n - LEGACY: Selects the policy from the legacy tower client.\n - ANCHOR: Selects the policy from the anchor tower client.", + "description": "The client type from which to retrieve the active offering policy.\n\n - LEGACY: Selects the policy from the legacy tower client.\n - ANCHOR: Selects the policy from the anchor tower client.\n - TAPROOT: Selects the policy from the taproot tower client.", "in": "query", "required": false, "type": "string", "enum": [ "LEGACY", - "ANCHOR" + "ANCHOR", + "TAPROOT" ], "default": "LEGACY" } @@ -318,10 +319,11 @@ "type": "string", "enum": [ "LEGACY", - "ANCHOR" + "ANCHOR", + "TAPROOT" ], "default": "LEGACY", - "description": " - LEGACY: Selects the policy from the legacy tower client.\n - ANCHOR: Selects the policy from the anchor tower client." + "description": " - LEGACY: Selects the policy from the legacy tower client.\n - ANCHOR: Selects the policy from the anchor tower client.\n - TAPROOT: Selects the policy from the taproot tower client." }, "wtclientrpcRemoveTowerResponse": { "type": "object" diff --git a/peer/brontide.go b/peer/brontide.go index 705ec6974b..d0df4f1e0b 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -1043,24 +1043,6 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, return p.cfg.ChainArb.NotifyContractUpdate(*chanPoint, update) } - var towerClient wtclient.ClientManager - if lnChan.ChanType().IsTaproot() { - // Leave the tower client as nil for now until the tower client - // has support for taproot channels. - // - // If the user has activated the tower client, then add a log - // to explain that any taproot channel updates wil not be - // backed up to a tower. - if p.cfg.TowerClient != nil { - p.log.Debugf("Updates for channel %s will not be "+ - "backed up to a watchtower as watchtowers "+ - "are not yet taproot channel compatible", - chanPoint) - } - } else { - towerClient = p.cfg.TowerClient - } - //nolint:lll linkCfg := htlcswitch.ChannelLinkConfig{ Peer: p, @@ -1090,7 +1072,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, MinFeeUpdateTimeout: htlcswitch.DefaultMinLinkFeeUpdateTimeout, MaxFeeUpdateTimeout: htlcswitch.DefaultMaxLinkFeeUpdateTimeout, OutgoingCltvRejectDelta: p.cfg.OutgoingCltvRejectDelta, - TowerClient: towerClient, + TowerClient: p.cfg.TowerClient, MaxOutgoingCltvExpiry: p.cfg.MaxOutgoingCltvExpiry, MaxFeeAllocation: p.cfg.MaxChannelFeeAllocation, MaxAnchorsCommitFeeRate: p.cfg.MaxAnchorsCommitFeeRate, diff --git a/server.go b/server.go index aed4d3c854..df2489db02 100644 --- a/server.go +++ b/server.go @@ -1553,6 +1553,13 @@ func newServer(cfg *Config, listenAddrs []net.Addr, anchorPolicy := policy anchorPolicy.BlobType |= blob.Type(blob.FlagAnchorChannel) + // Copy the policy for legacy channels and set the blob flag + // signalling support for taproot channels. + taprootPolicy := policy + taprootPolicy.TxPolicy.BlobType |= blob.Type( + blob.FlagTaprootChannel, + ) + s.towerClientMgr, err = wtclient.NewManager(&wtclient.Config{ FetchClosedChannel: fetchClosedChannel, BuildBreachRetribution: buildBreachRetribution, @@ -1574,7 +1581,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, MinBackoff: 10 * time.Second, MaxBackoff: 5 * time.Minute, MaxTasksInMemQueue: cfg.WtClient.MaxTasksInMemQueue, - }, policy, anchorPolicy) + }, policy, anchorPolicy, taprootPolicy) if err != nil { return nil, err } diff --git a/watchtower/blob/commitments.go b/watchtower/blob/commitments.go index 0f476e5a38..f862404470 100644 --- a/watchtower/blob/commitments.go +++ b/watchtower/blob/commitments.go @@ -27,6 +27,10 @@ const ( // anchor channel. The key differences are that the to_remote is // encumbered by a 1 block CSV and so is thus a P2WSH output. AnchorCommitment + + // TaprootCommitment represents the commitment transaction of a simple + // taproot channel. + TaprootCommitment ) // ToLocalInput constructs the input that will be used to spend the to_local @@ -61,9 +65,10 @@ func (c CommitmentType) ToRemoteInput(info *lnwallet.BreachRetribution) ( info.LocalOutputSignDesc, 0, ), nil - case AnchorCommitment: - // Anchor channels have a CSV-encumbered to-remote output. We'll - // construct a CSV input and assign the proper CSV delay of 1. + case AnchorCommitment, TaprootCommitment: + // Anchor and Taproot channels have a CSV-encumbered to-remote + // output. We'll construct a CSV input and assign the proper CSV + // delay of 1. return input.NewCsvInput( &info.LocalOutpoint, witnessType, info.LocalOutputSignDesc, 0, 1, @@ -80,6 +85,9 @@ func (c CommitmentType) ToLocalWitnessType() (input.WitnessType, error) { case LegacyTweaklessCommitment, LegacyCommitment, AnchorCommitment: return input.CommitmentRevoke, nil + case TaprootCommitment: + return input.TaprootCommitmentRevoke, nil + default: return nil, fmt.Errorf("unknown commitment type: %v", c) } @@ -97,6 +105,9 @@ func (c CommitmentType) ToRemoteWitnessType() (input.WitnessType, error) { case AnchorCommitment: return input.CommitmentToRemoteConfirmed, nil + case TaprootCommitment: + return input.TaprootRemoteCommitSpend, nil + default: return nil, fmt.Errorf("unknown commitment type: %v", c) } @@ -115,6 +126,10 @@ func (c CommitmentType) ToRemoteWitnessSize() (int, error) { case AnchorCommitment: return input.ToRemoteConfirmedWitnessSize, nil + // Taproot channels spend a confirmed P2SH output. + case TaprootCommitment: + return input.TaprootToRemoteWitnessSize, nil + default: return 0, fmt.Errorf("unknown commitment type: %v", c) } @@ -134,6 +149,9 @@ func (c CommitmentType) ToLocalWitnessSize() (int, error) { case AnchorCommitment: return input.ToLocalPenaltyWitnessSize, nil + case TaprootCommitment: + return input.TaprootToLocalRevokeWitnessSize, nil + default: return 0, fmt.Errorf("unknown commitment type: %v", c) } @@ -143,21 +161,22 @@ func (c CommitmentType) ToLocalWitnessSize() (int, error) { func (c CommitmentType) ParseRawSig(witness wire.TxWitness) (lnwire.Sig, error) { - switch c { - case LegacyCommitment, LegacyTweaklessCommitment, AnchorCommitment: - // Check that the witness has at least one item. - if len(witness) < 1 { - return lnwire.Sig{}, fmt.Errorf("the witness should " + - "have at least one element") - } + // Check that the witness has at least one item since this is required + // for all commitment types to follow. + if len(witness) < 1 { + return lnwire.Sig{}, fmt.Errorf("the witness should have at " + + "least one element") + } - // Check that the first witness element is non-nil. This is to - // ensure that the witness length check below does not panic. - if witness[0] == nil { - return lnwire.Sig{}, fmt.Errorf("the first witness " + - "element should not be nil") - } + // Check that the first witness element is non-nil. This is to ensure + // that the witness length checks below do not panic. + if witness[0] == nil { + return lnwire.Sig{}, fmt.Errorf("the first witness element " + + "should not be nil") + } + switch c { + case LegacyCommitment, LegacyTweaklessCommitment, AnchorCommitment: // Parse the DER-encoded signature from the first position of // the resulting witness. We trim an extra byte to remove the // sighash flag. @@ -167,6 +186,16 @@ func (c CommitmentType) ParseRawSig(witness wire.TxWitness) (lnwire.Sig, // signature. return lnwire.NewSigFromECDSARawSignature(rawSignature) + case TaprootCommitment: + rawSignature := witness[0] + if len(rawSignature) > 64 { + rawSignature = witness[0][:len(witness[0])-1] + } + + // Re-encode the schnorr signature into a fixed-size 64 byte + // signature. + return lnwire.NewSigFromSchnorrRawSignature(rawSignature) + default: return lnwire.Sig{}, fmt.Errorf("unknown commitment type: %v", c) @@ -190,6 +219,11 @@ func (c CommitmentType) NewJusticeKit(sweepScript []byte, sweepScript, breachInfo, withToRemote, ), nil + case TaprootCommitment: + return newTaprootJusticeKit( + sweepScript, breachInfo, withToRemote, + ) + default: return nil, fmt.Errorf("unknown commitment type: %v", c) } @@ -207,6 +241,9 @@ func (c CommitmentType) EmptyJusticeKit() (JusticeKit, error) { legacyJusticeKit: legacyJusticeKit{}, }, nil + case TaprootCommitment: + return &taprootJusticeKit{}, nil + default: return nil, fmt.Errorf("unknown commitment type: %v", c) } diff --git a/watchtower/blob/justice_kit.go b/watchtower/blob/justice_kit.go index 7741ff0264..8b6c20194f 100644 --- a/watchtower/blob/justice_kit.go +++ b/watchtower/blob/justice_kit.go @@ -1,11 +1,15 @@ package blob import ( + "bytes" "io" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -69,11 +73,10 @@ func newLegacyJusticeKit(sweepScript []byte, keyRing := breachInfo.KeyRing packet := justiceKitPacketV0{ - sweepAddress: sweepScript, - revocationPubKey: toBlobPubKey(keyRing.RevocationKey), - localDelayPubKey: toBlobPubKey(keyRing.ToLocalKey), - csvDelay: breachInfo.RemoteDelay, - commitToRemotePubKey: pubKey{}, + sweepAddress: sweepScript, + revocationPubKey: toBlobPubKey(keyRing.RevocationKey), + localDelayPubKey: toBlobPubKey(keyRing.ToLocalKey), + csvDelay: breachInfo.RemoteDelay, } if withToRemote { @@ -286,3 +289,214 @@ func (a *anchorJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript, return &pkScript, witness, 1, nil } + +// taprootJusticeKit is an implementation of the JusticeKit interface which can +// be used for backing up commitments of taproot channels. +type taprootJusticeKit struct { + justiceKitPacketV1 +} + +// A compile-time check to ensure that taprootJusticeKit implements the +// JusticeKit interface. +var _ JusticeKit = (*taprootJusticeKit)(nil) + +// newTaprootJusticeKit constructs a new taprootJusticeKit. +func newTaprootJusticeKit(sweepScript []byte, + breachInfo *lnwallet.BreachRetribution, + withToRemote bool) (*taprootJusticeKit, error) { + + keyRing := breachInfo.KeyRing + + tree, err := input.NewLocalCommitScriptTree( + breachInfo.RemoteDelay, keyRing.ToLocalKey, + keyRing.RevocationKey, + ) + if err != nil { + return nil, err + } + + packet := justiceKitPacketV1{ + sweepAddress: sweepScript, + revocationPubKey: toBlobSchnorrPubKey( + keyRing.RevocationKey, + ), + localDelayPubKey: toBlobSchnorrPubKey(keyRing.ToLocalKey), + delayScriptHash: tree.SettleLeaf.TapHash(), + } + + if withToRemote { + packet.commitToRemotePubKey = toBlobPubKey(keyRing.ToRemoteKey) + } + + return &taprootJusticeKit{packet}, nil +} + +// ToLocalOutputSpendInfo returns the info required to send the to-local +// output. It returns the output pubkey script and the witness required +// to spend the output. +// +// NOTE: This is part of the JusticeKit interface. +func (t *taprootJusticeKit) ToLocalOutputSpendInfo() (*txscript.PkScript, + wire.TxWitness, error) { + + revocationPubKey, err := schnorr.ParsePubKey(t.revocationPubKey[:]) + if err != nil { + return nil, nil, err + } + + localDelayedPubKey, err := schnorr.ParsePubKey(t.localDelayPubKey[:]) + if err != nil { + return nil, nil, err + } + + revokeScript, err := input.TaprootLocalCommitRevokeScript( + localDelayedPubKey, revocationPubKey, + ) + if err != nil { + return nil, nil, err + } + + revokeLeaf := txscript.NewBaseTapLeaf(revokeScript) + revokeLeafHash := revokeLeaf.TapHash() + rootHash := tapBranchHash(revokeLeafHash[:], t.delayScriptHash[:]) + + outputKey := txscript.ComputeTaprootOutputKey( + &input.TaprootNUMSKey, rootHash[:], + ) + + scriptPk, err := input.PayToTaprootScript(outputKey) + if err != nil { + return nil, nil, err + } + + ctrlBlock := txscript.ControlBlock{ + InternalKey: &input.TaprootNUMSKey, + OutputKeyYIsOdd: isOddPub(outputKey), + LeafVersion: revokeLeaf.LeafVersion, + InclusionProof: t.delayScriptHash[:], + } + + ctrlBytes, err := ctrlBlock.ToBytes() + if err != nil { + return nil, nil, err + } + + toLocalSig, err := t.commitToLocalSig.ToSignature() + if err != nil { + return nil, nil, err + } + + witness := make([][]byte, 3) + witness[0] = toLocalSig.Serialize() + witness[1] = revokeScript + witness[2] = ctrlBytes + + pkScript, err := txscript.ParsePkScript(scriptPk) + if err != nil { + return nil, nil, err + } + + return &pkScript, witness, nil +} + +// ToRemoteOutputSpendInfo returns the info required to send the to-remote +// output. It returns the output pubkey script, the witness required to spend +// the output and the sequence to apply. +// +// NOTE: This is part of the JusticeKit interface. +func (t *taprootJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript, + wire.TxWitness, uint32, error) { + + if len(t.commitToRemotePubKey[:]) == 0 { + return nil, nil, 0, ErrNoCommitToRemoteOutput + } + + toRemotePk, err := btcec.ParsePubKey(t.commitToRemotePubKey[:]) + if err != nil { + return nil, nil, 0, err + } + + scriptTree, err := input.NewRemoteCommitScriptTree(toRemotePk) + if err != nil { + return nil, nil, 0, err + } + + script, err := input.PayToTaprootScript(scriptTree.TaprootKey) + if err != nil { + return nil, nil, 0, err + } + + settleControlBlock := input.MakeTaprootCtrlBlock( + scriptTree.SettleLeaf.Script, &input.TaprootNUMSKey, + scriptTree.TapscriptTree, + ) + + ctrl, err := settleControlBlock.ToBytes() + if err != nil { + return nil, nil, 0, err + } + + toRemoteSig, err := t.commitToRemoteSig.ToSignature() + if err != nil { + return nil, nil, 0, err + } + + witness := make([][]byte, 3) + witness[0] = toRemoteSig.Serialize() + witness[1] = scriptTree.SettleLeaf.Script + witness[2] = ctrl + + pkScript, err := txscript.ParsePkScript(script) + if err != nil { + return nil, nil, 0, err + } + + return &pkScript, witness, 1, nil +} + +// HasCommitToRemoteOutput returns true if the blob contains a to-remote pubkey. +// +// NOTE: This is part of the JusticeKit interface. +func (t *taprootJusticeKit) HasCommitToRemoteOutput() bool { + return btcec.IsCompressedPubKey(t.commitToRemotePubKey[:]) +} + +// AddToLocalSig adds the to-local signature to the kit. +// +// NOTE: This is part of the JusticeKit interface. +func (t *taprootJusticeKit) AddToLocalSig(sig lnwire.Sig) { + t.commitToLocalSig = sig +} + +// AddToRemoteSig adds the to-remote signature to the kit. +// +// NOTE: This is part of the JusticeKit interface. +func (t *taprootJusticeKit) AddToRemoteSig(sig lnwire.Sig) { + t.commitToRemoteSig = sig +} + +// SweepAddress returns the sweep address to be used on the justice tx output. +// +// NOTE: This is part of the JusticeKit interface. +func (t *taprootJusticeKit) SweepAddress() []byte { + return t.sweepAddress +} + +// PlainTextSize is the size of the encoded-but-unencrypted blob in bytes. +// +// NOTE: This is part of the JusticeKit interface. +func (t *taprootJusticeKit) PlainTextSize() int { + return V1PlaintextSize +} + +func tapBranchHash(l, r []byte) chainhash.Hash { + if bytes.Compare(l, r) > 0 { + l, r = r, l + } + + return *chainhash.TaggedHash(chainhash.TagTapBranch, l, r) +} + +func isOddPub(key *btcec.PublicKey) bool { + return key.SerializeCompressed()[0] == secp.PubKeyFormatCompressedOdd +} diff --git a/watchtower/blob/justice_kit_packet.go b/watchtower/blob/justice_kit_packet.go index 8db3e9c732..fd799cd1fd 100644 --- a/watchtower/blob/justice_kit_packet.go +++ b/watchtower/blob/justice_kit_packet.go @@ -9,6 +9,8 @@ import ( "io" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/lnwire" "golang.org/x/crypto/chacha20poly1305" ) @@ -35,6 +37,17 @@ const ( // commit to-remote sig: 64 bytes, maybe blank V0PlaintextSize = 274 + // V1PlaintextSize is the plaintext size of a version 1 encoded blob. + // sweep address length: 1 byte + // padded sweep address: 42 bytes + // revocation pubkey: 32 bytes + // local delay pubkey: 32 bytes + // commit to-local revocation sig: 64 bytes + // hash of to-local delay script: 32 bytes + // commit to-remote pubkey: 33 bytes, maybe blank + // commit to-remote sig: 64 bytes, maybe blank + V1PlaintextSize = 300 + // MaxSweepAddrSize defines the maximum sweep address size that can be // encoded in a blob. MaxSweepAddrSize = 42 @@ -78,6 +91,17 @@ func Size(kit JusticeKit) int { return NonceSize + kit.PlainTextSize() + CiphertextExpansion } +// schnorrPubKey is a 32-byte serialized x-only public key. +type schnorrPubKey [32]byte + +// toBlobSchnorrPubKey serializes the given public key into a schnorrPubKey that +// can be set as a field on a JusticeKit. +func toBlobSchnorrPubKey(pubKey *btcec.PublicKey) schnorrPubKey { + var blobPubKey schnorrPubKey + copy(blobPubKey[:], schnorr.SerializePubKey(pubKey)) + return blobPubKey +} + // pubKey is a 33-byte, serialized compressed public key. type pubKey [33]byte @@ -400,3 +424,222 @@ func (b *justiceKitPacketV0) decode(r io.Reader) error { return nil } + +// justiceKitPacketV1 is the Blob of Justice for taproot channels. +type justiceKitPacketV1 struct { + // sweepAddress is the witness program of the output where the client's + // fund will be deposited. This value is included in the blobs, as + // opposed to the session info, such that the sweep addresses can't be + // correlated across sessions and/or towers. + // + // NOTE: This is chosen to be the length of a maximally sized witness + // program. + sweepAddress []byte + + // revocationPubKey is the x-only pubkey that guards the revocation + // clause of the remote party's to-local output. + revocationPubKey schnorrPubKey + + // localDelayPubKey is the x-only pubkey in the to-local script of + // the remote party, which guards the path where the remote party + // claims their commitment output. + localDelayPubKey schnorrPubKey + + // delayScriptHash is the hash of the to_local delay script that is used + // in the TapTree. + delayScriptHash [chainhash.HashSize]byte + + // commitToLocalSig is a signature under revocationPubKey using + // SIGHASH_DEFAULT. + commitToLocalSig lnwire.Sig + + // commitToRemotePubKey is the public key in the to-remote output of the + // revoked commitment transaction. This uses a 33-byte compressed pubkey + // encoding unlike the other public keys because it will not always be + // present and so this gives us an easy way to check if it is present or + // not. + // + // NOTE: This value is only used if it contains a valid compressed + // public key. + commitToRemotePubKey pubKey + + // commitToRemoteSig is a signature under commitToRemotePubKey using + // SIGHASH_DEFAULT. + // + // NOTE: This value is only used if commitToRemotePubKey contains a + // valid compressed public key. + commitToRemoteSig lnwire.Sig +} + +// encode encodes the justiceKitPacketV1 to the provided io.Writer. The encoding +// supports sweeping of the commit to-local output, and optionally the commit +// to-remote output. The encoding produces a constant-size plaintext size of +// 300 bytes. +// +// blob version 1 plaintext encoding: +// +// sweep address length: 1 byte +// padded sweep address: 42 bytes +// revocation pubkey: 32 bytes +// local delay pubkey: 32 bytes +// commit to-local revocation sig: 64 bytes +// hash of to-local delay script: 32 bytes +// commit to-remote pubkey: 33 bytes, maybe blank +// commit to-remote sig: 64 bytes, maybe blank +func (t *justiceKitPacketV1) encode(w io.Writer) error { + // Assert the sweep address length is sane. + if len(t.sweepAddress) > MaxSweepAddrSize { + return ErrSweepAddressToLong + } + + // Write the actual length of the sweep address as a single byte. + err := binary.Write(w, byteOrder, uint8(len(t.sweepAddress))) + if err != nil { + return err + } + + // Pad the sweep address to our maximum length of 42 bytes. + var sweepAddressBuf [MaxSweepAddrSize]byte + copy(sweepAddressBuf[:], t.sweepAddress) + + // Write padded 42-byte sweep address. + _, err = w.Write(sweepAddressBuf[:]) + if err != nil { + return err + } + + // Write 32-byte revocation public key. + _, err = w.Write(t.revocationPubKey[:]) + if err != nil { + return err + } + + // Write 32-byte local delay public key. + _, err = w.Write(t.localDelayPubKey[:]) + if err != nil { + return err + } + + // Write 64-byte revocation signature for commit to-local output. + _, err = w.Write(t.commitToLocalSig.RawBytes()) + if err != nil { + return err + } + + // Write 32-byte hash of the to-local delay script. + _, err = w.Write(t.delayScriptHash[:]) + if err != nil { + return err + } + + // Write 33-byte commit to-remote public key, which may be blank. + _, err = w.Write(t.commitToRemotePubKey[:]) + if err != nil { + return err + } + + // Write 64-byte commit to-remote signature, which may be blank. + _, err = w.Write(t.commitToRemoteSig.RawBytes()) + + return err +} + +// decode reconstructs a justiceKitPacketV1 from the io.Reader, using version 1 +// encoding scheme. This will parse a constant size input stream of 300 bytes to +// recover information for the commit to-local output, and possibly the commit +// to-remote output. +// +// blob version 1 plaintext encoding: +// +// sweep address length: 1 byte +// padded sweep address: 42 bytes +// revocation pubkey: 32 bytes +// local delay pubkey: 32 bytes +// commit to-local revocation sig: 64 bytes +// hash of to-local delay script: 32 bytes +// commit to-remote pubkey: 33 bytes, maybe blank +// commit to-remote sig: 64 bytes, maybe blank +func (t *justiceKitPacketV1) decode(r io.Reader) error { + // Read the sweep address length as a single byte. + var sweepAddrLen uint8 + err := binary.Read(r, byteOrder, &sweepAddrLen) + if err != nil { + return err + } + // Assert the sweep address length is sane. + if sweepAddrLen > MaxSweepAddrSize { + return ErrSweepAddressToLong + } + // Read padded 42-byte sweep address. + var sweepAddressBuf [MaxSweepAddrSize]byte + _, err = io.ReadFull(r, sweepAddressBuf[:]) + if err != nil { + return err + } + + // Parse sweep address from padded buffer. + t.sweepAddress = make([]byte, sweepAddrLen) + copy(t.sweepAddress, sweepAddressBuf[:]) + + // Read 32-byte revocation public key. + _, err = io.ReadFull(r, t.revocationPubKey[:]) + if err != nil { + return err + } + + // Read 32-byte local delay public key. + _, err = io.ReadFull(r, t.localDelayPubKey[:]) + if err != nil { + return err + } + + // Read 64-byte revocation signature for commit to-local output. + var localSig [64]byte + _, err = io.ReadFull(r, localSig[:]) + if err != nil { + return err + } + + // Read 32-byte to-local delay script hash. + _, err = io.ReadFull(r, t.delayScriptHash[:]) + if err != nil { + return err + } + + t.commitToLocalSig, err = lnwire.NewSigFromSchnorrRawSignature( + localSig[:], + ) + if err != nil { + return err + } + var ( + commitToRemotePubkey pubKey + commitToRemoteSig [64]byte + ) + // Read 33-byte commit to-remote public key, which may be discarded. + _, err = io.ReadFull(r, commitToRemotePubkey[:]) + if err != nil { + return err + } + // Read 64-byte commit to-remote signature, which may be discarded. + _, err = io.ReadFull(r, commitToRemoteSig[:]) + if err != nil { + return err + } + + // Only populate the commit to-remote fields in the decoded blob if a + // valid compressed public key was read from the reader. + if !btcec.IsCompressedPubKey(commitToRemotePubkey[:]) { + return nil + } + + t.commitToRemotePubKey = commitToRemotePubkey + t.commitToRemoteSig, err = lnwire.NewSigFromSchnorrRawSignature( + commitToRemoteSig[:], + ) + if err != nil { + return err + } + + return nil +} diff --git a/watchtower/blob/justice_kit_test.go b/watchtower/blob/justice_kit_test.go index 4851bbf7e0..fd12993a0a 100644 --- a/watchtower/blob/justice_kit_test.go +++ b/watchtower/blob/justice_kit_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/input" @@ -17,6 +18,8 @@ import ( "github.com/stretchr/testify/require" ) +const csvDelay = uint32(144) + func makePubKey() *btcec.PublicKey { priv, _ := btcec.NewPrivateKey() return priv.PubKey() @@ -39,6 +42,15 @@ func makeAddr(size int) []byte { return addr } +func makeSchnorrSig(i int) lnwire.Sig { + var sigBytes [64]byte + binary.BigEndian.PutUint64(sigBytes[:8], uint64(i)) + + sig, _ := lnwire.NewSigFromSchnorrRawSignature(sigBytes[:]) + + return sig +} + type descriptorTest struct { name string encVersion Type @@ -46,7 +58,6 @@ type descriptorTest struct { sweepAddr []byte revPubKey *btcec.PublicKey delayPubKey *btcec.PublicKey - csvDelay uint32 commitToLocalSig lnwire.Sig hasCommitToRemote bool commitToRemotePubKey *btcec.PublicKey @@ -63,7 +74,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(22), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), }, { @@ -73,7 +83,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(22), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), hasCommitToRemote: true, commitToRemotePubKey: makePubKey(), @@ -86,7 +95,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(34), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), encErr: ErrUnknownBlobType, }, @@ -97,7 +105,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(34), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), decErr: ErrUnknownBlobType, }, @@ -108,7 +115,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(0), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), }, { @@ -118,7 +124,6 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(MaxSweepAddrSize), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), }, { @@ -128,10 +133,30 @@ var descriptorTests = []descriptorTest{ sweepAddr: makeAddr(MaxSweepAddrSize + 1), revPubKey: makePubKey(), delayPubKey: makePubKey(), - csvDelay: 144, commitToLocalSig: makeSig(1), encErr: ErrSweepAddressToLong, }, + { + name: "taproot to-local only", + encVersion: TypeAltruistTaprootCommit, + decVersion: TypeAltruistTaprootCommit, + sweepAddr: makeAddr(34), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), + commitToLocalSig: makeSchnorrSig(1), + }, + { + name: "taproot to-local and to-remote", + encVersion: TypeAltruistTaprootCommit, + decVersion: TypeAltruistTaprootCommit, + sweepAddr: makeAddr(34), + revPubKey: makePubKey(), + delayPubKey: makePubKey(), + commitToLocalSig: makeSchnorrSig(1), + hasCommitToRemote: true, + commitToRemotePubKey: makePubKey(), + commitToRemoteSig: makeSchnorrSig(2), + }, } // TestBlobJusticeKitEncryptDecrypt asserts that encrypting and decrypting a @@ -154,7 +179,7 @@ func testBlobJusticeKitEncryptDecrypt(t *testing.T, test descriptorTest) { } breachInfo := &lnwallet.BreachRetribution{ - RemoteDelay: test.csvDelay, + RemoteDelay: csvDelay, KeyRing: &lnwallet.CommitmentKeyRing{ ToLocalKey: test.delayPubKey, ToRemoteKey: test.commitToRemotePubKey, @@ -221,6 +246,8 @@ type remoteWitnessTest struct { name string blobType Type expWitnessScript func(pk *btcec.PublicKey) []byte + expWitnessStack func(sig input.Signature) wire.TxWitness + createSig func(*btcec.PrivateKey, []byte) input.Signature } // TestJusticeKitRemoteWitnessConstruction tests that a JusticeKit returns the @@ -234,6 +261,21 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { expWitnessScript: func(pk *btcec.PublicKey) []byte { return pk.SerializeCompressed() }, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + sigBytes := append( + sig.Serialize(), + byte(txscript.SigHashAll), + ) + + return [][]byte{sigBytes} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + return ecdsa.Sign(priv, digest) + }, }, { name: "anchor commitment", @@ -242,6 +284,42 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { script, _ := input.CommitScriptToRemoteConfirmed(pk) return script }, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + sigBytes := append( + sig.Serialize(), + byte(txscript.SigHashAll), + ) + + return [][]byte{sigBytes} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + return ecdsa.Sign(priv, digest) + }, + }, + { + name: "taproot commitment", + blobType: TypeAltruistTaprootCommit, + expWitnessScript: func(pk *btcec.PublicKey) []byte { + tree, _ := input.NewRemoteCommitScriptTree(pk) + + return tree.SettleLeaf.Script + }, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + return [][]byte{sig.Serialize()} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + sig, _ := schnorr.Sign(priv, digest) + + return sig + }, }, } for _, test := range tests { @@ -252,8 +330,8 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) { } } -func testJusticeKitRemoteWitnessConstruction( - t *testing.T, test remoteWitnessTest) { +func testJusticeKitRemoteWitnessConstruction(t *testing.T, + test remoteWitnessTest) { // Generate the to-remote pubkey. toRemotePrivKey, err := btcec.NewPrivateKey() @@ -268,7 +346,7 @@ func testJusticeKitRemoteWitnessConstruction( // Sign a message using the to-remote private key. The exact message // doesn't matter as we won't be validating the signature's validity. digest := bytes.Repeat([]byte("a"), 32) - rawToRemoteSig := ecdsa.Sign(toRemotePrivKey, digest) + rawToRemoteSig := test.createSig(toRemotePrivKey, digest) // Convert the DER-encoded signature into a fixed-size sig. commitToRemoteSig, err := lnwire.NewSigFromSignature(rawToRemoteSig) @@ -298,24 +376,122 @@ func testJusticeKitRemoteWitnessConstruction( expToRemoteScript := test.expWitnessScript(toRemotePrivKey.PubKey()) require.Equal(t, expToRemoteScript, witness[1]) - // Compute the expected first element, by appending a sighash all byte - // to our raw DER-encoded signature. - rawToRemoteSigWithSigHash := append( - rawToRemoteSig.Serialize(), byte(txscript.SigHashAll), - ) - - // Assert that the expected witness stack is returned. - expWitnessStack := wire.TxWitness{ - rawToRemoteSigWithSigHash, - } + // Compute the expected signature. + expWitnessStack := test.expWitnessStack(rawToRemoteSig) require.Equal(t, expWitnessStack, witness[:1]) } +type localWitnessTest struct { + name string + blobType Type + expWitnessScript func(delay, rev *btcec.PublicKey) []byte + expWitnessStack func(sig input.Signature) wire.TxWitness + witnessScriptIndex int + createSig func(*btcec.PrivateKey, []byte) input.Signature +} + // TestJusticeKitToLocalWitnessConstruction tests that a JusticeKit returns the // proper to-local witness script and to-local witness stack for spending the // revocation path. func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { - csvDelay := uint32(144) + tests := []localWitnessTest{ + { + name: "legacy commitment", + blobType: TypeAltruistCommit, + expWitnessScript: func(delay, + rev *btcec.PublicKey) []byte { + + script, _ := input.CommitScriptToSelf( + csvDelay, delay, rev, + ) + + return script + }, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + sigBytes := append( + sig.Serialize(), + byte(txscript.SigHashAll), + ) + + return [][]byte{sigBytes, {1}} + }, + witnessScriptIndex: 2, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + return ecdsa.Sign(priv, digest) + }, + }, + { + name: "anchor commitment", + blobType: TypeAltruistAnchorCommit, + expWitnessScript: func(delay, + rev *btcec.PublicKey) []byte { + + script, _ := input.CommitScriptToSelf( + csvDelay, delay, rev, + ) + + return script + }, + witnessScriptIndex: 2, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + sigBytes := append( + sig.Serialize(), + byte(txscript.SigHashAll), + ) + + return [][]byte{sigBytes, {1}} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + return ecdsa.Sign(priv, digest) + }, + }, + { + name: "taproot commitment", + blobType: TypeAltruistTaprootCommit, + expWitnessScript: func(delay, + rev *btcec.PublicKey) []byte { + + script, _ := input.NewLocalCommitScriptTree( + csvDelay, delay, rev, + ) + + return script.RevocationLeaf.Script + }, + witnessScriptIndex: 1, + expWitnessStack: func( + sig input.Signature) wire.TxWitness { + + return [][]byte{sig.Serialize()} + }, + createSig: func(priv *btcec.PrivateKey, + digest []byte) input.Signature { + + sig, _ := schnorr.Sign(priv, digest) + + return sig + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + testJusticeKitToLocalWitnessConstruction(t, test) + }) + } +} + +func testJusticeKitToLocalWitnessConstruction(t *testing.T, + test localWitnessTest) { // Generate the revocation and delay private keys. revPrivKey, err := btcec.NewPrivateKey() @@ -327,13 +503,13 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { // Sign a message using the revocation private key. The exact message // doesn't matter as we won't be validating the signature's validity. digest := bytes.Repeat([]byte("a"), 32) - rawRevSig := ecdsa.Sign(revPrivKey, digest) + rawRevSig := test.createSig(revPrivKey, digest) // Convert the DER-encoded signature into a fixed-size sig. commitToLocalSig, err := lnwire.NewSigFromSignature(rawRevSig) require.NoError(t, err) - commitType, err := TypeAltruistCommit.CommitmentType(nil) + commitType, err := test.blobType.CommitmentType(nil) require.NoError(t, err) breachInfo := &lnwallet.BreachRetribution{ @@ -350,28 +526,18 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) { // Compute the expected to-local script, which is a function of the CSV // delay, revocation pubkey and delay pubkey. - expToLocalScript, err := input.CommitScriptToSelf( - csvDelay, delayPrivKey.PubKey(), revPrivKey.PubKey(), + expToLocalScript := test.expWitnessScript( + delayPrivKey.PubKey(), revPrivKey.PubKey(), ) - require.NoError(t, err) // Compute the to-local script that is returned by the justice kit. _, witness, err := justiceKit.ToLocalOutputSpendInfo() require.NoError(t, err) // Assert that the expected to-local script matches the actual script. - require.Equal(t, expToLocalScript, witness[2]) + require.Equal(t, expToLocalScript, witness[test.witnessScriptIndex]) - // Compute the expected signature in the bottom element of the stack, by - // appending a sighash all flag to the raw DER signature. - rawRevSigWithSigHash := append( - rawRevSig.Serialize(), byte(txscript.SigHashAll), - ) - - // Finally, validate against our expected witness stack. - expWitnessStack := wire.TxWitness{ - rawRevSigWithSigHash, - {1}, - } - require.Equal(t, expWitnessStack, witness[:2]) + // Finally, validate the witness. + expWitnessStack := test.expWitnessStack(rawRevSig) + require.Equal(t, expWitnessStack, witness[:test.witnessScriptIndex]) } diff --git a/watchtower/blob/type.go b/watchtower/blob/type.go index f77befb89f..aee163ec03 100644 --- a/watchtower/blob/type.go +++ b/watchtower/blob/type.go @@ -26,6 +26,10 @@ const ( // channel, and therefore must expect a P2WSH-style to-remote output if // one exists. FlagAnchorChannel Flag = 1 << 2 + + // FlagTaprootChannel signals that this blob is meant to spend a + // taproot channel and therefore must expect P2TR outputs. + FlagTaprootChannel Flag = 1 << 3 ) // Type returns a Type consisting solely of this flag enabled. @@ -42,6 +46,8 @@ func (f Flag) String() string { return "FlagCommitOutputs" case FlagAnchorChannel: return "FlagAnchorChannel" + case FlagTaprootChannel: + return "FlagTaprootChannel" default: return "FlagUnknown" } @@ -67,8 +73,26 @@ const ( // TypeRewardCommit sweeps only commitment outputs to a sweep address // controlled by the user, and pays a negotiated reward to the tower. TypeRewardCommit = Type(FlagCommitOutputs | FlagReward) + + // TypeAltruistTaprootCommit sweeps only the commitment outputs from a + // taproot channel commitment to a sweep address controlled by the user, + // and does not give the tower a reward. + TypeAltruistTaprootCommit = Type(FlagCommitOutputs | FlagTaprootChannel) ) +// TypeFromChannel returns the appropriate blob Type for the given channel +// type. +func TypeFromChannel(chanType channeldb.ChannelType) Type { + switch { + case chanType.IsTaproot(): + return TypeAltruistTaprootCommit + case chanType.HasAnchors(): + return TypeAltruistAnchorCommit + default: + return TypeAltruistCommit + } +} + // Identifier returns a unique, stable string identifier for the blob Type. func (t Type) Identifier() (string, error) { switch t { @@ -78,6 +102,8 @@ func (t Type) Identifier() (string, error) { return "anchor", nil case TypeRewardCommit: return "reward", nil + case TypeAltruistTaprootCommit: + return "taproot", nil default: return "", fmt.Errorf("unknown blob type: %v", t) } @@ -89,6 +115,9 @@ func (t Type) CommitmentType(chanType *channeldb.ChannelType) (CommitmentType, error) { switch { + case t.Has(FlagTaprootChannel): + return TaprootCommitment, nil + case t.Has(FlagAnchorChannel): return AnchorCommitment, nil @@ -124,14 +153,20 @@ func (t Type) IsAnchorChannel() bool { return t.Has(FlagAnchorChannel) } +// IsTaprootChannel returns true if the blob type is for a taproot channel. +func (t Type) IsTaprootChannel() bool { + return t.Has(FlagTaprootChannel) +} + // knownFlags maps the supported flags to their name. var knownFlags = map[Flag]struct{}{ - FlagReward: {}, - FlagCommitOutputs: {}, - FlagAnchorChannel: {}, + FlagReward: {}, + FlagCommitOutputs: {}, + FlagAnchorChannel: {}, + FlagTaprootChannel: {}, } -// String returns a human readable description of a Type. +// String returns a human-readable description of a Type. func (t Type) String() string { var ( hrPieces []string @@ -175,9 +210,10 @@ func (t Type) String() string { // supportedTypes is the set of all configurations known to be supported by the // package. var supportedTypes = map[Type]struct{}{ - TypeAltruistCommit: {}, - TypeRewardCommit: {}, - TypeAltruistAnchorCommit: {}, + TypeAltruistCommit: {}, + TypeRewardCommit: {}, + TypeAltruistAnchorCommit: {}, + TypeAltruistTaprootCommit: {}, } // IsSupportedType returns true if the given type is supported by the package. diff --git a/watchtower/blob/type_test.go b/watchtower/blob/type_test.go index 026263df30..87d9a8af1d 100644 --- a/watchtower/blob/type_test.go +++ b/watchtower/blob/type_test.go @@ -16,19 +16,28 @@ type typeStringTest struct { var typeStringTests = []typeStringTest{ { - name: "commit no-reward", - typ: blob.TypeAltruistCommit, - expStr: "[No-FlagAnchorChannel|FlagCommitOutputs|No-FlagReward]", + name: "commit no-reward", + typ: blob.TypeAltruistCommit, + expStr: "[No-FlagTaprootChannel|" + + "No-FlagAnchorChannel|" + + "FlagCommitOutputs|" + + "No-FlagReward]", }, { - name: "commit reward", - typ: blob.TypeRewardCommit, - expStr: "[No-FlagAnchorChannel|FlagCommitOutputs|FlagReward]", + name: "commit reward", + typ: blob.TypeRewardCommit, + expStr: "[No-FlagTaprootChannel|" + + "No-FlagAnchorChannel|" + + "FlagCommitOutputs|" + + "FlagReward]", }, { - name: "unknown flag", - typ: unknownFlag.Type(), - expStr: "0000000000010000[No-FlagAnchorChannel|No-FlagCommitOutputs|No-FlagReward]", + name: "unknown flag", + typ: unknownFlag.Type(), + expStr: "0000000000010000[No-FlagTaprootChannel|" + + "No-FlagAnchorChannel|" + + "No-FlagCommitOutputs|" + + "No-FlagReward]", }, } diff --git a/watchtower/lookout/justice_descriptor.go b/watchtower/lookout/justice_descriptor.go index 22db70ab51..b39d5c73a3 100644 --- a/watchtower/lookout/justice_descriptor.go +++ b/watchtower/lookout/justice_descriptor.go @@ -176,6 +176,8 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64, return nil, fmt.Errorf("error creating previous output "+ "fetcher: %v", err) } + + hashes := txscript.NewTxSigHashes(justiceTxn, prevOutFetcher) for _, inp := range inputs { // Lookup the input's new post-sort position. i := inputIndex[inp.outPoint] @@ -186,7 +188,7 @@ func (p *JusticeDescriptor) assembleJusticeTxn(txWeight int64, vm, err := txscript.NewEngine( inp.txOut.PkScript, justiceTxn, i, txscript.StandardVerifyFlags, - nil, nil, inp.txOut.Value, prevOutFetcher, + nil, hashes, inp.txOut.Value, prevOutFetcher, ) if err != nil { return nil, err diff --git a/watchtower/lookout/justice_descriptor_test.go b/watchtower/lookout/justice_descriptor_test.go index 42317648a9..2ca187fdc7 100644 --- a/watchtower/lookout/justice_descriptor_test.go +++ b/watchtower/lookout/justice_descriptor_test.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcutil/txsort" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -53,6 +54,8 @@ var ( altruistCommitType = blob.FlagCommitOutputs.Type() altruistAnchorCommitType = blob.TypeAltruistAnchorCommit + + altruisticTaprootCommitType = blob.TypeAltruistTaprootCommit ) // TestJusticeDescriptor asserts that a JusticeDescriptor is able to produce the @@ -74,6 +77,10 @@ func TestJusticeDescriptor(t *testing.T) { name: "altruist anchor commit type", blobType: altruistAnchorCommitType, }, + { + name: "altruist taproot commit type", + blobType: altruisticTaprootCommitType, + }, } for _, test := range tests { @@ -85,6 +92,7 @@ func TestJusticeDescriptor(t *testing.T) { func testJusticeDescriptor(t *testing.T, blobType blob.Type) { isAnchorChannel := blobType.IsAnchorChannel() + isTaprootChannel := blobType.IsTaprootChannel() const ( localAmount = btcutil.Amount(100000) @@ -108,15 +116,34 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { toRemoteKeyLoc = signer.AddPrivKey(toRemoteSK) ) - // Construct the to-local witness script. - toLocalScript, err := input.CommitScriptToSelf( - csvDelay, toLocalPK, revPK, + var ( + toLocalScript, toLocalScriptHash []byte + toLocalCommitTree *input.CommitScriptTree ) - require.NoError(t, err) - // Compute the to-local witness script hash. - toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript) - require.NoError(t, err) + if isTaprootChannel { + toLocalCommitTree, err = input.NewLocalCommitScriptTree( + csvDelay, toLocalPK, revPK, + ) + require.NoError(t, err) + + toLocalScript = toLocalCommitTree.RevocationLeaf.Script + + toLocalScriptHash, err = input.PayToTaprootScript( + toLocalCommitTree.TaprootKey, + ) + require.NoError(t, err) + } else { + // Construct the to-local witness script. + toLocalScript, err = input.CommitScriptToSelf( + csvDelay, toLocalPK, revPK, + ) + require.NoError(t, err) + + // Compute the to-local witness script hash. + toLocalScriptHash, err = input.WitnessScriptHash(toLocalScript) + require.NoError(t, err) + } // Compute the to-remote redeem script, witness script hash, and // sequence numbers. @@ -140,8 +167,37 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { toRemoteRedeemScript []byte toRemoteScriptHash []byte toRemoteSigningScript []byte + toRemoteCtrlBlock []byte ) - if isAnchorChannel { + switch { + case isTaprootChannel: + toRemoteSequence = 1 + + commitScriptTree, err := input.NewRemoteCommitScriptTree( + toRemotePK, + ) + require.NoError(t, err) + + toRemoteSigningScript = commitScriptTree.SettleLeaf.Script + + toRemoteScriptHash, err = input.PayToTaprootScript( + commitScriptTree.TaprootKey, + ) + require.NoError(t, err) + + tree := commitScriptTree.TapscriptTree + settleTapleafHash := commitScriptTree.SettleLeaf.TapHash() + settleIdx := tree.LeafProofIndex[settleTapleafHash] + settleMerkleProof := tree.LeafMerkleProofs[settleIdx] + settleControlBlock := settleMerkleProof.ToControlBlock( + &input.TaprootNUMSKey, + ) + + ctrlBytes, err := settleControlBlock.ToBytes() + require.NoError(t, err) + toRemoteCtrlBlock = ctrlBytes + + case isAnchorChannel: toRemoteSequence = 1 toRemoteRedeemScript, err = input.CommitScriptToRemoteConfirmed( toRemotePK, @@ -155,7 +211,8 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { // As it should be. toRemoteSigningScript = toRemoteRedeemScript - } else { + + default: toRemoteRedeemScript = toRemotePK.SerializeCompressed() toRemoteScriptHash, err = input.CommitScriptUnencumbered( toRemotePK, @@ -269,31 +326,82 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { justiceTxn.TxOut = outputs txsort.InPlaceSort(justiceTxn) - hashCache := input.NewTxSigHashesV0Only(justiceTxn) + var ( + toLocalSignDesc *input.SignDescriptor + toRemoteSignDesc *input.SignDescriptor + ) - // Create the sign descriptor used to sign for the to-local input. - toLocalSignDesc := &input.SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - KeyLocator: revKeyLoc, - }, - WitnessScript: toLocalScript, - Output: breachTxn.TxOut[0], - SigHashes: hashCache, - InputIndex: 0, - HashType: txscript.SigHashAll, - } + if isTaprootChannel { + prevOuts := map[wire.OutPoint]*wire.TxOut{ + { + Hash: breachTxID, + Index: 0, + }: breachTxn.TxOut[0], + { + Hash: breachTxID, + Index: 1, + }: breachTxn.TxOut[1], + } + prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts) + hashCache := txscript.NewTxSigHashes( + justiceTxn, prevOutputFetcher, + ) - // Create the sign descriptor used to sign for the to-remote input. - toRemoteSignDesc := &input.SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - KeyLocator: toRemoteKeyLoc, - PubKey: toRemotePK, - }, - WitnessScript: toRemoteSigningScript, - Output: breachTxn.TxOut[1], - SigHashes: hashCache, - InputIndex: 1, - HashType: txscript.SigHashAll, + toLocalSignDesc = &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: revKeyLoc, + }, + WitnessScript: toLocalScript, + Output: breachTxn.TxOut[0], + SigHashes: hashCache, + PrevOutputFetcher: prevOutputFetcher, + InputIndex: 0, + HashType: txscript.SigHashDefault, + SignMethod: input.TaprootScriptSpendSignMethod, + } + + toRemoteSignDesc = &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: toRemoteKeyLoc, + PubKey: toRemotePK, + }, + WitnessScript: toRemoteSigningScript, + Output: breachTxn.TxOut[1], + PrevOutputFetcher: prevOutputFetcher, + SigHashes: hashCache, + InputIndex: 1, + HashType: txscript.SigHashDefault, + SignMethod: input.TaprootScriptSpendSignMethod, + } + } else { + hashCache := input.NewTxSigHashesV0Only(justiceTxn) + + // Create the sign descriptor used to sign for the to-local + // input. + toLocalSignDesc = &input.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 = &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: toRemoteKeyLoc, + PubKey: toRemotePK, + }, + WitnessScript: toRemoteSigningScript, + Output: breachTxn.TxOut[1], + SigHashes: hashCache, + InputIndex: 1, + HashType: txscript.SigHashAll, + } } // Verify that our test justice transaction is sane. @@ -301,21 +409,19 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { err = blockchain.CheckTransactionSanity(btx) require.Nil(t, err) - // Compute a DER-encoded signature for the to-local input. + // Compute a signature for the to-local input. toLocalSigRaw, err := signer.SignOutputRaw(justiceTxn, toLocalSignDesc) require.Nil(t, 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. + // Compute the witness for the to-remote input. toRemoteSigRaw, err := signer.SignOutputRaw(justiceTxn, toRemoteSignDesc) require.Nil(t, err) - // Convert the DER to-local sig into a fixed-size signature. + // Convert the to-local sig into a fixed-size signature. toLocalSig, err := lnwire.NewSigFromSignature(toLocalSigRaw) require.Nil(t, err) - // Convert the DER to-remote sig into a fixed-size signature. + // Convert the to-remote sig into a fixed-size signature. toRemoteSig, err := lnwire.NewSigFromSignature(toRemoteSigRaw) require.Nil(t, err) @@ -342,7 +448,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { // 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) - require.Nil(t, err) + require.NoError(t, err) // Retrieve the published justice transaction. var wtJusticeTxn *wire.MsgTx @@ -352,18 +458,59 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) { 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.Serialize(), - 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.Serialize(), - byte(txscript.SigHashAll)) - justiceTxn.TxIn[1].Witness[1] = toRemoteRedeemScript + if isTaprootChannel { + revokeLeaf := txscript.NewBaseTapLeaf(toLocalScript) + outputKey := txscript.ComputeTaprootOutputKey( + &input.TaprootNUMSKey, toLocalCommitTree.TapscriptRoot, + ) + + var outputKeyYIsOdd bool + if outputKey.SerializeCompressed()[0] == + secp.PubKeyFormatCompressedOdd { + + outputKeyYIsOdd = true + } + + delayScriptHash := toLocalCommitTree.SettleLeaf.TapHash() + + ctrlBlock := txscript.ControlBlock{ + InternalKey: &input.TaprootNUMSKey, + OutputKeyYIsOdd: outputKeyYIsOdd, + LeafVersion: revokeLeaf.LeafVersion, + InclusionProof: delayScriptHash[:], + } + + ctrlBytes, err := ctrlBlock.ToBytes() + require.NoError(t, err) + + justiceTxn.TxIn[0].Witness = make([][]byte, 3) + justiceTxn.TxIn[0].Witness[0] = toLocalSigRaw.Serialize() + justiceTxn.TxIn[0].Witness[1] = toLocalScript + justiceTxn.TxIn[0].Witness[2] = ctrlBytes + + // Construct the test's to-remote witness. + justiceTxn.TxIn[1].Witness = make([][]byte, 3) + justiceTxn.TxIn[1].Witness[0] = toRemoteSigRaw.Serialize() + justiceTxn.TxIn[1].Witness[1] = toRemoteSigningScript + justiceTxn.TxIn[1].Witness[2] = toRemoteCtrlBlock + } else { + // Construct the test's to-local witness. + justiceTxn.TxIn[0].Witness = make([][]byte, 3) + justiceTxn.TxIn[0].Witness[0] = append( + toLocalSigRaw.Serialize(), + 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.Serialize(), + byte(txscript.SigHashAll), + ) + justiceTxn.TxIn[1].Witness[1] = toRemoteRedeemScript + } // Assert that the watchtower derives the same justice txn. require.Equal(t, justiceTxn, wtJusticeTxn) diff --git a/watchtower/wtclient/backup_task.go b/watchtower/wtclient/backup_task.go index a458afecbd..44a82aacbf 100644 --- a/watchtower/wtclient/backup_task.go +++ b/watchtower/wtclient/backup_task.go @@ -213,12 +213,6 @@ func (t *backupTask) bindSession(session *wtdb.ClientSessionBody, } } - if chanType.HasAnchors() != session.Policy.IsAnchorChannel() { - log.Criticalf("Invalid task (has_anchors=%t) for session "+ - "(has_anchors=%t)", chanType.HasAnchors(), - session.Policy.IsAnchorChannel()) - } - // Now, compute the output values depending on whether FlagReward is set // in the current session's policy. outputs, err := session.Policy.ComputeJusticeTxOuts( @@ -334,6 +328,7 @@ func (t *backupTask) craftSessionPayload( switch inp.WitnessType() { case toLocalWitnessType: justiceKit.AddToLocalSig(signature) + case toRemoteWitnessType: justiceKit.AddToRemoteSig(signature) default: diff --git a/watchtower/wtclient/backup_task_internal_test.go b/watchtower/wtclient/backup_task_internal_test.go index 6604935eaf..d8c207c0fc 100644 --- a/watchtower/wtclient/backup_task_internal_test.go +++ b/watchtower/wtclient/backup_task_internal_test.go @@ -85,9 +85,11 @@ func genTaskTest( bindErr error, chanType channeldb.ChannelType) backupTaskTest { - // Set the anchor flag in the blob type if the session needs to support - // anchor channels. - if chanType.HasAnchors() { + // Set the anchor or taproot flag in the blob type if the session needs + // to support anchor or taproot channels. + if chanType.IsTaproot() { + blobType |= blob.Type(blob.FlagTaprootChannel) + } else if chanType.HasAnchors() { blobType |= blob.Type(blob.FlagAnchorChannel) } @@ -129,30 +131,112 @@ func genTaskTest( // to that output as local, though relative to their commitment, it is // paying to-the-remote party (which is us). if toLocalAmt > 0 { - toLocalSignDesc := &input.SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - KeyLocator: revKeyLoc, - PubKey: revPK, - }, - Output: &wire.TxOut{ - Value: toLocalAmt, - }, - HashType: txscript.SigHashAll, + var toLocalSignDesc *input.SignDescriptor + + if chanType.IsTaproot() { + scriptTree, _ := input.NewLocalCommitScriptTree( + csvDelay, toLocalPK, revPK, + ) + + pkScript, _ := input.PayToTaprootScript( + scriptTree.TaprootKey, + ) + + revokeTapleafHash := txscript.NewBaseTapLeaf( + scriptTree.RevocationLeaf.Script, + ).TapHash() + + tapTree := scriptTree.TapscriptTree + revokeIdx := tapTree.LeafProofIndex[revokeTapleafHash] + revokeMerkleProof := tapTree.LeafMerkleProofs[revokeIdx] + revokeControlBlock := revokeMerkleProof.ToControlBlock( + &input.TaprootNUMSKey, + ) + ctrlBytes, _ := revokeControlBlock.ToBytes() + + toLocalSignDesc = &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: revKeyLoc, + PubKey: revPK, + }, + Output: &wire.TxOut{ + Value: toLocalAmt, + PkScript: pkScript, + }, + WitnessScript: scriptTree.RevocationLeaf.Script, + SignMethod: input.TaprootScriptSpendSignMethod, //nolint:lll + HashType: txscript.SigHashDefault, + ControlBlock: ctrlBytes, + } + } else { + toLocalSignDesc = &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: revKeyLoc, + PubKey: revPK, + }, + Output: &wire.TxOut{ + Value: toLocalAmt, + }, + HashType: txscript.SigHashAll, + } } + breachInfo.RemoteOutputSignDesc = toLocalSignDesc breachTxn.AddTxOut(toLocalSignDesc.Output) } if toRemoteAmt > 0 { - toRemoteSignDesc := &input.SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - KeyLocator: toRemoteKeyLoc, - PubKey: toRemotePK, - }, - Output: &wire.TxOut{ - Value: toRemoteAmt, - }, - HashType: txscript.SigHashAll, + var toRemoteSignDesc *input.SignDescriptor + + if chanType.IsTaproot() { + scriptTree, _ := input.NewRemoteCommitScriptTree( + toRemotePK, + ) + + pkScript, _ := input.PayToTaprootScript( + scriptTree.TaprootKey, + ) + + revokeTapleafHash := txscript.NewBaseTapLeaf( + scriptTree.SettleLeaf.Script, + ).TapHash() + + tapTree := scriptTree.TapscriptTree + revokeIdx := tapTree.LeafProofIndex[revokeTapleafHash] + revokeMerkleProof := tapTree.LeafMerkleProofs[revokeIdx] + revokeControlBlock := revokeMerkleProof.ToControlBlock( + &input.TaprootNUMSKey, + ) + + ctrlBytes, _ := revokeControlBlock.ToBytes() + + toRemoteSignDesc = &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: toRemoteKeyLoc, + PubKey: toRemotePK, + }, + WitnessScript: scriptTree.SettleLeaf.Script, + SignMethod: input.TaprootScriptSpendSignMethod, //nolint:lll + Output: &wire.TxOut{ + Value: toRemoteAmt, + PkScript: pkScript, + }, + HashType: txscript.SigHashDefault, + InputIndex: 1, + ControlBlock: ctrlBytes, + } + } else { + toRemoteSignDesc = &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: toRemoteKeyLoc, + PubKey: toRemotePK, + }, + Output: &wire.TxOut{ + Value: toRemoteAmt, + }, + HashType: txscript.SigHashAll, + } } + breachInfo.LocalOutputSignDesc = toRemoteSignDesc breachTxn.AddTxOut(toRemoteSignDesc.Output) } @@ -248,6 +332,7 @@ func TestBackupTask(t *testing.T) { channeldb.SingleFunderBit, channeldb.SingleFunderTweaklessBit, channeldb.AnchorOutputsBit, + channeldb.SimpleTaprootFeatureBit, } var backupTaskTests []backupTaskTest @@ -272,7 +357,16 @@ func TestBackupTask(t *testing.T) { sweepFeeRateNoRewardRemoteDust chainfee.SatPerKWeight = 227500 sweepFeeRateRewardRemoteDust chainfee.SatPerKWeight = 175350 ) - if chanType.HasAnchors() { + if chanType.IsTaproot() { + expSweepCommitNoRewardBoth = 299165 + expSweepCommitNoRewardLocal = 199468 + expSweepCommitNoRewardRemote = 99531 + sweepFeeRateNoRewardRemoteDust = 213200 + expSweepCommitRewardBoth = 295993 + expSweepCommitRewardLocal = 197296 + expSweepCommitRewardRemote = 98359 + sweepFeeRateRewardRemoteDust = 167000 + } else if chanType.HasAnchors() { expSweepCommitNoRewardBoth = 299236 expSweepCommitNoRewardLocal = 199513 expSweepCommitNoRewardRemote = 99557 diff --git a/watchtower/wtclient/client_test.go b/watchtower/wtclient/client_test.go index c2380dbffd..81674f0c01 100644 --- a/watchtower/wtclient/client_test.go +++ b/watchtower/wtclient/client_test.go @@ -76,12 +76,12 @@ var ( waitTime = 15 * time.Second defaultTxPolicy = wtpolicy.TxPolicy{ - BlobType: blob.TypeAltruistCommit, + BlobType: blob.TypeAltruistTaprootCommit, SweepFeeRate: wtpolicy.DefaultSweepFeeRate, } highSweepRateTxPolicy = wtpolicy.TxPolicy{ - BlobType: blob.TypeAltruistCommit, + BlobType: blob.TypeAltruistTaprootCommit, SweepFeeRate: 1000000, // The high sweep fee creates dust. } ) @@ -229,17 +229,25 @@ func (c *mockChannel) createRemoteCommitTx(t *testing.T) { t.Helper() // Construct the to-local witness script. - toLocalScript, err := input.CommitScriptToSelf( + toLocalScriptTree, err := input.NewLocalCommitScriptTree( c.csvDelay, c.toLocalPK, c.revPK, ) require.NoError(t, err, "unable to create to-local script") + // Construct the to-remote witness script. + toRemoteScriptTree, err := input.NewRemoteCommitScriptTree(c.toRemotePK) + require.NoError(t, err, "unable to create to-remote script") + // Compute the to-local witness script hash. - toLocalScriptHash, err := input.WitnessScriptHash(toLocalScript) + toLocalScriptHash, err := input.PayToTaprootScript( + toLocalScriptTree.TaprootKey, + ) require.NoError(t, err, "unable to create to-local witness script hash") // Compute the to-remote witness script hash. - toRemoteScriptHash, err := input.CommitScriptUnencumbered(c.toRemotePK) + toRemoteScriptHash, err := input.PayToTaprootScript( + toRemoteScriptTree.TaprootKey, + ) require.NoError(t, err, "unable to create to-remote script") // Construct the remote commitment txn, containing the to-local and @@ -264,6 +272,19 @@ func (c *mockChannel) createRemoteCommitTx(t *testing.T) { PkScript: toLocalScriptHash, }) + revokeTapleafHash := txscript.NewBaseTapLeaf( + toLocalScriptTree.RevocationLeaf.Script, + ).TapHash() + tapTree := toLocalScriptTree.TapscriptTree + revokeIdx := tapTree.LeafProofIndex[revokeTapleafHash] + revokeMerkleProof := tapTree.LeafMerkleProofs[revokeIdx] + revokeControlBlock := revokeMerkleProof.ToControlBlock( + &input.TaprootNUMSKey, + ) + + ctrlBytes, err := revokeControlBlock.ToBytes() + require.NoError(t, err) + // Create the sign descriptor used to sign for the to-local // input. toLocalSignDesc = &input.SignDescriptor{ @@ -271,9 +292,11 @@ func (c *mockChannel) createRemoteCommitTx(t *testing.T) { KeyLocator: c.revKeyLoc, PubKey: c.revPK, }, - WitnessScript: toLocalScript, + WitnessScript: toLocalScriptTree.RevocationLeaf.Script, Output: commitTxn.TxOut[outputIndex], - HashType: txscript.SigHashAll, + HashType: txscript.SigHashDefault, + SignMethod: input.TaprootScriptSpendSignMethod, + ControlBlock: ctrlBytes, } outputIndex++ } @@ -283,6 +306,18 @@ func (c *mockChannel) createRemoteCommitTx(t *testing.T) { PkScript: toRemoteScriptHash, }) + toRemoteTapleafHash := txscript.NewBaseTapLeaf( + toRemoteScriptTree.SettleLeaf.Script, + ).TapHash() + tapTree := toRemoteScriptTree.TapscriptTree + remoteIdx := tapTree.LeafProofIndex[toRemoteTapleafHash] + remoteMerkleProof := tapTree.LeafMerkleProofs[remoteIdx] + remoteControlBlock := remoteMerkleProof.ToControlBlock( + &input.TaprootNUMSKey, + ) + + ctrlBytes, _ := remoteControlBlock.ToBytes() + // Create the sign descriptor used to sign for the to-remote // input. toRemoteSignDesc = &input.SignDescriptor{ @@ -290,9 +325,11 @@ func (c *mockChannel) createRemoteCommitTx(t *testing.T) { KeyLocator: c.toRemoteKeyLoc, PubKey: c.toRemotePK, }, - WitnessScript: toRemoteScriptHash, + WitnessScript: toRemoteScriptTree.SettleLeaf.Script, Output: commitTxn.TxOut[outputIndex], - HashType: txscript.SigHashAll, + HashType: txscript.SigHashDefault, + SignMethod: input.TaprootScriptSpendSignMethod, + ControlBlock: ctrlBytes, } outputIndex++ } @@ -516,7 +553,7 @@ func newHarness(t *testing.T, cfg harnessCfg) *testHarness { _, retribution := h.channelFromID(id).getState(commitHeight) - return retribution, channeldb.SingleFunderBit, nil + return retribution, channeldb.SimpleTaprootFeatureBit, nil } if !cfg.noServerStart { @@ -664,7 +701,9 @@ func (h *testHarness) registerChannel(id uint64) { h.t.Helper() chanID := chanIDFromInt(id) - err := h.clientMgr.RegisterChannel(chanID, channeldb.SingleFunderBit) + err := h.clientMgr.RegisterChannel( + chanID, channeldb.SimpleTaprootFeatureBit, + ) require.NoError(h.t, err) } @@ -1404,7 +1443,7 @@ var clientTests = []clientTest{ // Wait for all the updates to be populated in the // server's database. - h.server.waitForUpdates(hints, 10*time.Second) + h.server.waitForUpdates(hints, waitTime) }, }, { diff --git a/watchtower/wtclient/manager.go b/watchtower/wtclient/manager.go index 73f259085d..70f2a05428 100644 --- a/watchtower/wtclient/manager.go +++ b/watchtower/wtclient/manager.go @@ -523,10 +523,7 @@ func (m *Manager) Policy(blobType blob.Type) (wtpolicy.Policy, error) { func (m *Manager) RegisterChannel(id lnwire.ChannelID, chanType channeldb.ChannelType) error { - blobType := blob.TypeAltruistCommit - if chanType.HasAnchors() { - blobType = blob.TypeAltruistAnchorCommit - } + blobType := blob.TypeFromChannel(chanType) m.clientsMu.Lock() if _, ok := m.clients[blobType]; !ok { diff --git a/watchtower/wtclient/session_negotiator.go b/watchtower/wtclient/session_negotiator.go index 2b1e988a1d..ad359233ef 100644 --- a/watchtower/wtclient/session_negotiator.go +++ b/watchtower/wtclient/session_negotiator.go @@ -120,15 +120,8 @@ var _ SessionNegotiator = (*sessionNegotiator)(nil) // newSessionNegotiator initializes a fresh sessionNegotiator instance. func newSessionNegotiator(cfg *NegotiatorConfig) *sessionNegotiator { // Generate the set of features the negotiator will present to the tower - // upon connection. For anchor channels, we'll conditionally signal that - // we require support for anchor channels depending on the requested - // policy. - features := []lnwire.FeatureBit{ - wtwire.AltruistSessionsRequired, - } - if cfg.Policy.IsAnchorChannel() { - features = append(features, wtwire.AnchorCommitRequired) - } + // upon connection. + features := cfg.Policy.FeatureBits() localInit := wtwire.NewInitMessage( lnwire.NewRawFeatureVector(features...), diff --git a/watchtower/wtmock/signer.go b/watchtower/wtmock/signer.go index 1cf1f56646..af3ebe58d2 100644 --- a/watchtower/wtmock/signer.go +++ b/watchtower/wtmock/signer.go @@ -2,6 +2,7 @@ package wtmock import ( "crypto/sha256" + "fmt" "sync" "github.com/btcsuite/btcd/btcec/v2" @@ -47,6 +48,69 @@ func (s *MockSigner) SignOutputRaw(tx *wire.MsgTx, panic("cannot sign w/ unknown key") } + // In case of a taproot output any signature is always a Schnorr + // signature, based on the new tapscript sighash algorithm. + if txscript.IsPayToTaproot(signDesc.Output.PkScript) { + sigHashes := txscript.NewTxSigHashes( + tx, signDesc.PrevOutputFetcher, + ) + + // Are we spending a script path or the key path? The API is + // slightly different, so we need to account for that to get the + // raw signature. + var ( + rawSig []byte + err error + ) + switch signDesc.SignMethod { + case input.TaprootKeySpendBIP0086SignMethod, + input.TaprootKeySpendSignMethod: + + // This function tweaks the private key using the tap + // root key supplied as the tweak. + rawSig, err = txscript.RawTxInTaprootSignature( + tx, sigHashes, signDesc.InputIndex, + signDesc.Output.Value, signDesc.Output.PkScript, + signDesc.TapTweak, signDesc.HashType, + privKey, + ) + if err != nil { + return nil, err + } + + case input.TaprootScriptSpendSignMethod: + leaf := txscript.TapLeaf{ + LeafVersion: txscript.BaseLeafVersion, + Script: witnessScript, + } + rawSig, err = txscript.RawTxInTapscriptSignature( + tx, sigHashes, signDesc.InputIndex, + signDesc.Output.Value, signDesc.Output.PkScript, + leaf, signDesc.HashType, privKey, + ) + if err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unknown sign method: %v", + signDesc.SignMethod) + } + + // The signature returned above might have a sighash flag + // attached if a non-default type was used. We'll slice this + // off if it exists to ensure we can properly parse the raw + // signature. + sig, err := schnorr.ParseSignature( + rawSig[:schnorr.SignatureSize], + ) + if err != nil { + return nil, err + } + + return sig, nil + } + sig, err := txscript.RawTxInWitnessSignature( tx, signDesc.SigHashes, signDesc.InputIndex, amt, witnessScript, signDesc.HashType, privKey, diff --git a/watchtower/wtpolicy/policy.go b/watchtower/wtpolicy/policy.go index 429f1ffb6c..9d5763fb2f 100644 --- a/watchtower/wtpolicy/policy.go +++ b/watchtower/wtpolicy/policy.go @@ -8,7 +8,9 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/watchtower/blob" + "github.com/lightningnetwork/lnd/watchtower/wtwire" ) const ( @@ -120,14 +122,38 @@ func (p Policy) String() string { p.SweepFeeRate) } +// FeatureBits returns the watchtower feature bits required for the given +// policy. +func (p *Policy) FeatureBits() []lnwire.FeatureBit { + features := []lnwire.FeatureBit{ + wtwire.AltruistSessionsRequired, + } + + t := p.TxPolicy.BlobType + switch { + case t.IsTaprootChannel(): + features = append(features, wtwire.TaprootCommitRequired) + case t.IsAnchorChannel(): + features = append(features, wtwire.AnchorCommitRequired) + } + + return features +} + // IsAnchorChannel returns true if the session policy requires anchor channels. -func (p Policy) IsAnchorChannel() bool { +func (p *Policy) IsAnchorChannel() bool { return p.TxPolicy.BlobType.IsAnchorChannel() } +// IsTaprootChannel returns true if the session policy requires taproot +// channels. +func (p *Policy) IsTaprootChannel() bool { + return p.TxPolicy.BlobType.IsTaprootChannel() +} + // Validate ensures that the policy satisfies some minimal correctness // constraints. -func (p Policy) Validate() error { +func (p *Policy) Validate() error { // RewardBase and RewardRate should not be set if the policy doesn't // have a reward. if !p.BlobType.Has(blob.FlagReward) && diff --git a/watchtower/wtpolicy/policy_test.go b/watchtower/wtpolicy/policy_test.go index b73c484567..b3587640dd 100644 --- a/watchtower/wtpolicy/policy_test.go +++ b/watchtower/wtpolicy/policy_test.go @@ -93,20 +93,32 @@ func TestPolicyValidate(t *testing.T) { } } -// TestPolicyIsAnchorChannel asserts that the IsAnchorChannel helper properly -// reflects the anchor bit of the policy's blob type. -func TestPolicyIsAnchorChannel(t *testing.T) { - policyNoAnchor := wtpolicy.Policy{ +// TestPolicyIsChannelType asserts that the IsAnchorChannel and IsTaprootChannel +// helpers properly reflect the anchor bit of the policy's blob type. +func TestPolicyIsChannelType(t *testing.T) { + t.Parallel() + + policyLegacy := wtpolicy.Policy{ TxPolicy: wtpolicy.TxPolicy{ BlobType: blob.TypeAltruistCommit, }, } - require.Equal(t, false, policyNoAnchor.IsAnchorChannel()) + require.False(t, policyLegacy.IsAnchorChannel()) + require.False(t, policyLegacy.IsTaprootChannel()) policyAnchor := wtpolicy.Policy{ TxPolicy: wtpolicy.TxPolicy{ BlobType: blob.TypeAltruistAnchorCommit, }, } - require.Equal(t, true, policyAnchor.IsAnchorChannel()) + require.True(t, policyAnchor.IsAnchorChannel()) + require.False(t, policyAnchor.IsTaprootChannel()) + + policyTaproot := wtpolicy.Policy{ + TxPolicy: wtpolicy.TxPolicy{ + BlobType: blob.TypeAltruistTaprootCommit, + }, + } + require.True(t, policyTaproot.IsTaprootChannel()) + require.False(t, policyTaproot.IsAnchorChannel()) } diff --git a/watchtower/wtwire/features.go b/watchtower/wtwire/features.go index 83ab207f71..413e81c1ae 100644 --- a/watchtower/wtwire/features.go +++ b/watchtower/wtwire/features.go @@ -9,6 +9,8 @@ var FeatureNames = map[lnwire.FeatureBit]string{ AltruistSessionsOptional: "altruist-sessions", AnchorCommitRequired: "anchor-commit", AnchorCommitOptional: "anchor-commit", + TaprootCommitRequired: "taproot-commit", + TaprootCommitOptional: "taproot-commit", } const ( @@ -30,4 +32,13 @@ const ( // AnchorCommitOptional specifies that the advertising tower allows the // remote party to negotiate sessions for protecting anchor channels. AnchorCommitOptional lnwire.FeatureBit = 3 + + // TaprootCommitRequired specifies that the advertising tower requires + // the remote party to negotiate sessions for protecting taproot + // channels. + TaprootCommitRequired lnwire.FeatureBit = 4 + + // TaprootCommitOptional specifies that the advertising tower allows the + // remote party to negotiate sessions for protecting taproot channels. + TaprootCommitOptional lnwire.FeatureBit = 5 )