From 9508a0d8e0c63cbf49c809740ab40c2f0ea6fe6e Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sat, 4 Jan 2020 23:10:26 +0100 Subject: [PATCH] Refactor BTC utils into package --- btc/channel.go | 77 ++++++++++++++++++++++++ chain/api.go => btc/explorer_api.go | 14 ++--- hdkeychain.go => btc/hdkeychain.go | 33 ++++++----- signer.go => btc/signer.go | 27 +++++---- cmd_derivekey.go | 5 +- cmd_forceclose.go | 90 ++++------------------------- cmd_rescueclosed.go | 10 ++-- cmd_summary.go | 17 +++--- cmd_sweeptimelock.go | 11 ++-- dataformat/input.go | 3 +- dataformat/summary.go | 4 +- main.go | 7 ++- 12 files changed, 160 insertions(+), 138 deletions(-) create mode 100644 btc/channel.go rename chain/api.go => btc/explorer_api.go (85%) rename hdkeychain.go => btc/hdkeychain.go (70%) rename signer.go => btc/signer.go (70%) diff --git a/btc/channel.go b/btc/channel.go new file mode 100644 index 0000000..e5918f6 --- /dev/null +++ b/btc/channel.go @@ -0,0 +1,77 @@ +package btc + +import ( + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" +) + +type LightningChannel struct { + LocalChanCfg channeldb.ChannelConfig + RemoteChanCfg channeldb.ChannelConfig + SignDesc *input.SignDescriptor + ChannelState *channeldb.OpenChannel + TXSigner *Signer +} + +// CreateSignDesc derives the SignDescriptor for commitment transactions from +// other fields on the LightningChannel. +func (lc *LightningChannel) CreateSignDesc() error { + localKey := lc.LocalChanCfg.MultiSigKey.PubKey.SerializeCompressed() + remoteKey := lc.RemoteChanCfg.MultiSigKey.PubKey.SerializeCompressed() + + multiSigScript, err := input.GenMultiSigScript(localKey, remoteKey) + if err != nil { + return err + } + + fundingPkScript, err := input.WitnessScriptHash(multiSigScript) + if err != nil { + return err + } + lc.SignDesc = &input.SignDescriptor{ + KeyDesc: lc.LocalChanCfg.MultiSigKey, + WitnessScript: multiSigScript, + Output: &wire.TxOut{ + PkScript: fundingPkScript, + Value: int64(lc.ChannelState.Capacity), + }, + HashType: txscript.SigHashAll, + InputIndex: 0, + } + + return nil +} + +// SignedCommitTx function take the latest commitment transaction and populate +// it with witness data. +func (lc *LightningChannel) SignedCommitTx() (*wire.MsgTx, error) { + // Fetch the current commitment transaction, along with their signature + // for the transaction. + localCommit := lc.ChannelState.LocalCommitment + commitTx := localCommit.CommitTx.Copy() + theirSig := append(localCommit.CommitSig, byte(txscript.SigHashAll)) + + // With this, we then generate the full witness so the caller can + // broadcast a fully signed transaction. + lc.SignDesc.SigHashes = txscript.NewTxSigHashes(commitTx) + ourSigRaw, err := lc.TXSigner.SignOutputRaw(commitTx, lc.SignDesc) + if err != nil { + return nil, err + } + + ourSig := append(ourSigRaw, byte(txscript.SigHashAll)) + + // With the final signature generated, create the witness stack + // required to spend from the multi-sig output. + ourKey := lc.LocalChanCfg.MultiSigKey.PubKey.SerializeCompressed() + theirKey := lc.RemoteChanCfg.MultiSigKey.PubKey.SerializeCompressed() + + commitTx.TxIn[0].Witness = input.SpendMultiSig( + lc.SignDesc.WitnessScript, ourKey, + ourSig, theirKey, theirSig, + ) + + return commitTx, nil +} diff --git a/chain/api.go b/btc/explorer_api.go similarity index 85% rename from chain/api.go rename to btc/explorer_api.go index c7fb826..8076a80 100644 --- a/chain/api.go +++ b/btc/explorer_api.go @@ -1,4 +1,4 @@ -package chain +package btc import ( "bytes" @@ -13,7 +13,7 @@ var ( ErrTxNotFound = errors.New("transaction not found") ) -type Api struct { +type ExplorerApi struct { BaseUrl string } @@ -51,9 +51,9 @@ type Status struct { BlockHash string `json:"block_hash"` } -func (a *Api) Transaction(txid string) (*TX, error) { +func (a *ExplorerApi) Transaction(txid string) (*TX, error) { tx := &TX{} - err := Fetch(fmt.Sprintf("%s/tx/%s", a.BaseUrl, txid), tx) + err := fetchJSON(fmt.Sprintf("%s/tx/%s", a.BaseUrl, txid), tx) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func (a *Api) Transaction(txid string) (*TX, error) { "%s/tx/%s/outspend/%d", a.BaseUrl, txid, idx, ) outspend := Outspend{} - err := Fetch(url, &outspend) + err := fetchJSON(url, &outspend) if err != nil { return nil, err } @@ -71,7 +71,7 @@ func (a *Api) Transaction(txid string) (*TX, error) { return tx, nil } -func (a *Api) PublishTx(rawTxHex string) (string, error) { +func (a *ExplorerApi) PublishTx(rawTxHex string) (string, error) { url := fmt.Sprintf("%s/tx", a.BaseUrl) resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex)) if err != nil { @@ -85,7 +85,7 @@ func (a *Api) PublishTx(rawTxHex string) (string, error) { return body.String(), nil } -func Fetch(url string, target interface{}) error { +func fetchJSON(url string, target interface{}) error { resp, err := http.Get(url) if err != nil { return err diff --git a/hdkeychain.go b/btc/hdkeychain.go similarity index 70% rename from hdkeychain.go rename to btc/hdkeychain.go index 6b8fa70..663b3c4 100644 --- a/hdkeychain.go +++ b/btc/hdkeychain.go @@ -1,19 +1,20 @@ -package chantools +package btc import ( "fmt" + "strconv" + "strings" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/hdkeychain" "github.com/lightningnetwork/lnd/keychain" - "strconv" - "strings" ) const ( - hardenedKeyStart = uint32(hdkeychain.HardenedKeyStart) + HardenedKeyStart = uint32(hdkeychain.HardenedKeyStart) ) -func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( +func DeriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( *hdkeychain.ExtendedKey, error) { var ( @@ -29,7 +30,7 @@ func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( return currentKey, nil } -func parsePath(path string) ([]uint32, error) { +func ParsePath(path string) ([]uint32, error) { path = strings.TrimSpace(path) if len(path) == 0 { return nil, fmt.Errorf("path cannot be empty") @@ -43,7 +44,7 @@ func parsePath(path string) ([]uint32, error) { index := uint32(0) part := parts[i] if strings.Contains(parts[i], "'") { - index += hardenedKeyStart + index += HardenedKeyStart part = strings.TrimRight(parts[i], "'") } parsed, err := strconv.Atoi(part) @@ -56,25 +57,25 @@ func parsePath(path string) ([]uint32, error) { return indices, nil } -type channelBackupEncryptionRing struct { - extendedKey *hdkeychain.ExtendedKey - chainParams *chaincfg.Params +type ChannelBackupEncryptionRing struct { + ExtendedKey *hdkeychain.ExtendedKey + ChainParams *chaincfg.Params } -func (r *channelBackupEncryptionRing) DeriveNextKey(_ keychain.KeyFamily) ( +func (r *ChannelBackupEncryptionRing) DeriveNextKey(_ keychain.KeyFamily) ( keychain.KeyDescriptor, error) { return keychain.KeyDescriptor{}, nil } -func (r *channelBackupEncryptionRing) DeriveKey(keyLoc keychain.KeyLocator) ( +func (r *ChannelBackupEncryptionRing) DeriveKey(keyLoc keychain.KeyLocator) ( keychain.KeyDescriptor, error) { var empty = keychain.KeyDescriptor{} - keyBackup, err := deriveChildren(r.extendedKey, []uint32{ - hardenedKeyStart + uint32(keychain.BIP0043Purpose), - hardenedKeyStart + r.chainParams.HDCoinType, - hardenedKeyStart + uint32(keyLoc.Family), + keyBackup, err := DeriveChildren(r.ExtendedKey, []uint32{ + HardenedKeyStart + uint32(keychain.BIP0043Purpose), + HardenedKeyStart + r.ChainParams.HDCoinType, + HardenedKeyStart + uint32(keyLoc.Family), 0, keyLoc.Index, }) diff --git a/signer.go b/btc/signer.go similarity index 70% rename from signer.go rename to btc/signer.go index 97e6c58..f158075 100644 --- a/signer.go +++ b/btc/signer.go @@ -1,9 +1,10 @@ -package chantools +package btc import ( "fmt" "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil/hdkeychain" @@ -11,17 +12,18 @@ import ( "github.com/lightningnetwork/lnd/keychain" ) -type signer struct { - extendedKey *hdkeychain.ExtendedKey +type Signer struct { + ExtendedKey *hdkeychain.ExtendedKey + ChainParams *chaincfg.Params } -func (s *signer) SignOutputRaw(tx *wire.MsgTx, +func (s *Signer) SignOutputRaw(tx *wire.MsgTx, signDesc *input.SignDescriptor) ([]byte, error) { witnessScript := signDesc.WitnessScript // First attempt to fetch the private key which corresponds to the // specified public key. - privKey, err := s.fetchPrivKey(&signDesc.KeyDesc) + privKey, err := s.FetchPrivKey(&signDesc.KeyDesc) if err != nil { return nil, err } @@ -40,18 +42,19 @@ func (s *signer) SignOutputRaw(tx *wire.MsgTx, return sig[:len(sig)-1], nil } -func (s *signer) ComputeInputScript(tx *wire.MsgTx, - signDesc *input.SignDescriptor) (*input.Script, error) { +func (s *Signer) ComputeInputScript(_ *wire.MsgTx, _ *input.SignDescriptor) ( + *input.Script, error) { + return nil, fmt.Errorf("unimplemented") } -func (s *signer) fetchPrivKey(descriptor *keychain.KeyDescriptor) ( +func (s *Signer) FetchPrivKey(descriptor *keychain.KeyDescriptor) ( *btcec.PrivateKey, error) { - key, err := deriveChildren(s.extendedKey, []uint32{ - hardenedKeyStart + uint32(keychain.BIP0043Purpose), - hardenedKeyStart + chainParams.HDCoinType, - hardenedKeyStart + uint32(descriptor.Family), + key, err := DeriveChildren(s.ExtendedKey, []uint32{ + HardenedKeyStart + uint32(keychain.BIP0043Purpose), + HardenedKeyStart + s.ChainParams.HDCoinType, + HardenedKeyStart + uint32(descriptor.Family), 0, descriptor.Index, }) diff --git a/cmd_derivekey.go b/cmd_derivekey.go index 86af459..0aa5014 100644 --- a/cmd_derivekey.go +++ b/cmd_derivekey.go @@ -5,17 +5,18 @@ import ( "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" + "github.com/guggero/chantools/btc" ) func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string, neuter bool) error { fmt.Printf("Deriving path %s for network %s.\n", path, chainParams.Name) - parsedPath, err := parsePath(path) + parsedPath, err := btc.ParsePath(path) if err != nil { return fmt.Errorf("could not parse derivation path: %v", err) } - derivedKey, err := deriveChildren(extendedKey, parsedPath) + derivedKey, err := btc.DeriveChildren(extendedKey, parsedPath) if err != nil { return fmt.Errorf("could not derive children: %v", err) } diff --git a/cmd_forceclose.go b/cmd_forceclose.go index b3f140e..05df781 100644 --- a/cmd_forceclose.go +++ b/cmd_forceclose.go @@ -10,9 +10,8 @@ import ( "time" "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil/hdkeychain" - "github.com/guggero/chantools/chain" + "github.com/guggero/chantools/btc" "github.com/guggero/chantools/dataformat" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" @@ -26,8 +25,8 @@ func forceCloseChannels(extendedKey *hdkeychain.ExtendedKey, if err != nil { return err } - chainApi := &chain.Api{BaseUrl: cfg.ApiUrl} - signer := &signer{extendedKey: extendedKey} + chainApi := &btc.ExplorerApi{BaseUrl: cfg.ApiUrl} + signer := &btc.Signer{ExtendedKey: extendedKey} // Go through all channels in the DB, find the still open ones and // publish their local commitment TX. @@ -54,19 +53,19 @@ func forceCloseChannels(extendedKey *hdkeychain.ExtendedKey, } // Create signed transaction. - lc := &LightningChannel{ - localChanCfg: channel.LocalChanCfg, - remoteChanCfg: channel.RemoteChanCfg, - channelState: channel, - txSigner: signer, + lc := &btc.LightningChannel{ + LocalChanCfg: channel.LocalChanCfg, + RemoteChanCfg: channel.RemoteChanCfg, + ChannelState: channel, + TXSigner: signer, } - err := lc.createSignDesc() + err := lc.CreateSignDesc() if err != nil { return err } // Serialize transaction. - signedTx, err := lc.getSignedCommitTx() + signedTx, err := lc.SignedCommitTx() if err != nil { return err } @@ -149,72 +148,3 @@ func forceCloseChannels(extendedKey *hdkeychain.ExtendedKey, log.Infof("Writing result to %s", fileName) return ioutil.WriteFile(fileName, summaryBytes, 0644) } - -type LightningChannel struct { - localChanCfg channeldb.ChannelConfig - remoteChanCfg channeldb.ChannelConfig - signDesc *input.SignDescriptor - channelState *channeldb.OpenChannel - txSigner *signer -} - -// createSignDesc derives the SignDescriptor for commitment transactions from -// other fields on the LightningChannel. -func (lc *LightningChannel) createSignDesc() error { - localKey := lc.localChanCfg.MultiSigKey.PubKey.SerializeCompressed() - remoteKey := lc.remoteChanCfg.MultiSigKey.PubKey.SerializeCompressed() - - multiSigScript, err := input.GenMultiSigScript(localKey, remoteKey) - if err != nil { - return err - } - - fundingPkScript, err := input.WitnessScriptHash(multiSigScript) - if err != nil { - return err - } - lc.signDesc = &input.SignDescriptor{ - KeyDesc: lc.localChanCfg.MultiSigKey, - WitnessScript: multiSigScript, - Output: &wire.TxOut{ - PkScript: fundingPkScript, - Value: int64(lc.channelState.Capacity), - }, - HashType: txscript.SigHashAll, - InputIndex: 0, - } - - return nil -} - -// getSignedCommitTx function take the latest commitment transaction and -// populate it with witness data. -func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) { - // Fetch the current commitment transaction, along with their signature - // for the transaction. - localCommit := lc.channelState.LocalCommitment - commitTx := localCommit.CommitTx.Copy() - theirSig := append(localCommit.CommitSig, byte(txscript.SigHashAll)) - - // With this, we then generate the full witness so the caller can - // broadcast a fully signed transaction. - lc.signDesc.SigHashes = txscript.NewTxSigHashes(commitTx) - ourSigRaw, err := lc.txSigner.SignOutputRaw(commitTx, lc.signDesc) - if err != nil { - return nil, err - } - - ourSig := append(ourSigRaw, byte(txscript.SigHashAll)) - - // With the final signature generated, create the witness stack - // required to spend from the multi-sig output. - ourKey := lc.localChanCfg.MultiSigKey.PubKey.SerializeCompressed() - theirKey := lc.remoteChanCfg.MultiSigKey.PubKey.SerializeCompressed() - - commitTx.TxIn[0].Witness = input.SpendMultiSig( - lc.signDesc.WitnessScript, ourKey, - ourSig, theirKey, theirSig, - ) - - return commitTx, nil -} diff --git a/cmd_rescueclosed.go b/cmd_rescueclosed.go index ffba586..24eb982 100644 --- a/cmd_rescueclosed.go +++ b/cmd_rescueclosed.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/guggero/chantools/btc" "io/ioutil" "time" @@ -150,10 +151,11 @@ func fillCache(extendedKey *hdkeychain.ExtendedKey) error { cache = make([]*cacheEntry, cacheSize) for i := 0; i < cacheSize; i++ { - key, err := deriveChildren(extendedKey, []uint32{ - hardenedKeyStart + uint32(keychain.BIP0043Purpose), - hardenedKeyStart + chainParams.HDCoinType, - hardenedKeyStart + uint32(keychain.KeyFamilyPaymentBase), + key, err := btc.DeriveChildren(extendedKey, []uint32{ + btc.HardenedKeyStart + uint32(keychain.BIP0043Purpose), + btc.HardenedKeyStart + chainParams.HDCoinType, + btc.HardenedKeyStart + + uint32(keychain.KeyFamilyPaymentBase), 0, uint32(i), }) diff --git a/cmd_summary.go b/cmd_summary.go index ce7922d..287346a 100644 --- a/cmd_summary.go +++ b/cmd_summary.go @@ -6,7 +6,7 @@ import ( "io/ioutil" "time" - "github.com/guggero/chantools/chain" + "github.com/guggero/chantools/btc" "github.com/guggero/chantools/dataformat" ) @@ -16,11 +16,11 @@ func summarizeChannels(apiUrl string, summaryFile := &dataformat.SummaryEntryFile{ Channels: channels, } - chainApi := &chain.Api{BaseUrl: apiUrl} + chainApi := &btc.ExplorerApi{BaseUrl: apiUrl} for idx, channel := range channels { tx, err := chainApi.Transaction(channel.FundingTXID) - if err == chain.ErrTxNotFound { + if err == btc.ErrTxNotFound { log.Errorf("Funding TX %s not found. Ignoring.", channel.FundingTXID) channel.ChanExists = false @@ -93,8 +93,9 @@ func summarizeChannels(apiUrl string, return ioutil.WriteFile(fileName, summaryBytes, 0644) } -func reportOutspend(api *chain.Api, summaryFile *dataformat.SummaryEntryFile, - entry *dataformat.SummaryEntry, os *chain.Outspend) error { +func reportOutspend(api *btc.ExplorerApi, + summaryFile *dataformat.SummaryEntryFile, + entry *dataformat.SummaryEntry, os *btc.Outspend) error { spendTx, err := api.Transaction(os.Txid) if err != nil { @@ -102,7 +103,7 @@ func reportOutspend(api *chain.Api, summaryFile *dataformat.SummaryEntryFile, } summaryFile.FundsClosedChannels += entry.LocalBalance - var utxo []*chain.Vout + var utxo []*btc.Vout for _, vout := range spendTx.Vout { if !vout.Outspend.Spent { utxo = append(utxo, vout) @@ -174,7 +175,7 @@ func reportOutspend(api *chain.Api, summaryFile *dataformat.SummaryEntryFile, return nil } -func couldBeOurs(entry *dataformat.SummaryEntry, utxo []*chain.Vout) bool { +func couldBeOurs(entry *dataformat.SummaryEntry, utxo []*btc.Vout) bool { if len(utxo) == 1 && utxo[0].Value == entry.RemoteBalance { return false } @@ -182,6 +183,6 @@ func couldBeOurs(entry *dataformat.SummaryEntry, utxo []*chain.Vout) bool { return entry.LocalBalance != 0 } -func isCoopClose(tx *chain.TX) bool { +func isCoopClose(tx *btc.TX) bool { return tx.Vin[0].Sequence == 0xffffffff } diff --git a/cmd_sweeptimelock.go b/cmd_sweeptimelock.go index 192641b..5b623f9 100644 --- a/cmd_sweeptimelock.go +++ b/cmd_sweeptimelock.go @@ -10,7 +10,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil/hdkeychain" - "github.com/guggero/chantools/chain" + "github.com/guggero/chantools/btc" "github.com/guggero/chantools/dataformat" "github.com/lightningnetwork/lnd/input" ) @@ -24,8 +24,11 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiUrl string, publish bool) error { // Create signer and transaction template. - signer := &signer{extendedKey: extendedKey} - chainApi := &chain.Api{BaseUrl: apiUrl} + signer := &btc.Signer{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } + chainApi := &btc.ExplorerApi{BaseUrl: apiUrl} sweepTx := wire.NewMsgTx(2) totalOutputValue := int64(0) @@ -78,7 +81,7 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiUrl string, return fmt.Errorf("error parsing commit point: %v", err) } delayDesc := fc.DelayBasePoint.Desc() - delayPrivKey, err := signer.fetchPrivKey(delayDesc) + delayPrivKey, err := signer.FetchPrivKey(delayDesc) if err != nil { return fmt.Errorf("error getting private key: %v", err) } diff --git a/dataformat/input.go b/dataformat/input.go index 262f6dc..fa9fc6b 100644 --- a/dataformat/input.go +++ b/dataformat/input.go @@ -4,9 +4,10 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/lightningnetwork/lnd/channeldb" "strconv" "strings" + + "github.com/lightningnetwork/lnd/channeldb" ) type NumberString uint64 diff --git a/dataformat/summary.go b/dataformat/summary.go index 4891ba8..6365c22 100644 --- a/dataformat/summary.go +++ b/dataformat/summary.go @@ -1,6 +1,8 @@ package dataformat -import "github.com/lightningnetwork/lnd/keychain" +import ( + "github.com/lightningnetwork/lnd/keychain" +) type ClosingTX struct { TXID string `json:"txid"` diff --git a/main.go b/main.go index d87e17e..6414241 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/hdkeychain" + "github.com/guggero/chantools/btc" "github.com/guggero/chantools/dataformat" "github.com/jessevdk/go-flags" "github.com/lightningnetwork/lnd/aezeed" @@ -291,9 +292,9 @@ func (c *dumpBackupCommand) Execute(_ []string) error { return fmt.Errorf("backup file is required") } multiFile := chanbackup.NewMultiFile(c.MultiFile) - multi, err := multiFile.ExtractMulti(&channelBackupEncryptionRing{ - extendedKey: extendedKey, - chainParams: chainParams, + multi, err := multiFile.ExtractMulti(&btc.ChannelBackupEncryptionRing{ + ExtendedKey: extendedKey, + ChainParams: chainParams, }) if err != nil { return fmt.Errorf("could not extract multi file: %v", err)