-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
It provides function SignCloseTx which produces a signed force close transaction from a channel backup and private key material.
- Loading branch information
Showing
3 changed files
with
352 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package scbforceclose | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/btcsuite/btcd/btcec/v2" | ||
"github.com/btcsuite/btcd/btcutil" | ||
"github.com/btcsuite/btcd/chaincfg/chainhash" | ||
"github.com/btcsuite/btcd/txscript" | ||
"github.com/btcsuite/btcd/wire" | ||
"github.com/lightningnetwork/lnd/chanbackup" | ||
"github.com/lightningnetwork/lnd/channeldb" | ||
"github.com/lightningnetwork/lnd/fn" | ||
"github.com/lightningnetwork/lnd/input" | ||
"github.com/lightningnetwork/lnd/keychain" | ||
"github.com/lightningnetwork/lnd/lnwallet" | ||
"github.com/lightningnetwork/lnd/shachain" | ||
) | ||
|
||
// SignCloseTx produces a signed commit tx from a channel backup. | ||
func SignCloseTx(s chanbackup.Single, keyRing keychain.KeyRing, | ||
ecdher keychain.ECDHRing, signer input.Signer) (*wire.MsgTx, error) { | ||
|
||
var errNoInputs = errors.New("channel backup does not have data " + | ||
"needed to sign force close tx") | ||
|
||
closeTxInputs, err := s.CloseTxInputs.UnwrapOrErr(errNoInputs) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Each of the keys in our local channel config only have their | ||
// locators populated, so we'll re-derive the raw key now. | ||
localMultiSigKey, err := keyRing.DeriveKey( | ||
s.LocalChanCfg.MultiSigKey.KeyLocator, | ||
) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to derive multisig key: %w", err) | ||
} | ||
|
||
// Determine the value of tapscriptRoot option. | ||
tapscriptRootOpt := fn.None[chainhash.Hash]() | ||
if s.Version.HasTapscriptRoot() { | ||
tapscriptRootOpt = closeTxInputs.TapscriptRoot | ||
} | ||
|
||
// Create signature descriptor. | ||
signDesc, err := createSignDesc( | ||
localMultiSigKey, s.RemoteChanCfg.MultiSigKey.PubKey, | ||
s.Version, s.Capacity, tapscriptRootOpt, | ||
) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create signDesc: %w", err) | ||
} | ||
|
||
// Build inputs for GetSignedCommitTx. | ||
inputs := lnwallet.SignedCommitTxInputs{ | ||
CommitTx: closeTxInputs.CommitTx, | ||
CommitSig: closeTxInputs.CommitSig, | ||
OurKey: localMultiSigKey, | ||
TheirKey: s.RemoteChanCfg.MultiSigKey, | ||
SignDesc: signDesc, | ||
} | ||
|
||
// Add special fields in case of a taproot channel. | ||
if s.Version.IsTaproot() { | ||
producer, err := createTaprootNonceProducer( | ||
s.ShaChainRootDesc, localMultiSigKey.PubKey, ecdher, | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
inputs.Taproot = fn.Some(lnwallet.TaprootSignedCommitTxInputs{ | ||
CommitHeight: closeTxInputs.CommitHeight, | ||
TaprootNonceProducer: producer, | ||
TapscriptRoot: tapscriptRootOpt, | ||
}) | ||
} | ||
|
||
return lnwallet.GetSignedCommitTx(inputs, signer) | ||
} | ||
|
||
// createSignDesc creates SignDescriptor from local and remote keys, | ||
// backup version and capacity. | ||
// See LightningChannel.createSignDesc on how signDesc is produced. | ||
func createSignDesc(localMultiSigKey keychain.KeyDescriptor, | ||
remoteKey *btcec.PublicKey, version chanbackup.SingleBackupVersion, | ||
capacity btcutil.Amount, tapscriptRoot fn.Option[chainhash.Hash]) ( | ||
*input.SignDescriptor, error) { | ||
|
||
var fundingPkScript, multiSigScript []byte | ||
|
||
localKey := localMultiSigKey.PubKey | ||
|
||
var err error | ||
if version.IsTaproot() { | ||
fundingPkScript, _, err = input.GenTaprootFundingScript( | ||
localKey, remoteKey, int64(capacity), tapscriptRoot, | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} else { | ||
multiSigScript, err = input.GenMultiSigScript( | ||
localKey.SerializeCompressed(), | ||
remoteKey.SerializeCompressed(), | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
fundingPkScript, err = input.WitnessScriptHash(multiSigScript) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return &input.SignDescriptor{ | ||
KeyDesc: localMultiSigKey, | ||
WitnessScript: multiSigScript, | ||
Output: &wire.TxOut{ | ||
PkScript: fundingPkScript, | ||
Value: int64(capacity), | ||
}, | ||
HashType: txscript.SigHashAll, | ||
PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher( | ||
fundingPkScript, int64(capacity), | ||
), | ||
InputIndex: 0, | ||
}, nil | ||
} | ||
|
||
// createTaprootNonceProducer makes taproot nonce producer from a | ||
// ShaChainRootDesc and our public multisig key. | ||
func createTaprootNonceProducer(shaChainRootDesc keychain.KeyDescriptor, | ||
localKey *btcec.PublicKey, ecdher keychain.ECDHRing) (shachain.Producer, | ||
error) { | ||
|
||
if shaChainRootDesc.PubKey != nil { | ||
return nil, errors.New("taproot channels always use ECDH, " + | ||
"but legacy ShaChainRootDesc with pubkey found") | ||
} | ||
|
||
// This is the scheme in which the shachain root is derived via an ECDH | ||
// operation on the private key of ShaChainRootDesc and our public | ||
// multisig key. | ||
ecdh, err := ecdher.ECDH(shaChainRootDesc, localKey) | ||
if err != nil { | ||
return nil, fmt.Errorf("ecdh failed: %w", err) | ||
} | ||
|
||
// The shachain root that seeds RevocationProducer for this channel. | ||
revRoot := chainhash.Hash(ecdh) | ||
|
||
revocationProducer := shachain.NewRevocationProducer(revRoot) | ||
|
||
return channeldb.DeriveMusig2Shachain(revocationProducer) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package scbforceclose | ||
|
||
import ( | ||
"bytes" | ||
_ "embed" | ||
"encoding/hex" | ||
"encoding/json" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/btcsuite/btcd/btcutil/hdkeychain" | ||
"github.com/btcsuite/btcd/chaincfg" | ||
"github.com/btcsuite/btcd/txscript" | ||
"github.com/lightninglabs/chantools/lnd" | ||
"github.com/lightningnetwork/lnd/aezeed" | ||
"github.com/lightningnetwork/lnd/chanbackup" | ||
"github.com/lightningnetwork/lnd/input" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
//go:embed testdata/channel_backups.json | ||
var channelBackupsJSON []byte | ||
|
||
// TestSignCloseTx tests that SignCloseTx produces valid transactions. | ||
func TestSignCloseTx(t *testing.T) { | ||
// Load prepared channel backups with seeds and passwords. | ||
type TestCase struct { | ||
Name string `json:"name"` | ||
RootKey string `json:"rootkey"` | ||
Password string `json:"password"` | ||
Mnemonic string `json:"mnemonic"` | ||
Single bool `json:"single"` | ||
ChannelBackup string `json:"channel_backup"` | ||
PkScript string `json:"pk_script"` | ||
AmountSats int64 `json:"amount_sats"` | ||
} | ||
|
||
var testdata struct { | ||
Cases []TestCase `json:"cases"` | ||
} | ||
require.NoError(t, json.Unmarshal(channelBackupsJSON, &testdata)) | ||
|
||
chainParams := &chaincfg.RegressionNetParams | ||
|
||
for _, tc := range testdata.Cases { | ||
t.Run(tc.Name, func(t *testing.T) { | ||
var extendedKey *hdkeychain.ExtendedKey | ||
if tc.RootKey != "" { | ||
// Parse root key. | ||
var err error | ||
extendedKey, err = hdkeychain.NewKeyFromString( | ||
tc.RootKey, | ||
) | ||
require.NoError(t, err) | ||
} else { | ||
// Generate root key from seed and password. | ||
words := strings.Split(tc.Mnemonic, " ") | ||
require.Len(t, words, 24) | ||
var mnemonic aezeed.Mnemonic | ||
copy(mnemonic[:], words) | ||
cipherSeed, err := mnemonic.ToCipherSeed( | ||
[]byte(tc.Password), | ||
) | ||
require.NoError(t, err) | ||
extendedKey, err = hdkeychain.NewMaster( | ||
cipherSeed.Entropy[:], chainParams, | ||
) | ||
require.NoError(t, err) | ||
} | ||
|
||
// Make key ring and signer. | ||
keyRing := &lnd.HDKeyRing{ | ||
ExtendedKey: extendedKey, | ||
ChainParams: chainParams, | ||
} | ||
|
||
signer := &lnd.Signer{ | ||
ExtendedKey: extendedKey, | ||
ChainParams: chainParams, | ||
} | ||
musigSessionManager := input.NewMusigSessionManager( | ||
signer.FetchPrivateKey, | ||
) | ||
signer.MusigSessionManager = musigSessionManager | ||
|
||
// Unpack channel.backup. | ||
backup, err := hex.DecodeString( | ||
tc.ChannelBackup, | ||
) | ||
require.NoError(t, err) | ||
r := bytes.NewReader(backup) | ||
|
||
var s chanbackup.Single | ||
if tc.Single { | ||
err := s.UnpackFromReader(r, keyRing) | ||
require.NoError(t, err) | ||
} else { | ||
var m chanbackup.Multi | ||
err := m.UnpackFromReader(r, keyRing) | ||
require.NoError(t, err) | ||
|
||
// Extract a single channel backup from | ||
// multi backup. | ||
require.Len(t, m.StaticBackups, 1) | ||
s = m.StaticBackups[0] | ||
} | ||
|
||
// Sign force close transaction. | ||
sweepTx, err := SignCloseTx( | ||
s, keyRing, signer, signer, | ||
) | ||
require.NoError(t, err) | ||
|
||
// Check if the transaction is valid. | ||
pkScript, err := hex.DecodeString(tc.PkScript) | ||
require.NoError(t, err) | ||
fetcher := txscript.NewCannedPrevOutputFetcher( | ||
pkScript, tc.AmountSats, | ||
) | ||
|
||
sigHashes := txscript.NewTxSigHashes(sweepTx, fetcher) | ||
|
||
vm, err := txscript.NewEngine( | ||
pkScript, sweepTx, 0, | ||
txscript.StandardVerifyFlags, | ||
nil, sigHashes, tc.AmountSats, fetcher, | ||
) | ||
require.NoError(t, err) | ||
|
||
require.NoError(t, vm.Execute()) | ||
}) | ||
} | ||
} |
Oops, something went wrong.