diff --git a/chanbruteforce.go b/chanbruteforce.go index 851f120..10a43f9 100644 --- a/chanbruteforce.go +++ b/chanbruteforce.go @@ -109,28 +109,9 @@ func bruteForceChannels(cfg *config, entries []*SummaryEntry, } func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) { - // First parse address to get targetPubKeyHash from it later. - targetAddr, err := btcutil.DecodeAddress(addr, chainParams) + targetPubKeyHash, err := parseAddr(addr) if err != nil { - return "", err - } - - var targetPubKeyHash []byte - // Make the check on the decoded address according to the active - // network (testnet or mainnet only). - if !targetAddr.IsForNet(chainParams) { - return "", fmt.Errorf( - "address: %v is not valid for this network: %v", - targetAddr.String(), chainParams.Name, - ) - } - - // Must be a bech32 native SegWit address. - switch targetAddr.(type) { - case *btcutil.AddressWitnessPubKeyHash: - targetPubKeyHash = targetAddr.ScriptAddress() - default: - return "", fmt.Errorf("address: must be a bech32 P2WPKH address") + return "", fmt.Errorf("error parsing addr: %v", err) } // Loop through all cached payment base point keys, tweak each of it @@ -223,3 +204,30 @@ func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( } return currentKey, nil } + +func parseAddr(addr string) ([]byte, error) { + // First parse address to get targetPubKeyHash from it later. + targetAddr, err := btcutil.DecodeAddress(addr, chainParams) + if err != nil { + return nil, err + } + + var targetPubKeyHash []byte + // Make the check on the decoded address according to the active + // network (testnet or mainnet only). + if !targetAddr.IsForNet(chainParams) { + return nil, fmt.Errorf( + "address: %v is not valid for this network: %v", + targetAddr.String(), chainParams.Name, + ) + } + + // Must be a bech32 native SegWit address. + switch targetAddr.(type) { + case *btcutil.AddressWitnessPubKeyHash: + targetPubKeyHash = targetAddr.ScriptAddress() + default: + return nil, fmt.Errorf("address: must be a bech32 P2WPKH address") + } + return targetPubKeyHash, nil +} \ No newline at end of file diff --git a/chansummary.go b/chansummary.go index 7642c71..985eb4b 100644 --- a/chansummary.go +++ b/chansummary.go @@ -30,7 +30,8 @@ func collectChanSummary(cfg *config, channels []*SummaryEntry) error { if outspend.Spent { summaryFile.ClosedChannels++ channel.ClosingTX = &ClosingTX{ - TXID: outspend.Txid, + TXID: outspend.Txid, + ConfHeight: uint32(outspend.Status.BlockHeight), } err := reportOutspend( diff --git a/entry.go b/entry.go index f108400..58a2e0c 100644 --- a/entry.go +++ b/entry.go @@ -6,11 +6,12 @@ type ClosingTX struct { AllOutsSpent bool `json:"all_outputs_spent"` OurAddr string `json:"our_addr"` SweepPrivkey string `json:"sweep_privkey"` + ConfHeight uint32 `json:"conf_height"` } type Basepoint struct { - Family uint16 `json:"family"` - Index uint32 `json:"index"` + Family uint16 `json:"family,omitempty"` + Index uint32 `json:"index,omitempty"` Pubkey string `json:"pubkey"` } @@ -21,12 +22,13 @@ type Out struct { } 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"` + TXID string `json:"txid"` + Serialized string `json:"serialized"` + CSVDelay uint16 `json:"csv_delay"` + DelayBasepoint *Basepoint `json:"delay_basepoint"` + RevocationBasepoint *Basepoint `json:"revocation_basepoint"` + CommitPoint string `json:"commit_point"` + Outs []*Out `json:"outs"` } type SummaryEntry struct { diff --git a/forceclose.go b/forceclose.go index aca7a8a..f28c404 100644 --- a/forceclose.go +++ b/forceclose.go @@ -9,13 +9,11 @@ import ( "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, @@ -25,8 +23,8 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry, if err != nil { return err } - - chainApi := &chainApi{baseUrl:cfg.ApiUrl} + + chainApi := &chainApi{baseUrl: cfg.ApiUrl} extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey) if err != nil { @@ -85,6 +83,7 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry, // Calculate commit point. basepoint := channel.LocalChanCfg.DelayBasePoint + revpoint := channel.RemoteChanCfg.RevocationBasePoint revocationPreimage, err := channel.RevocationProducer.AtIndex( localCommit.CommitHeight, ) @@ -98,11 +97,20 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry, DelayBasepoint: &Basepoint{ Family: uint16(basepoint.Family), Index: basepoint.Index, + Pubkey: hex.EncodeToString( + basepoint.PubKey.SerializeCompressed(), + ), + }, + RevocationBasepoint: &Basepoint{ + Pubkey: hex.EncodeToString( + revpoint.PubKey.SerializeCompressed(), + ), }, CommitPoint: hex.EncodeToString( point.SerializeCompressed(), ), - Outs: make([]*Out, len(localCommitTx.TxOut)), + Outs: make([]*Out, len(localCommitTx.TxOut)), + CSVDelay: channel.LocalChanCfg.CsvDelay, } for idx, out := range localCommitTx.TxOut { script, err := txscript.DisasmString(out.PkScript) @@ -115,7 +123,7 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry, Value: uint64(out.Value), } } - + // Publish TX. if publish { response, err := chainApi.PublishTx(serialized) @@ -139,50 +147,6 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry, 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 diff --git a/main.go b/main.go index b6e9e72..7a629e4 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,11 @@ func Main() error { "channel.db provided", "", &forceCloseCommand{}, ) + _, _ = parser.AddCommand( + "sweeptimelock", "Sweep the force-closed state after the time " + + "lock has expired", "", + &sweepTimeLockCommand{}, + ) _, err := parser.Parse() return err @@ -118,6 +123,34 @@ func (c *forceCloseCommand) Execute(args []string) error { return forceCloseChannels(cfg, entries, db, c.Publish) } +type sweepTimeLockCommand struct { + Publish bool `long:"publish" description:"Should the sweep TX be published to the chain API?"` + SweepAddr string `long:"sweepaddr" description:"The address the funds should be sweeped to"` +} + +func (c *sweepTimeLockCommand) Execute(args []string) error { + // Check that root key is valid. + if cfg.RootKey == "" { + return fmt.Errorf("root key is required") + } + _, err := hdkeychain.NewKeyFromString(cfg.RootKey) + if err != nil { + return fmt.Errorf("error parsing root key: %v", err) + } + + // Make sure sweep addr is set. + if c.SweepAddr == "" { + return fmt.Errorf("sweep addr is required") + } + + // Parse channel entries from any of the possible input files. + entries, err := ParseInput(cfg) + if err != nil { + return err + } + return sweepTimeLock(cfg, entries, c.SweepAddr, c.Publish) +} + func setupLogging() { logWriter.RegisterSubLogger("CHAN", log) err := logWriter.InitLogRotator("./results/chantools.log", 10, 3) diff --git a/signer.go b/signer.go new file mode 100644 index 0000000..57f4195 --- /dev/null +++ b/signer.go @@ -0,0 +1,89 @@ +package chantools + +import ( + "fmt" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +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 + } + + privKey, err = maybeTweakPrivKey(signDesc, privKey) + 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) ComputeInputScript(tx *wire.MsgTx, + signDesc *input.SignDescriptor) (*input.Script, error) { + return nil, fmt.Errorf("unimplemented") +} + +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() +} + +// maybeTweakPrivKey examines the single and double tweak parameters on the +// passed sign descriptor and may perform a mapping on the passed private key +// in order to utilize the tweaks, if populated. +func maybeTweakPrivKey(signDesc *input.SignDescriptor, + privKey *btcec.PrivateKey) (*btcec.PrivateKey, error) { + + var retPriv *btcec.PrivateKey + switch { + + case signDesc.SingleTweak != nil: + retPriv = input.TweakPrivKey(privKey, + signDesc.SingleTweak) + + case signDesc.DoubleTweak != nil: + retPriv = input.DeriveRevocationPrivKey(privKey, + signDesc.DoubleTweak) + + default: + retPriv = privKey + } + + return retPriv, nil +} \ No newline at end of file diff --git a/sweeptimelock.go b/sweeptimelock.go new file mode 100644 index 0000000..4c12bb0 --- /dev/null +++ b/sweeptimelock.go @@ -0,0 +1,234 @@ +package chantools + +import ( + "bytes" + "encoding/hex" + "fmt" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +const ( + maxCsvTimeout = 15000 + feeSatPerByte = 3 +) + +func sweepTimeLock(cfg *config, entries []*SummaryEntry, sweepAddr string, + publish bool) error { + + extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey) + if err != nil { + return err + } + signer := &signer{extendedKey: extendedKey} + sweepTx := wire.NewMsgTx(2) + entryIndex := 0 + value := int64(0) + signDescs := make([]*input.SignDescriptor, 0) + + for _, entry := range entries { + if entry.ClosingTX == nil || entry.ForceClose == nil || + entry.ClosingTX.AllOutsSpent || entry.LocalBalance == 0 { + + log.Infof("Not sweeping %s, info missing or all spent", + entry.ChannelPoint) + continue + } + + fc := entry.ForceClose + + txindex := -1 + if len(fc.Outs) == 1 { + txindex = 0 + if fc.Outs[0].Value != entry.LocalBalance { + log.Errorf("Potential value mismatch! %d vs %d (%s)", + fc.Outs[0].Value, entry.LocalBalance, + entry.ChannelPoint) + } + } else { + for idx, out := range fc.Outs { + if out.Value == entry.LocalBalance { + txindex = idx + } + } + } + + if txindex == -1 { + log.Errorf("Could not find sweep output for chan %s", + entry.ChannelPoint) + continue + } + txHash, err := chainhash.NewHashFromStr(fc.TXID) + if err != nil { + return fmt.Errorf("error parsing tx hash: %v", err) + } + + commitPointBytes, err := hex.DecodeString(fc.CommitPoint) + if err != nil { + return fmt.Errorf("error parsing commit point: %v", err) + } + commitPoint, err := btcec.ParsePubKey(commitPointBytes, btcec.S256()) + if err != nil { + return fmt.Errorf("error parsing commit point: %v", err) + } + revPointBytes, err := hex.DecodeString(fc.RevocationBasepoint.Pubkey) + if err != nil { + return fmt.Errorf("error parsing commit point: %v", err) + } + revPoint, err := btcec.ParsePubKey(revPointBytes, btcec.S256()) + if err != nil { + return fmt.Errorf("error parsing commit point: %v", err) + } + + delayKeyDesc := &keychain.KeyDescriptor{ + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(fc.DelayBasepoint.Family), + Index: fc.DelayBasepoint.Index, + }, + } + delayPrivkey, err := signer.fetchPrivKey(delayKeyDesc) + if err != nil { + return fmt.Errorf("error getting private key: %v", err) + } + + delayKey := input.TweakPubKey(delayPrivkey.PubKey(), commitPoint) + revocationKey := input.DeriveRevocationPubkey( + revPoint, commitPoint, + ) + + var ( + csvTimeout = int32(-1) + script []byte + scriptHash []byte + ) + targetScript, err := hex.DecodeString(fc.Outs[txindex].Script) + if err != nil { + return fmt.Errorf("error parsing target script: %v", err) + } + if len(targetScript) != 34 { + log.Errorf("invalid target script: %x", targetScript) + continue + } + for i := 0; csvTimeout == -1 && i < maxCsvTimeout; i++ { + s, err := input.CommitScriptToSelf( + uint32(i), delayKey, revocationKey, + ) + if err != nil { + return fmt.Errorf("error creating script: %v", err) + } + sh, err := input.WitnessScriptHash(s) + if err != nil { + return fmt.Errorf("error hashing script: %v", err) + } + if bytes.Equal(targetScript[0:8], sh[0:8]) { + csvTimeout = int32(i) + script = s + scriptHash = sh + } + } + if csvTimeout == -1 || len(script) == 0 { + log.Errorf("Could not create matching script for %s " + + "or csv too high: %d", entry.ChannelPoint, + csvTimeout) + continue + } + + sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: *txHash, + Index: uint32(txindex), + }, + SignatureScript: nil, + Witness: nil, + Sequence: input.LockTimeToSequence( + false, uint32(csvTimeout), + ), + }) + + singleTweak := input.SingleTweakBytes( + commitPoint, delayPrivkey.PubKey(), + ) + + signDesc := &input.SignDescriptor{ + KeyDesc: *delayKeyDesc, + SingleTweak: singleTweak, + WitnessScript: script, + Output: &wire.TxOut{ + PkScript: scriptHash, + Value: int64(fc.Outs[txindex].Value), + }, + HashType: txscript.SigHashAll, + InputIndex: entryIndex, + } + value += int64(fc.Outs[txindex].Value) + signDescs = append(signDescs, signDesc) + + entryIndex++ + } + + if len(signDescs) != len(sweepTx.TxIn) { + return fmt.Errorf("length mismatch") + } + + sweepScript, err := pkhScript(sweepAddr) + if err != nil { + return err + } + sweepTx.TxOut = []*wire.TxOut{{ + Value: value, + PkScript: sweepScript, + }} + + sigHashes := txscript.NewTxSigHashes(sweepTx) + for idx, desc := range signDescs { + desc.SigHashes = sigHashes + witness, err := input.CommitSpendTimeout(signer, desc, sweepTx) + if err != nil { + return err + } + sweepTx.TxIn[idx].Witness = witness + } + + size := sweepTx.SerializeSize() + fee := int64(size*feeSatPerByte) + sweepTx.TxOut[0].Value = value - fee + + // Sign again after output fixing. + sigHashes = txscript.NewTxSigHashes(sweepTx) + for idx, desc := range signDescs { + desc.SigHashes = sigHashes + witness, err := input.CommitSpendTimeout(signer, desc, sweepTx) + if err != nil { + return err + } + sweepTx.TxIn[idx].Witness = witness + } + + var buf bytes.Buffer + err = sweepTx.Serialize(&buf) + if err != nil { + return err + } + log.Infof("Fee %d sats of %d total amount (for size %d)", + fee, value, sweepTx.SerializeSize()) + log.Infof("Transaction: %x", buf.Bytes()) + + return nil +} + +func pkhScript(addr string) ([]byte, error) { + targetPubKeyHash, err := parseAddr(addr) + if err != nil { + return nil, err + } + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddData(targetPubKeyHash) + + return builder.Script() +}