Skip to content

Commit be09de8

Browse files
committed
add package scbforceclose
It provides function SignCloseTx which produces a signed force close transaction from a channel backup and private key material.
1 parent 63e5b86 commit be09de8

File tree

3 files changed

+352
-0
lines changed

3 files changed

+352
-0
lines changed

scbforceclose/sign_close_tx.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package scbforceclose
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcd/btcec/v2"
8+
"github.com/btcsuite/btcd/btcutil"
9+
"github.com/btcsuite/btcd/chaincfg/chainhash"
10+
"github.com/btcsuite/btcd/txscript"
11+
"github.com/btcsuite/btcd/wire"
12+
"github.com/lightningnetwork/lnd/chanbackup"
13+
"github.com/lightningnetwork/lnd/channeldb"
14+
"github.com/lightningnetwork/lnd/fn/v2"
15+
"github.com/lightningnetwork/lnd/input"
16+
"github.com/lightningnetwork/lnd/keychain"
17+
"github.com/lightningnetwork/lnd/lnwallet"
18+
"github.com/lightningnetwork/lnd/shachain"
19+
)
20+
21+
// SignCloseTx produces a signed commit tx from a channel backup.
22+
func SignCloseTx(s chanbackup.Single, keyRing keychain.KeyRing,
23+
ecdher keychain.ECDHRing, signer input.Signer) (*wire.MsgTx, error) {
24+
25+
var errNoInputs = errors.New("channel backup does not have data " +
26+
"needed to sign force close tx")
27+
28+
closeTxInputs, err := s.CloseTxInputs.UnwrapOrErr(errNoInputs)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
// Each of the keys in our local channel config only have their
34+
// locators populated, so we'll re-derive the raw key now.
35+
localMultiSigKey, err := keyRing.DeriveKey(
36+
s.LocalChanCfg.MultiSigKey.KeyLocator,
37+
)
38+
if err != nil {
39+
return nil, fmt.Errorf("unable to derive multisig key: %w", err)
40+
}
41+
42+
// Determine the value of tapscriptRoot option.
43+
tapscriptRootOpt := fn.None[chainhash.Hash]()
44+
if s.Version.HasTapscriptRoot() {
45+
tapscriptRootOpt = closeTxInputs.TapscriptRoot
46+
}
47+
48+
// Create signature descriptor.
49+
signDesc, err := createSignDesc(
50+
localMultiSigKey, s.RemoteChanCfg.MultiSigKey.PubKey,
51+
s.Version, s.Capacity, tapscriptRootOpt,
52+
)
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to create signDesc: %w", err)
55+
}
56+
57+
// Build inputs for GetSignedCommitTx.
58+
inputs := lnwallet.SignedCommitTxInputs{
59+
CommitTx: closeTxInputs.CommitTx,
60+
CommitSig: closeTxInputs.CommitSig,
61+
OurKey: localMultiSigKey,
62+
TheirKey: s.RemoteChanCfg.MultiSigKey,
63+
SignDesc: signDesc,
64+
}
65+
66+
// Add special fields in case of a taproot channel.
67+
if s.Version.IsTaproot() {
68+
producer, err := createTaprootNonceProducer(
69+
s.ShaChainRootDesc, localMultiSigKey.PubKey, ecdher,
70+
)
71+
if err != nil {
72+
return nil, err
73+
}
74+
inputs.Taproot = fn.Some(lnwallet.TaprootSignedCommitTxInputs{
75+
CommitHeight: closeTxInputs.CommitHeight,
76+
TaprootNonceProducer: producer,
77+
TapscriptRoot: tapscriptRootOpt,
78+
})
79+
}
80+
81+
return lnwallet.GetSignedCommitTx(inputs, signer)
82+
}
83+
84+
// createSignDesc creates SignDescriptor from local and remote keys,
85+
// backup version and capacity.
86+
// See LightningChannel.createSignDesc on how signDesc is produced.
87+
func createSignDesc(localMultiSigKey keychain.KeyDescriptor,
88+
remoteKey *btcec.PublicKey, version chanbackup.SingleBackupVersion,
89+
capacity btcutil.Amount, tapscriptRoot fn.Option[chainhash.Hash]) (
90+
*input.SignDescriptor, error) {
91+
92+
var fundingPkScript, multiSigScript []byte
93+
94+
localKey := localMultiSigKey.PubKey
95+
96+
var err error
97+
if version.IsTaproot() {
98+
fundingPkScript, _, err = input.GenTaprootFundingScript(
99+
localKey, remoteKey, int64(capacity), tapscriptRoot,
100+
)
101+
if err != nil {
102+
return nil, err
103+
}
104+
} else {
105+
multiSigScript, err = input.GenMultiSigScript(
106+
localKey.SerializeCompressed(),
107+
remoteKey.SerializeCompressed(),
108+
)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
fundingPkScript, err = input.WitnessScriptHash(multiSigScript)
114+
if err != nil {
115+
return nil, err
116+
}
117+
}
118+
119+
return &input.SignDescriptor{
120+
KeyDesc: localMultiSigKey,
121+
WitnessScript: multiSigScript,
122+
Output: &wire.TxOut{
123+
PkScript: fundingPkScript,
124+
Value: int64(capacity),
125+
},
126+
HashType: txscript.SigHashAll,
127+
PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher(
128+
fundingPkScript, int64(capacity),
129+
),
130+
InputIndex: 0,
131+
}, nil
132+
}
133+
134+
// createTaprootNonceProducer makes taproot nonce producer from a
135+
// ShaChainRootDesc and our public multisig key.
136+
func createTaprootNonceProducer(shaChainRootDesc keychain.KeyDescriptor,
137+
localKey *btcec.PublicKey, ecdher keychain.ECDHRing) (shachain.Producer,
138+
error) {
139+
140+
if shaChainRootDesc.PubKey != nil {
141+
return nil, errors.New("taproot channels always use ECDH, " +
142+
"but legacy ShaChainRootDesc with pubkey found")
143+
}
144+
145+
// This is the scheme in which the shachain root is derived via an ECDH
146+
// operation on the private key of ShaChainRootDesc and our public
147+
// multisig key.
148+
ecdh, err := ecdher.ECDH(shaChainRootDesc, localKey)
149+
if err != nil {
150+
return nil, fmt.Errorf("ecdh failed: %w", err)
151+
}
152+
153+
// The shachain root that seeds RevocationProducer for this channel.
154+
revRoot := chainhash.Hash(ecdh)
155+
156+
revocationProducer := shachain.NewRevocationProducer(revRoot)
157+
158+
return channeldb.DeriveMusig2Shachain(revocationProducer)
159+
}

