From a2114a28f432d62d49b158e89e361b33fe882142 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 24 Nov 2019 15:59:24 +0100 Subject: [PATCH] Add forceclose command --- chainapi.go | 15 +++ chanbruteforce.go | 11 -- entry.go | 40 ++++++-- forceclose.go | 253 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 40 ++++++-- 5 files changed, 333 insertions(+), 26 deletions(-) create mode 100644 forceclose.go diff --git a/chainapi.go b/chainapi.go index cbfe8c3..6ce91f6 100644 --- a/chainapi.go +++ b/chainapi.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "strings" ) var ( @@ -70,6 +71,20 @@ func (a *chainApi) Transaction(txid string) (*transaction, error) { return tx, nil } +func (a *chainApi) 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 { + return "", err + } + body := new(bytes.Buffer) + _, err = body.ReadFrom(resp.Body) + if err != nil { + return "", err + } + return body.String(), nil +} + func Fetch(url string, target interface{}) error { resp, err := http.Get(url) if err != nil { diff --git a/chanbruteforce.go b/chanbruteforce.go index 593cc89..851f120 100644 --- a/chanbruteforce.go +++ b/chanbruteforce.go @@ -223,14 +223,3 @@ func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( } return currentKey, nil } - -func addrFromDesc(desc *keychain.KeyDescriptor) (string, error) { - hash160 := btcutil.Hash160(desc.PubKey.SerializeCompressed()) - addr, err := btcutil.NewAddressWitnessPubKeyHash( - hash160, chainParams, - ) - if err != nil { - return "", err - } - return addr.String(), nil -} diff --git a/entry.go b/entry.go index a1b8dd9..f108400 100644 --- a/entry.go +++ b/entry.go @@ -8,16 +8,38 @@ type ClosingTX struct { SweepPrivkey string `json:"sweep_privkey"` } +type Basepoint struct { + Family uint16 `json:"family"` + Index uint32 `json:"index"` + Pubkey string `json:"pubkey"` +} + +type Out struct { + Script string `json:"script"` + ScriptAsm string `json:"script_asm"` + Value uint64 `json:"value"` +} + +type ForceClose struct { + TXID string `json:"txid"` + Serialized string `json:"serialized"` + CSVDelay uint16 `json:"csv_delay"` + DelayBasepoint *Basepoint `json:"delay_basepoint"` + CommitPoint string `json:"commit_point"` + Outs []*Out `json:"outs"` +} + type SummaryEntry struct { - RemotePubkey string `json:"remote_pubkey"` - ChannelPoint string `json:"channel_point"` - FundingTXID string `json:"funding_txid"` - FundingTXIndex uint32 `json:"funding_tx_index"` - Capacity uint64 `json:"capacity"` - Initiator bool `json:"initiator"` - LocalBalance uint64 `json:"local_balance"` - RemoteBalance uint64 `json:"remote_balance"` - ClosingTX *ClosingTX `json:"closing_tx,omitempty"` + RemotePubkey string `json:"remote_pubkey"` + ChannelPoint string `json:"channel_point"` + FundingTXID string `json:"funding_txid"` + FundingTXIndex uint32 `json:"funding_tx_index"` + Capacity uint64 `json:"capacity"` + Initiator bool `json:"initiator"` + LocalBalance uint64 `json:"local_balance"` + RemoteBalance uint64 `json:"remote_balance"` + ClosingTX *ClosingTX `json:"closing_tx,omitempty"` + ForceClose *ForceClose `json:"force_close"` } type SummaryEntryFile struct { diff --git a/forceclose.go b/forceclose.go new file mode 100644 index 0000000..aca7a8a --- /dev/null +++ b/forceclose.go @@ -0,0 +1,253 @@ +package chantools + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +func forceCloseChannels(cfg *config, entries []*SummaryEntry, + chanDb *channeldb.DB, publish bool) error { + + channels, err := chanDb.FetchAllChannels() + if err != nil { + return err + } + + chainApi := &chainApi{baseUrl:cfg.ApiUrl} + + extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey) + if err != nil { + return err + } + signer := &signer{extendedKey: extendedKey} + + // Go through all channels in the DB, find the still open ones and + // publish their local commitment TX. + for _, channel := range channels { + channelPoint := channel.FundingOutpoint.String() + var channelEntry *SummaryEntry + for _, entry := range entries { + if entry.ChannelPoint == channelPoint { + channelEntry = entry + } + } + + // Don't try anything with closed channels. + if channelEntry == nil || channelEntry.ClosingTX != nil { + continue + } + + localCommit := channel.LocalCommitment + localCommitTx := localCommit.CommitTx + if localCommitTx == nil { + log.Errorf("Cannot force-close, no local commit TX for "+ + "channel %s", channelEntry.ChannelPoint) + continue + } + + // Create signed transaction. + lc := &LightningChannel{ + localChanCfg: channel.LocalChanCfg, + remoteChanCfg: channel.RemoteChanCfg, + channelState: channel, + txSigner: signer, + } + err := lc.createSignDesc() + if err != nil { + return err + } + + // Serialize transaction. + signedTx, err := lc.getSignedCommitTx() + if err != nil { + return err + } + var buf bytes.Buffer + err = signedTx.Serialize(io.Writer(&buf)) + if err != nil { + return err + } + hash := signedTx.TxHash() + serialized := hex.EncodeToString(buf.Bytes()) + + // Calculate commit point. + basepoint := channel.LocalChanCfg.DelayBasePoint + revocationPreimage, err := channel.RevocationProducer.AtIndex( + localCommit.CommitHeight, + ) + if err != nil { + return err + } + point := input.ComputeCommitmentPoint(revocationPreimage[:]) + channelEntry.ForceClose = &ForceClose{ + TXID: hash.String(), + Serialized: serialized, + DelayBasepoint: &Basepoint{ + Family: uint16(basepoint.Family), + Index: basepoint.Index, + }, + CommitPoint: hex.EncodeToString( + point.SerializeCompressed(), + ), + Outs: make([]*Out, len(localCommitTx.TxOut)), + } + for idx, out := range localCommitTx.TxOut { + script, err := txscript.DisasmString(out.PkScript) + if err != nil { + return err + } + channelEntry.ForceClose.Outs[idx] = &Out{ + Script: hex.EncodeToString(out.PkScript), + ScriptAsm: script, + Value: uint64(out.Value), + } + } + + // Publish TX. + if publish { + response, err := chainApi.PublishTx(serialized) + if err != nil { + return err + } + log.Infof("Published TX %s, response: %s", hash.String(), + response) + } + } + + summaryBytes, err := json.MarshalIndent(&SummaryEntryFile{ + Channels: entries, + }, "", " ") + if err != nil { + return err + } + fileName := fmt.Sprintf("results/forceclose-%s.json", + time.Now().Format("2006-01-02-15-04-05")) + log.Infof("Writing result to %s", fileName) + return ioutil.WriteFile(fileName, summaryBytes, 0644) +} + +type signer struct { + extendedKey *hdkeychain.ExtendedKey +} + +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) + if err != nil { + return nil, err + } + + amt := signDesc.Output.Value + sig, err := txscript.RawTxInWitnessSignature( + tx, signDesc.SigHashes, signDesc.InputIndex, amt, + witnessScript, signDesc.HashType, privKey, + ) + if err != nil { + return nil, err + } + + // Chop off the sighash flag at the end of the signature. + return sig[:len(sig)-1], nil +} + +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), + 0, + descriptor.Index, + }) + if err != nil { + return nil, err + } + return key.ECPrivKey() +} + +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/main.go b/main.go index 3aedad7..b6e9e72 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,8 @@ package chantools import ( "fmt" - + "path" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/jessevdk/go-flags" "github.com/lightningnetwork/lnd/build" @@ -19,8 +20,8 @@ type config struct { ListChannels string `long:"listchannels" description:"The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin."` PendingChannels string `long:"pendingchannels" description:"The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin."` FromSummary string `long:"fromsummary" description:"The channel input is in the format of this tool's channel summary. Specify '-' to read from stdin."` - FromChannelDB string `long:"fromchanneldb" description:"The channel input is in the format of an lnd channel.db file. Specify '-' to read from stdin."` - RescueDB string `long:"rescuedb" description:"The lnd channel.db file to use for rescuing remote force-closed channels."` + FromChannelDB string `long:"fromchanneldb" description:"The channel input is in the format of an lnd channel.db file."` + ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for rescuing or force-closing channels."` } var ( @@ -45,6 +46,11 @@ func Main() error { "are in outputs of remotely force-closed channels", "", &rescueClosedCommand{}, ) + _, _ = parser.AddCommand( + "forceclose", "Force-close the last state that is in the " + + "channel.db provided", "", + &forceCloseCommand{}, + ) _, err := parser.Parse() return err @@ -73,11 +79,11 @@ func (c *rescueClosedCommand) Execute(args []string) error { return fmt.Errorf("error parsing root key: %v", err) } - // Check that we have a rescue DB. - if cfg.RescueDB == "" { + // Check that we have a channel DB. + if cfg.ChannelDB == "" { return fmt.Errorf("rescue DB is required") } - db, err := channeldb.Open(cfg.RescueDB) + db, err := channeldb.Open(path.Dir(cfg.ChannelDB)) if err != nil { return fmt.Errorf("error opening rescue DB: %v", err) } @@ -90,6 +96,28 @@ func (c *rescueClosedCommand) Execute(args []string) error { return bruteForceChannels(cfg, entries, db) } +type forceCloseCommand struct { + Publish bool `long:"publish" description:"Should the force-closing TX be published to the chain API?"` +} + +func (c *forceCloseCommand) Execute(args []string) error { + // Check that we have a channel DB. + if cfg.ChannelDB == "" { + return fmt.Errorf("rescue DB is required") + } + db, err := channeldb.Open(path.Dir(cfg.ChannelDB)) + if err != nil { + return fmt.Errorf("error opening rescue DB: %v", err) + } + + // Parse channel entries from any of the possible input files. + entries, err := ParseInput(cfg) + if err != nil { + return err + } + return forceCloseChannels(cfg, entries, db, c.Publish) +} + func setupLogging() { logWriter.RegisterSubLogger("CHAN", log) err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)