Skip to content

Commit

Permalink
add package scbforceclose
Browse files Browse the repository at this point in the history
It provides function SignCloseTx which produces a signed force close
transaction from a channel backup and private key material.
  • Loading branch information
starius committed Oct 2, 2024
1 parent 05b187d commit 7c7f092
Show file tree
Hide file tree
Showing 3 changed files with 352 additions and 0 deletions.
159 changes: 159 additions & 0 deletions scbforceclose/sign_close_tx.go
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)
}
133 changes: 133 additions & 0 deletions scbforceclose/sign_close_tx_test.go
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())
})
}
}
Loading

0 comments on commit 7c7f092

Please sign in to comment.