scbforceclose/sign_close_tx_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package scbforceclose
2+
3+
import (
4+
"bytes"
5+
_ "embed"
6+
"encoding/hex"
7+
"encoding/json"
8+
"strings"
9+
"testing"
10+
11+
"github.com/btcsuite/btcd/btcutil/hdkeychain"
12+
"github.com/btcsuite/btcd/chaincfg"
13+
"github.com/btcsuite/btcd/txscript"
14+
"github.com/lightninglabs/chantools/lnd"
15+
"github.com/lightningnetwork/lnd/aezeed"
16+
"github.com/lightningnetwork/lnd/chanbackup"
17+
"github.com/lightningnetwork/lnd/input"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
//go:embed testdata/channel_backups.json
22+
var channelBackupsJSON []byte
23+
24+
// TestSignCloseTx tests that SignCloseTx produces valid transactions.
25+
func TestSignCloseTx(t *testing.T) {
26+
// Load prepared channel backups with seeds and passwords.
27+
type TestCase struct {
28+
Name string `json:"name"`
29+
RootKey string `json:"rootkey"`
30+
Password string `json:"password"`
31+
Mnemonic string `json:"mnemonic"`
32+
Single bool `json:"single"`
33+
ChannelBackup string `json:"channel_backup"`
34+
PkScript string `json:"pk_script"`
35+
AmountSats int64 `json:"amount_sats"`
36+
}
37+
38+
var testdata struct {
39+
Cases []TestCase `json:"cases"`
40+
}
41+
require.NoError(t, json.Unmarshal(channelBackupsJSON, &testdata))
42+
43+
chainParams := &chaincfg.RegressionNetParams
44+
45+
for _, tc := range testdata.Cases {
46+
t.Run(tc.Name, func(t *testing.T) {
47+
var extendedKey *hdkeychain.ExtendedKey
48+
if tc.RootKey != "" {
49+
// Parse root key.
50+
var err error
51+
extendedKey, err = hdkeychain.NewKeyFromString(
52+
tc.RootKey,
53+
)
54+
require.NoError(t, err)
55+
} else {
56+
// Generate root key from seed and password.
57+
words := strings.Split(tc.Mnemonic, " ")
58+
require.Len(t, words, 24)
59+
var mnemonic aezeed.Mnemonic
60+
copy(mnemonic[:], words)
61+
cipherSeed, err := mnemonic.ToCipherSeed(
62+
[]byte(tc.Password),
63+
)
64+
require.NoError(t, err)
65+
extendedKey, err = hdkeychain.NewMaster(
66+
cipherSeed.Entropy[:], chainParams,
67+
)
68+
require.NoError(t, err)
69+
}
70+
71+
// Make key ring and signer.
72+
keyRing := &lnd.HDKeyRing{
73+
ExtendedKey: extendedKey,
74+
ChainParams: chainParams,
75+
}
76+
77+
signer := &lnd.Signer{
78+
ExtendedKey: extendedKey,
79+
ChainParams: chainParams,
80+
}
81+
musigSessionManager := input.NewMusigSessionManager(
82+
signer.FetchPrivateKey,
83+
)
84+
signer.MusigSessionManager = musigSessionManager
85+
86+
// Unpack channel.backup.
87+
backup, err := hex.DecodeString(
88+
tc.ChannelBackup,
89+
)
90+
require.NoError(t, err)
91+
r := bytes.NewReader(backup)
92+
93+
var s chanbackup.Single
94+
if tc.Single {
95+
err := s.UnpackFromReader(r, keyRing)
96+
require.NoError(t, err)
97+
} else {
98+
var m chanbackup.Multi
99+
err := m.UnpackFromReader(r, keyRing)
100+
require.NoError(t, err)
101+
102+
// Extract a single channel backup from
103+
// multi backup.
104+
require.Len(t, m.StaticBackups, 1)
105+
s = m.StaticBackups[0]
106+
}
107+
108+
// Sign force close transaction.
109+
sweepTx, err := SignCloseTx(
110+
s, keyRing, signer, signer,
111+
)
112+
require.NoError(t, err)
113+
114+
// Check if the transaction is valid.
115+
pkScript, err := hex.DecodeString(tc.PkScript)
116+
require.NoError(t, err)
117+
fetcher := txscript.NewCannedPrevOutputFetcher(
118+
pkScript, tc.AmountSats,
119+
)
120+
121+
sigHashes := txscript.NewTxSigHashes(sweepTx, fetcher)
122+
123+
vm, err := txscript.NewEngine(
124+
pkScript, sweepTx, 0,
125+
txscript.StandardVerifyFlags,
126+
nil, sigHashes, tc.AmountSats, fetcher,
127+
)
128+
require.NoError(t, err)
129+
130+
require.NoError(t, vm.Execute())
131+
})
132+
}
133+
}

0 commit comments

Comments
 (0)