diff --git a/btc/address.go b/btc/address.go new file mode 100644 index 0000000..41ec219 --- /dev/null +++ b/btc/address.go @@ -0,0 +1,36 @@ +package btc + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" +) + +type AddressType string + +var ( + AddressP2PK AddressType = "P2PK" + AddressP2PKH AddressType = "P2PKH" + AddressP2SH AddressType = "P2SH" + AddressP2WPKH AddressType = "P2WPKH" + AddressSegwit AddressType = "Bech32" + AddressTaproot AddressType = "P2TR" +) + +func PublicKeyAddress(pub *btcec.PublicKey, network *chaincfg.Params, addressType AddressType) (btcutil.Address, error) { + switch addressType { + case AddressP2PK: + return btcutil.NewAddressPubKey(pub.SerializeCompressed(), network) + case AddressP2PKH: + return btcutil.NewAddressPubKeyHash(btcutil.Hash160(pub.SerializeCompressed()), network) + case AddressP2WPKH: + return btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(pub.SerializeCompressed()), network) + case AddressTaproot: + // todo: add taproot support + panic("todo") + default: + return nil, fmt.Errorf("unsupported address type") + } +} diff --git a/btc/address_test.go b/btc/address_test.go new file mode 100644 index 0000000..37b5b35 --- /dev/null +++ b/btc/address_test.go @@ -0,0 +1 @@ +package btc_test diff --git a/btc/btc.go b/btc/btc.go index acf283b..409411e 100644 --- a/btc/btc.go +++ b/btc/btc.go @@ -2,13 +2,16 @@ package btc import ( "crypto/sha512" + "errors" "fmt" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/wallet/txsizes" @@ -28,6 +31,10 @@ const ( SigHashSingleAnyoneCanPay = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay ) +// SizeUpdater returns the base and segwit size of signing a particular type of utxo. This is used by the tx building +// function to estimate the fee. +type SizeUpdater func() (int, int) + var ( P2pkhUpdater = func() (int, int) { return txsizes.RedeemP2PKHSigScriptSize, 0 @@ -79,7 +86,7 @@ func GenerateSystemPrivKey(mnemonic string, userPubkeyHex []byte) (*btcec.Privat masterKey, err = hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) if err != nil { // Very small chance to happen - if err == hdkeychain.ErrUnusableSeed { + if errors.Is(err, hdkeychain.ErrUnusableSeed) { continue } return nil, err @@ -108,21 +115,28 @@ func NewRawInputs() RawInputs { // fees. `inputs` will be a list of utxos that required to be included in the transaction, it comes with the base and // segwit size of the signature for fee-estimation purpose. `utxos` is a list of transaction will be picked // to cover the output amount and fees. We assume the utxos all comes from a single address. The `sizeUpdater` function -// returns the base and segwit size of each utxo from the utxos. If there's any change, it will be sent back to the +// returns the base and segwit size of each utxo from the `utxos`. If there's any change, it will be sent back to the // `changeAddr`. -func BuildTransaction(feeRate int, network *chaincfg.Params, inputs RawInputs, utxos []UTXO, recipients []Recipient, sizeUpdater func() (int, int), changeAddr btcutil.Address) (*wire.MsgTx, error) { +func BuildTransaction(network *chaincfg.Params, feeRate int, inputs RawInputs, utxos []UTXO, sizeUpdater SizeUpdater, recipients []Recipient, changeAddr btcutil.Address) (*wire.MsgTx, error) { tx := wire.NewMsgTx(DefaultBTCVersion) totalIn, totalOut := int64(0), int64(0) base, segwit := inputs.BaseSize, inputs.SegwitSize - // Adding required inputs + // Adding required inputs and output for _, utxo := range inputs.VIN { + // // Skip the utxo if the amount is not large enough. + // if utxo.Amount <= int64(minUtxoValue) { + // continue + // } hash, err := chainhash.NewHashFromStr(utxo.TxID) if err != nil { return nil, err } txIn := wire.NewTxIn(wire.NewOutPoint(hash, utxo.Vout), nil, nil) tx.AddTxIn(txIn) + if utxo.Amount == 0 { + return nil, fmt.Errorf("utxo amount is not set") + } totalIn += utxo.Amount } for _, recipient := range recipients { @@ -140,46 +154,41 @@ func BuildTransaction(feeRate int, network *chaincfg.Params, inputs RawInputs, u // Function to check if the input amount is greater than or equal to the output amount plus fees valueCheck := func() (bool, error) { - if totalIn > totalOut { - vs := EstimateVirtualSize(tx, base, segwit) - fees := int64(vs * feeRate) - - // If the amount is enough to cover the outputs and fees - if totalIn > totalOut+fees { - // Add a change utxo to the output if the change amount is greater than the dust - if totalIn-totalOut-fees > DustAmount { - if changeAddr != nil { - changeScript, err := txscript.PayToAddrScript(changeAddr) - if err != nil { - return false, err - } - tx.AddTxOut(wire.NewTxOut(0, changeScript)) // adjust the amount later - - // Estimate the fees again as we add a new output - vs := EstimateVirtualSize(tx, base, segwit) - fees := int64(vs * feeRate) - - // Adjust the change utxo amount if it's still enough, delete it otherwise - if totalIn-totalOut-fees > DustAmount { - tx.TxOut[len(tx.TxOut)-1].Value = totalIn - totalOut - fees - } else { - tx.TxOut = tx.TxOut[:len(tx.TxOut)-1] - } + if totalIn <= totalOut { + return false, nil + } + + vs := EstimateVirtualSize(tx, base, segwit) + fees := int64(vs * feeRate) + + // If the amount is enough to cover the outputs and fees + if totalIn > totalOut+fees { + // Add a change utxo to the output if the change amount is greater than the dust + if totalIn-totalOut-fees > DustAmount { + if changeAddr != nil { + changeScript, err := txscript.PayToAddrScript(changeAddr) + if err != nil { + return false, err + } + tx.AddTxOut(wire.NewTxOut(0, changeScript)) // adjust the amount later + + // Estimate the fees again as we add a new output + vs := EstimateVirtualSize(tx, base, segwit) + fees := int64(vs * feeRate) + + // Adjust the change utxo amount if it's still enough, delete it otherwise + if totalIn-totalOut-fees > DustAmount { + tx.TxOut[len(tx.TxOut)-1].Value = totalIn - totalOut - fees } else { - // Or adjust one of the output amount which is pending - for i, out := range tx.TxOut { - if out.Value == 0 { - tx.TxOut[i].Value = totalIn - totalOut - fees - break - } - } + tx.TxOut = tx.TxOut[:len(tx.TxOut)-1] } } - return true, nil } + + return true, nil } - return false, nil + return false, nil } // Check if the existing inputs are enough and we might not need to add extra utxos @@ -191,14 +200,25 @@ func BuildTransaction(feeRate int, network *chaincfg.Params, inputs RawInputs, u return tx, nil } + // Calculate the minimum utxo value we want to add to the tx. + // This is to prevent adding a dust utxo and cause the tx use more fees. + minUtxoValue := 0 + if sizeUpdater != nil { + minBase, minSegwit := sizeUpdater() + minVS := minBase + (minSegwit+3)/blockchain.WitnessScaleFactor + minUtxoValue = minVS * feeRate + } + // Keep adding utxos until we have enough funds to cover the output amount for _, utxo := range utxos { hash, err := chainhash.NewHashFromStr(utxo.TxID) if err != nil { return nil, err } - txIn := wire.NewTxIn(wire.NewOutPoint(hash, utxo.Vout), nil, nil) - tx.AddTxIn(txIn) + tx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(hash, utxo.Vout), nil, nil)) + if utxo.Amount <= int64(minUtxoValue) { + return nil, fmt.Errorf("utxo amount is not set") + } totalIn += utxo.Amount if sizeUpdater != nil { additionalBaseSize, additionalSegwitSize := sizeUpdater() @@ -218,3 +238,14 @@ func BuildTransaction(feeRate int, network *chaincfg.Params, inputs RawInputs, u return nil, fmt.Errorf("funds not enough") } + +func BuildRbfTransaction(network *chaincfg.Params, feeRate int, inputs RawInputs, utxos []UTXO, sizeUpdater SizeUpdater, recipients []Recipient, changeAddr btcutil.Address) (*wire.MsgTx, error) { + tx, err := BuildTransaction(network, feeRate, inputs, utxos, sizeUpdater, recipients, changeAddr) + if err != nil { + return nil, err + } + for i := range tx.TxIn { + tx.TxIn[i].Sequence = mempool.MaxRBFSequence + } + return tx, nil +} diff --git a/btc/btc_test.go b/btc/btc_test.go index cb1841f..0a0bf87 100644 --- a/btc/btc_test.go +++ b/btc/btc_test.go @@ -1,38 +1,150 @@ package btc_test import ( - "bytes" - "testing/quick" + "context" + "errors" + "fmt" + "time" - "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/catalogfi/blockchain/btc" - "github.com/tyler-smith/go-bip39" + "github.com/catalogfi/blockchain/btc/btctest" + "github.com/catalogfi/blockchain/testutil" + "github.com/fatih/color" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Bitcoin", func() { - Context("keys", func() { - It("should generate deterministic keys from mnemonic and user public key", func() { - test := func() bool { - entropy, err := bip39.NewEntropy(256) + Context("RBF", func() { + It("should be able to build transaction which support rbf", func(ctx context.Context) { + By("Initialization (Update these fields if testing on testnet/mainnet)") + network := &chaincfg.RegressionNetParams + privKey1, p2pkhAddr1, err := btctest.NewBtcKey(network) + Expect(err).To(BeNil()) + _, p2pkhAddr2, err := btctest.NewBtcKey(network) + Expect(err).To(BeNil()) + indexer := btctest.RegtestIndexer() + + By("Funding the addresses") + txhash1, err := testutil.NigiriFaucet(p2pkhAddr1.EncodeAddress()) + Expect(err).To(BeNil()) + By(fmt.Sprintf("Funding address1 %v , txid = %v", p2pkhAddr1.EncodeAddress(), txhash1)) + time.Sleep(5 * time.Second) + + By("Construct a RBF tx which sends money from p2pkhAddr1 to p2pkhAddr2") + utxos, err := indexer.GetUTXOs(ctx, p2pkhAddr1) + Expect(err).To(BeNil()) + amount, feeRate := int64(1e7), 4 + recipients := []btc.Recipient{ + { + To: p2pkhAddr2.EncodeAddress(), + Amount: amount, + }, + } + transaction, err := btc.BuildRbfTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, p2pkhAddr1) + Expect(err).To(BeNil()) + + By("Sign and submit the fund tx") + for i := range transaction.TxIn { + pkScript, err := txscript.PayToAddrScript(p2pkhAddr1) + Expect(err).To(BeNil()) + + sigScript, err := txscript.SignatureScript(transaction, i, pkScript, txscript.SigHashAll, privKey1, true) Expect(err).To(BeNil()) - mnemonic, err := bip39.NewMnemonic(entropy) + transaction.TxIn[i].SignatureScript = sigScript + } + Expect(indexer.SubmitTx(ctx, transaction)).Should(Succeed()) + By(color.GreenString("RBF tx hash = %v", transaction.TxHash().String())) + + By("Construct a tx with higher fees") + transaction1, err := btc.BuildTransaction(network, feeRate+5, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, p2pkhAddr1) + Expect(err).To(BeNil()) + transaction1.TxOut[1].Value++ + + By("Sign and submit replacement tx") + for i := range transaction1.TxIn { + pkScript, err := txscript.PayToAddrScript(p2pkhAddr1) Expect(err).To(BeNil()) - key, err := btcec.NewPrivateKey() + + sigScript, err := txscript.SignatureScript(transaction1, i, pkScript, txscript.SigHashAll, privKey1, true) + Expect(err).To(BeNil()) + transaction1.TxIn[i].SignatureScript = sigScript + } + Expect(indexer.SubmitTx(ctx, transaction1)).Should(Succeed()) + color.Green("Replacement tx = %v", transaction1.TxHash().String()) + }) + + It("should get an error when trying to replace a mined tx", func() { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + By("Initialise a local regnet client") + network := &chaincfg.RegressionNetParams + client, err := btctest.RegtestClient() + Expect(err).To(BeNil()) + indexer := btctest.RegtestIndexer() + By("New address") + privKey, pkAddr, err := btctest.NewBtcKey(network) + Expect(err).To(BeNil()) + _, toAddr, err := btctest.NewBtcKey(network) + Expect(err).To(BeNil()) + + By("funding the addresses") + txhash, err := testutil.NigiriFaucet(pkAddr.EncodeAddress()) + Expect(err).To(BeNil()) + By(fmt.Sprintf("Funding address1 %v , txid = %v", pkAddr.EncodeAddress(), txhash)) + time.Sleep(5 * time.Second) + + By("Construct a new tx") + utxos, err := indexer.GetUTXOs(ctx, pkAddr) + Expect(err).To(BeNil()) + amount, feeRate := int64(1e5), 5 + recipients := []btc.Recipient{ + { + To: toAddr.EncodeAddress(), + Amount: amount, + }, + } + transaction, err := btc.BuildRbfTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, pkAddr) + Expect(err).To(BeNil()) + + By("Sign the transaction inputs") + for i := range transaction.TxIn { + pkScript, err := txscript.PayToAddrScript(pkAddr) Expect(err).To(BeNil()) - extendedKey1, err := btc.GenerateSystemPrivKey(mnemonic, key.PubKey().SerializeCompressed()) + sigScript, err := txscript.SignatureScript(transaction, i, pkScript, txscript.SigHashAll, privKey, true) Expect(err).To(BeNil()) - extendedKey2, err := btc.GenerateSystemPrivKey(mnemonic, key.PubKey().SerializeCompressed()) + transaction.TxIn[i].SignatureScript = sigScript + } + + By("Submit the transaction") + Expect(client.SubmitTx(ctx, transaction)).Should(Succeed()) + By(fmt.Sprintf("Funding tx hash = %v", color.YellowString(transaction.TxHash().String()))) + time.Sleep(time.Second) + + By("Build a new tx with higher fee") + feeRate += 2 + transaction, err = btc.BuildRbfTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, pkAddr) + Expect(err).To(BeNil()) + + By("Sign the transaction inputs") + for i := range transaction.TxIn { + pkScript, err := txscript.PayToAddrScript(pkAddr) Expect(err).To(BeNil()) - Expect(bytes.Equal(extendedKey1.Serialize(), extendedKey2.Serialize())).Should(BeTrue()) - return true + sigScript, err := txscript.SignatureScript(transaction, i, pkScript, txscript.SigHashAll, privKey, true) + Expect(err).To(BeNil()) + transaction.TxIn[i].SignatureScript = sigScript + Expect(testutil.NigiriNewBlock()).Should(Succeed()) } - Expect(quick.Check(test, nil)).NotTo(HaveOccurred()) + By("Submit the transaction again and it should be rejected") + err = client.SubmitTx(ctx, transaction) + Expect(errors.Is(err, btc.ErrTxInputsMissingOrSpent)).Should(BeTrue()) }) }) }) diff --git a/btc/btctest/client.go b/btc/btctest/client.go index 0605052..f708dbc 100644 --- a/btc/btctest/client.go +++ b/btc/btctest/client.go @@ -23,6 +23,7 @@ type MockClient struct { FuncGetBlockByHeight func(context.Context, int64) (*btcjson.GetBlockVerboseResult, error) FuncGetBlockByHash func(context.Context, *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) FuncGetTxOut func(context.Context, *chainhash.Hash, uint32) (*btcjson.GetTxOutResult, error) + FuncGetNetworkInfo func(ctx context.Context) (*btcjson.GetNetworkInfoResult, error) } // Make sure the MockClient implements the Client interface @@ -87,10 +88,17 @@ func (m *MockClient) GetTxOut(ctx context.Context, hash *chainhash.Hash, vout ui return m.Client.GetTxOut(ctx, hash, vout) } +func (m *MockClient) GetNetworkInfo(ctx context.Context) (*btcjson.GetNetworkInfoResult, error) { + if m.FuncGetNetworkInfo != nil { + return m.FuncGetNetworkInfo(ctx) + } + return m.Client.GetNetworkInfo(ctx) +} + // MockIndexerClient for testing purpose type MockIndexerClient struct { Indexer btc.IndexerClient - FuncGetAddressTxs func(context.Context, btcutil.Address) ([]btc.Transaction, error) + FuncGetAddressTxs func(context.Context, btcutil.Address, string) ([]btc.Transaction, error) FuncGetUTXOs func(context.Context, btcutil.Address) (btc.UTXOs, error) FuncGetTipBlockHeight func(ctx context.Context) (uint64, error) FuncGetTx func(context.Context, string) (btc.Transaction, error) @@ -111,11 +119,11 @@ func NewMockIndexerClientWrapper(indexer btc.IndexerClient) *MockIndexerClient { } } -func (client MockIndexerClient) GetAddressTxs(ctx context.Context, address btcutil.Address) ([]btc.Transaction, error) { +func (client MockIndexerClient) GetAddressTxs(ctx context.Context, address btcutil.Address, lastSeenTxid string) ([]btc.Transaction, error) { if client.FuncGetAddressTxs != nil { - return client.FuncGetAddressTxs(ctx, address) + return client.FuncGetAddressTxs(ctx, address, lastSeenTxid) } - return client.Indexer.GetAddressTxs(ctx, address) + return client.Indexer.GetAddressTxs(ctx, address, lastSeenTxid) } func (client MockIndexerClient) GetUTXOs(ctx context.Context, address btcutil.Address) (btc.UTXOs, error) { diff --git a/btc/client.go b/btc/client.go index 73631cb..2f968f5 100644 --- a/btc/client.go +++ b/btc/client.go @@ -47,6 +47,9 @@ type Client interface { // GetTxOut returns details about an unspent transaction output. It will return nil result if the utxo has been // spent. GetTxOut(ctx context.Context, hash *chainhash.Hash, vout uint32) (*btcjson.GetTxOutResult, error) + + // GetNetworkInfo returns the network configuration of the node we connect to. + GetNetworkInfo(ctx context.Context) (*btcjson.GetNetworkInfoResult, error) } type client struct { @@ -281,3 +284,29 @@ func (client *client) GetTxOut(ctx context.Context, hash *chainhash.Hash, vout u return result, nil } } + +func (client *client) GetNetworkInfo(ctx context.Context) (*btcjson.GetNetworkInfoResult, error) { + future := client.rpcClient.GetNetworkInfoAsync() + results := make(chan *btcjson.GetNetworkInfoResult, 1) + errs := make(chan error, 1) + go func() { + defer close(results) + defer close(errs) + + result, err := future.Receive() + if err != nil { + errs <- err + return + } + results <- result + }() + + select { + case <-ctx.Done(): + return nil, fmt.Errorf("GetNetworkInfo : %w", ctx.Err()) + case err := <-errs: + return nil, err + case result := <-results: + return result, nil + } +} diff --git a/btc/client_test.go b/btc/client_test.go index c7f7afa..f8a9e4d 100644 --- a/btc/client_test.go +++ b/btc/client_test.go @@ -8,8 +8,6 @@ import ( "reflect" "time" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" @@ -97,6 +95,11 @@ var _ = Describe("bitcoin client", func() { scriptPubkey, err := txscript.PayToAddrScript(addr) Expect(err).To(BeNil()) Expect(txout.ScriptPubKey.Hex).Should(Equal(hex.EncodeToString(scriptPubkey))) + + By("GetNetworkInfo()") + netInfo, err := client.GetNetworkInfo(ctx) + Expect(err).To(BeNil()) + Expect(netInfo.RelayFee).Should(BeNumerically(">=", 0)) }) }) }) @@ -113,10 +116,7 @@ var _ = Describe("bitcoin client", func() { indexer := btctest.RegtestIndexer() By("New address") - privKey, err := btcec.NewPrivateKey() - Expect(err).To(BeNil()) - pubKey := privKey.PubKey() - pkAddr, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(pubKey.SerializeCompressed()), network) + privKey, pkAddr, err := btctest.NewBtcKey(network) Expect(err).To(BeNil()) By("funding the addresses") @@ -135,7 +135,7 @@ var _ = Describe("bitcoin client", func() { Amount: amount, }, } - transaction, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients, btc.P2pkhUpdater, pkAddr) + transaction, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, pkAddr) Expect(err).To(BeNil()) By("Sign the transaction inputs") @@ -176,7 +176,7 @@ var _ = Describe("bitcoin client", func() { }, } - transaction1, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients1, btc.P2pkhUpdater, pkAddr) + transaction1, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients1, pkAddr) Expect(err).To(BeNil()) for i := range transaction1.TxIn { pkScript, err := txscript.PayToAddrScript(pkAddr) @@ -186,7 +186,7 @@ var _ = Describe("bitcoin client", func() { Expect(err).To(BeNil()) transaction1.TxIn[i].SignatureScript = sigScript } - By("Expect a `ErrAlreadyInChain` error if the tx is already in a block") + By("Expect a `ErrTxInputsMissingOrSpent` error if the tx is already in a block") err = client.SubmitTx(ctx, transaction1) Expect(errors.Is(err, btc.ErrTxInputsMissingOrSpent)).Should(BeTrue()) }) @@ -197,15 +197,9 @@ var _ = Describe("bitcoin client", func() { By("Initialization keys ") network := &chaincfg.RegressionNetParams - privKey1, err := btcec.NewPrivateKey() - Expect(err).To(BeNil()) - pubKey1 := privKey1.PubKey() - pkAddr1, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(pubKey1.SerializeCompressed()), network) - Expect(err).To(BeNil()) - privKey2, err := btcec.NewPrivateKey() + privKey1, pkAddr1, err := btctest.NewBtcKey(network) Expect(err).To(BeNil()) - pubKey2 := privKey2.PubKey() - pkAddr2, err := btcutil.NewAddressPubKeyHash(btcutil.Hash160(pubKey2.SerializeCompressed()), network) + _, pkAddr2, err := btctest.NewBtcKey(network) Expect(err).To(BeNil()) client, err := btctest.RegtestClient() Expect(err).To(BeNil()) @@ -227,7 +221,7 @@ var _ = Describe("bitcoin client", func() { Amount: amount, }, } - transaction, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients, btc.P2pkhUpdater, pkAddr1) + transaction, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, pkAddr1) Expect(err).To(BeNil()) By("Sign and submit the fund tx") diff --git a/btc/fee.go b/btc/fee.go index 5b487ee..a228531 100644 --- a/btc/fee.go +++ b/btc/fee.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" ) @@ -19,14 +20,17 @@ const ( ) var ( + // RedeemHtlcRefundSigScriptSize is an estimate of the sigScript size when refunding an htlc script // stack number + stack size * 4 + signature + public key + script size RedeemHtlcRefundSigScriptSize = 1 + 4 + 73 + 33 + 89 + // RedeemHtlcRedeemSigScriptSize is an estimate of the sigScript size when redeeming an htlc script // stack number + stack size * 5 + signature + public key + secret + script size RedeemHtlcRedeemSigScriptSize = func(secretSize int) int { return 1 + 5 + 73 + 33 + secretSize + +1 + 89 } + // RedeemMultisigSigScriptSize is an estimate of the sigScript size from an 2-of-2 multisig script // stack number + stack size * 4 + signature * 2 + script size RedeemMultisigSigScriptSize = 1 + 4 + 73*2 + 71 ) @@ -40,7 +44,7 @@ func EstimateVirtualSize(tx *wire.MsgTx, extraBaseSize, extraSegwitSize int) int return baseSize + (swSize+3)/blockchain.WitnessScaleFactor } -// TxVirtualSize returns the virtual size of a transaction +// TxVirtualSize returns the virtual size of a transaction. func TxVirtualSize(tx *wire.MsgTx) int { size := tx.SerializeSize() baseSize := tx.SerializeSizeStripped() @@ -48,6 +52,19 @@ func TxVirtualSize(tx *wire.MsgTx) int { return baseSize + (swSize+3)/blockchain.WitnessScaleFactor } +// TotalFee returns the total amount fees used by the given tx. +func TotalFee(tx *wire.MsgTx, fetcher txscript.PrevOutputFetcher) int { + fees := int64(0) + for _, in := range tx.TxIn { + output := fetcher.FetchPrevOutput(in.PreviousOutPoint) + fees += output.Value + } + for _, out := range tx.TxOut { + fees -= out.Value + } + return int(fees) +} + type FeeSuggestion struct { Minimum int `json:"minimumFee"` Economy int `json:"economyFee"` @@ -102,7 +119,7 @@ func (f *mempoolFeeEstimator) FeeSuggestion() (FeeSuggestion, error) { return f.last, nil } else { return FeeSuggestion{ - 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, }, nil } } @@ -160,7 +177,7 @@ func (f *blockstreamFeeEstimator) FeeSuggestion() (FeeSuggestion, error) { } return f.last, nil } else { - return FeeSuggestion{2, 2, 2, 2, 2}, nil + return FeeSuggestion{1, 1, 1, 1, 1}, nil } } diff --git a/btc/fee_test.go b/btc/fee_test.go index 52b2e1f..8a578d0 100644 --- a/btc/fee_test.go +++ b/btc/fee_test.go @@ -1,9 +1,12 @@ package btc_test import ( + "bytes" "context" "crypto/sha256" + "encoding/hex" "fmt" + "log" "math/rand" "time" @@ -17,6 +20,7 @@ import ( "github.com/catalogfi/blockchain/btc" "github.com/catalogfi/blockchain/btc/btctest" "github.com/catalogfi/blockchain/testutil" + "github.com/fatih/color" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -69,21 +73,21 @@ var _ = Describe("bitcoin fees", func() { estimatorTestnet := btc.NewMempoolFeeEstimator(&chaincfg.TestNet3Params, "", 15*time.Second) fees, err := estimatorTestnet.FeeSuggestion() Expect(err).Should(BeNil()) - Expect(fees.Minimum).Should(Equal(2)) - Expect(fees.Economy).Should(Equal(2)) - Expect(fees.Low).Should(Equal(2)) - Expect(fees.Medium).Should(Equal(2)) - Expect(fees.High).Should(Equal(2)) + Expect(fees.Minimum).Should(Equal(1)) + Expect(fees.Economy).Should(Equal(1)) + Expect(fees.Low).Should(Equal(1)) + Expect(fees.Medium).Should(Equal(1)) + Expect(fees.High).Should(Equal(1)) By("Regnet") estimatorRegnet := btc.NewMempoolFeeEstimator(&chaincfg.RegressionNetParams, "", 15*time.Second) fees, err = estimatorRegnet.FeeSuggestion() Expect(err).Should(BeNil()) - Expect(fees.Minimum).Should(Equal(2)) - Expect(fees.Economy).Should(Equal(2)) - Expect(fees.Low).Should(Equal(2)) - Expect(fees.Medium).Should(Equal(2)) - Expect(fees.High).Should(Equal(2)) + Expect(fees.Minimum).Should(Equal(1)) + Expect(fees.Economy).Should(Equal(1)) + Expect(fees.Low).Should(Equal(1)) + Expect(fees.Medium).Should(Equal(1)) + Expect(fees.High).Should(Equal(1)) }) }) @@ -132,21 +136,21 @@ var _ = Describe("bitcoin fees", func() { estimatorTestnet := btc.NewBlockstreamFeeEstimator(&chaincfg.TestNet3Params, "", 15*time.Second) fees, err := estimatorTestnet.FeeSuggestion() Expect(err).Should(BeNil()) - Expect(fees.Minimum).Should(Equal(2)) - Expect(fees.Economy).Should(Equal(2)) - Expect(fees.Low).Should(Equal(2)) - Expect(fees.Medium).Should(Equal(2)) - Expect(fees.High).Should(Equal(2)) + Expect(fees.Minimum).Should(Equal(1)) + Expect(fees.Economy).Should(Equal(1)) + Expect(fees.Low).Should(Equal(1)) + Expect(fees.Medium).Should(Equal(1)) + Expect(fees.High).Should(Equal(1)) By("Regnet") estimatorRegnet := btc.NewBlockstreamFeeEstimator(&chaincfg.RegressionNetParams, "", 15*time.Second) fees, err = estimatorRegnet.FeeSuggestion() Expect(err).Should(BeNil()) - Expect(fees.Minimum).Should(Equal(2)) - Expect(fees.Economy).Should(Equal(2)) - Expect(fees.Low).Should(Equal(2)) - Expect(fees.Medium).Should(Equal(2)) - Expect(fees.High).Should(Equal(2)) + Expect(fees.Minimum).Should(Equal(1)) + Expect(fees.Economy).Should(Equal(1)) + Expect(fees.Low).Should(Equal(1)) + Expect(fees.Medium).Should(Equal(1)) + Expect(fees.High).Should(Equal(1)) }) }) @@ -165,6 +169,103 @@ var _ = Describe("bitcoin fees", func() { }) }) + Context("Testing too long chain", func() { + XIt("should return an error", func(ctx context.Context) { + By("Initialization (Update these fields if testing on testnet/mainnet)") + network := &chaincfg.RegressionNetParams + privKey1, p2pkhAddr1, err := btctest.NewBtcKey(network) + Expect(err).To(BeNil()) + privKey2, p2pkhAddr2, err := btctest.NewBtcKey(network) + Expect(err).To(BeNil()) + indexer := btctest.RegtestIndexer() + + By("Funding the addresses") + txhash1, err := testutil.NigiriFaucet(p2pkhAddr1.EncodeAddress()) + Expect(err).To(BeNil()) + By(fmt.Sprintf("Funding address1 %v , txid = %v", p2pkhAddr1.EncodeAddress(), txhash1)) + By(fmt.Sprintf("address2 %v , txid = %v", p2pkhAddr2.EncodeAddress(), txhash1)) + time.Sleep(5 * time.Second) + + utxos, err := indexer.GetUTXOs(context.Background(), p2pkhAddr1) + Expect(err).To(BeNil()) + + var inputTx string + for i := 0; i < 25; i++ { + feeRate := 10 + rawInputs := btc.RawInputs{ + VIN: utxos, + BaseSize: txsizes.RedeemP2PKHSigScriptSize * len(utxos), + SegwitSize: 0, + } + var recipients []btc.Recipient + if i == 10 { + recipients = append(recipients, btc.Recipient{ + To: p2pkhAddr2.EncodeAddress(), + Amount: 1e6, + }) + } + + transaction, err := btc.BuildTransaction(network, feeRate, rawInputs, utxos, btc.P2pkhUpdater, recipients, p2pkhAddr1) + Expect(err).To(BeNil()) + + for i := range transaction.TxIn { + pkScript, err := txscript.PayToAddrScript(p2pkhAddr1) + Expect(err).To(BeNil()) + + sigScript, err := txscript.SignatureScript(transaction, i, pkScript, txscript.SigHashAll, privKey1, true) + Expect(err).To(BeNil()) + transaction.TxIn[i].SignatureScript = sigScript + } + Expect(indexer.SubmitTx(ctx, transaction)).Should(Succeed()) + By(fmt.Sprintf("txhash %v = %v", i+1, color.YellowString(transaction.TxHash().String()))) + + vout := uint32(0) + if i == 10 { + vout = uint32(1) + inputTx = transaction.TxHash().String() + } + utxos = []btc.UTXO{ + { + TxID: transaction.TxHash().String(), + Vout: vout, + Amount: transaction.TxOut[vout].Value, + }, + } + } + + // Try construct a new transaction + rawInputs := btc.RawInputs{ + VIN: []btc.UTXO{ + { + TxID: inputTx, + Vout: 0, + Amount: 1e6, + }, + }, + BaseSize: txsizes.RedeemP2PKHSigScriptSize, + SegwitSize: 0, + } + + transaction, err := btc.BuildTransaction(network, 10, rawInputs, nil, nil, nil, p2pkhAddr2) + Expect(err).To(BeNil()) + for i := range transaction.TxIn { + pkScript, err := txscript.PayToAddrScript(p2pkhAddr2) + Expect(err).To(BeNil()) + + sigScript, err := txscript.SignatureScript(transaction, i, pkScript, txscript.SigHashAll, privKey2, true) + Expect(err).To(BeNil()) + transaction.TxIn[i].SignatureScript = sigScript + } + buffer := bytes.NewBuffer([]byte{}) + if err := transaction.Serialize(buffer); err != nil { + panic(err) + } + log.Print(hex.EncodeToString(buffer.Bytes())) + Expect(indexer.SubmitTx(ctx, transaction)).Should(Succeed()) + By(fmt.Sprintf("txhash = %v", color.YellowString(transaction.TxHash().String()))) + }) + }) + Context("estimate transaction fees", func() { It("should return a proper estimate of tx size for a simple transfer", func() { By("Initialization keys ") @@ -197,7 +298,7 @@ var _ = Describe("bitcoin fees", func() { Amount: amount, }, } - transaction, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients, btc.P2pkhUpdater, pkAddr1) + transaction, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, pkAddr1) Expect(err).To(BeNil()) By("Estimate the tx size before signing") @@ -262,7 +363,7 @@ var _ = Describe("bitcoin fees", func() { Amount: amount, }, } - transaction, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients, btc.MultisigUpdater, pkAddr1) + transaction, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.MultisigUpdater, recipients, pkAddr1) Expect(err).To(BeNil()) By("Estimate the tx size before signing") @@ -335,7 +436,7 @@ var _ = Describe("bitcoin fees", func() { Amount: amount, }, } - transaction, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients, btc.HtlcUpdater(len(secret)), pkAddr1) + transaction, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.HtlcUpdater(len(secret)), recipients, pkAddr1) Expect(err).To(BeNil()) By("Estimate the tx size before signing") @@ -403,7 +504,7 @@ var _ = Describe("bitcoin fees", func() { Amount: amount, }, } - transaction, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients, btc.HtlcUpdater(0), pkAddr1) + transaction, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.HtlcUpdater(0), recipients, pkAddr1) Expect(err).To(BeNil()) By("Estimate the tx size before signing") diff --git a/btc/indexer.go b/btc/indexer.go index f7a8305..e0c2c99 100644 --- a/btc/indexer.go +++ b/btc/indexer.go @@ -27,9 +27,10 @@ const ( ) type Transaction struct { - TxID string `json:"txid"` - VINs []VIN `json:"vin"` - Status Status `json:"status"` + TxID string `json:"txid"` + VINs []VIN `json:"vin"` + VOUTs []Prevout `json:"vout"` + Status Status `json:"status"` } type VIN struct { @@ -44,11 +45,13 @@ type Prevout struct { ScriptPubKeyType string `json:"scriptpubkey_type"` ScriptPubKey string `json:"scriptpubkey"` ScriptPubKeyAddress string `json:"scriptpubkey_address"` + Value int `json:"value"` } type Status struct { Confirmed bool `json:"confirmed"` BlockHeight uint64 `json:"block_height"` + BlockHash string `json:"block_hash"` } // IndexerClient provides some rpc functions which usually cannot be achieved by the standard bitcoin json-rpc methods. @@ -56,7 +59,7 @@ type Status struct { type IndexerClient interface { // GetAddressTxs returns the tx history of the given address. - GetAddressTxs(ctx context.Context, address btcutil.Address) ([]Transaction, error) + GetAddressTxs(ctx context.Context, address btcutil.Address, lastSeenTxid string) ([]Transaction, error) // GetUTXOs return all utxos of the given address. GetUTXOs(ctx context.Context, address btcutil.Address) (UTXOs, error) @@ -88,11 +91,17 @@ func NewElectrsIndexerClient(logger *zap.Logger, url string, retryInterval time. } } -func (client *electrsIndexerClient) GetAddressTxs(ctx context.Context, address btcutil.Address) ([]Transaction, error) { +func (client *electrsIndexerClient) GetAddressTxs(ctx context.Context, address btcutil.Address, lastSeenTxid string) ([]Transaction, error) { endpoint, err := url.JoinPath(client.url, "address", address.EncodeAddress(), "txs") if err != nil { return nil, err } + if lastSeenTxid != "" { + endpoint, err = url.JoinPath(client.url, "address", address.EncodeAddress(), "txs", "chain", lastSeenTxid) + if err != nil { + return nil, err + } + } // Send the request txs := []Transaction{} diff --git a/btc/indexer_test.go b/btc/indexer_test.go index e5ce046..ccd7318 100644 --- a/btc/indexer_test.go +++ b/btc/indexer_test.go @@ -64,7 +64,7 @@ var _ = Describe("Indexer client", func() { Amount: amount, }, } - rawTx, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients, btc.P2pkhUpdater, addr) + rawTx, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, addr) Expect(err).To(BeNil()) for i := range rawTx.TxIn { pkScript, err := txscript.PayToAddrScript(addr) @@ -81,7 +81,7 @@ var _ = Describe("Indexer client", func() { Expect(err.Error()).Should(ContainSubstring("not enough data")) By("GetAddressTxs()") - txs, err := client.GetAddressTxs(context.Background(), addr) + txs, err := client.GetAddressTxs(context.Background(), addr, "") Expect(err).To(BeNil()) has := false for _, tx := range txs { @@ -122,7 +122,7 @@ var _ = Describe("Indexer client", func() { Amount: amount, }, } - transaction, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients, btc.P2pkhUpdater, pkAddr) + transaction, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients, pkAddr) Expect(err).To(BeNil()) for i := range transaction.TxIn { pkScript, err := txscript.PayToAddrScript(pkAddr) @@ -151,7 +151,7 @@ var _ = Describe("Indexer client", func() { Amount: 2 * amount, }, } - transaction1, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, recipients1, btc.P2pkhUpdater, pkAddr) + transaction1, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, recipients1, pkAddr) Expect(err).To(BeNil()) for i := range transaction1.TxIn { pkScript, err := txscript.PayToAddrScript(pkAddr) diff --git a/btc/scripts_test.go b/btc/scripts_test.go index 7ab79a2..7676f55 100644 --- a/btc/scripts_test.go +++ b/btc/scripts_test.go @@ -62,7 +62,7 @@ var _ = Describe("Bitcoin scripts", func() { Amount: amount, }, } - fundingTx, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, fundingRecipients, btc.P2pkhUpdater, p2pkhAddr1) + fundingTx, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, fundingRecipients, p2pkhAddr1) Expect(err).To(BeNil()) By("Sign and submit the funding tx") @@ -87,18 +87,12 @@ var _ = Describe("Bitcoin scripts", func() { Amount: amount, }, } - redeemRecipients := []btc.Recipient{ - { - To: p2pkhAddr2.EncodeAddress(), - Amount: 0, - }, - } rawInputs := btc.RawInputs{ VIN: redeemInput, BaseSize: 0, SegwitSize: btc.RedeemMultisigSigScriptSize, } - redeemTx, err := btc.BuildTransaction(feeRate, network, rawInputs, nil, redeemRecipients, nil, nil) + redeemTx, err := btc.BuildTransaction(network, feeRate, rawInputs, nil, nil, nil, p2pkhAddr2) Expect(err).To(BeNil()) By("Sign and submit the redeem tx") @@ -162,7 +156,7 @@ var _ = Describe("Bitcoin scripts", func() { Amount: amount, }, } - fundingTx, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, fundingRecipients, btc.P2pkhUpdater, p2pkhAddr1) + fundingTx, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, fundingRecipients, p2pkhAddr1) Expect(err).To(BeNil()) By("Sign and submit the funding tx") @@ -237,18 +231,12 @@ var _ = Describe("Bitcoin scripts", func() { Amount: amount, }, } - htlcSpendRecipients := []btc.Recipient{ - { - To: p2pkhAddr2.EncodeAddress(), - Amount: 0, - }, - } rawInputs := btc.RawInputs{ VIN: htlcSpendInputs, BaseSize: 0, SegwitSize: btc.RedeemHtlcRedeemSigScriptSize(len(secret)), } - htlcSpendTx, err := btc.BuildTransaction(feeRate, network, rawInputs, nil, htlcSpendRecipients, nil, nil) + htlcSpendTx, err := btc.BuildTransaction(network, feeRate, rawInputs, nil, nil, nil, p2pkhAddr2) Expect(err).To(BeNil()) By("Sign and submit the tx") @@ -302,7 +290,7 @@ var _ = Describe("Bitcoin scripts", func() { Amount: amount, }, } - fundingTx, err := btc.BuildTransaction(feeRate, network, btc.NewRawInputs(), utxos, fundingRecipients, btc.P2pkhUpdater, p2pkhAddr1) + fundingTx, err := btc.BuildTransaction(network, feeRate, btc.NewRawInputs(), utxos, btc.P2pkhUpdater, fundingRecipients, p2pkhAddr1) Expect(err).To(BeNil()) By("Sign and submit the fund tx") @@ -383,18 +371,12 @@ var _ = Describe("Bitcoin scripts", func() { Amount: amount, }, } - htlcSpendRecipients := []btc.Recipient{ - { - To: p2pkhAddr2.EncodeAddress(), - Amount: 0, - }, - } rawInputs := btc.RawInputs{ VIN: htlcSpendInput, BaseSize: 0, SegwitSize: btc.RedeemHtlcRefundSigScriptSize, } - htlcSpendTx, err := btc.BuildTransaction(feeRate, network, rawInputs, nil, htlcSpendRecipients, nil, nil) + htlcSpendTx, err := btc.BuildTransaction(network, feeRate, rawInputs, nil, nil, nil, p2pkhAddr2) Expect(err).To(BeNil()) By("Sign the htlc spend tx") @@ -419,8 +401,8 @@ var _ = Describe("Bitcoin scripts", func() { err = client.SubmitTx(ctx, htlcSpendTx) Expect(err).To(BeNil()) By(fmt.Sprintf("Htlc SpendTx tx hash = %v", color.YellowString(htlcSpendTx.TxHash().String()))) + Expect(testutil.NigiriNewBlock()).Should(Succeed()) }) - }) Context("IsHtlc function", func() { diff --git a/go.mod b/go.mod index a954afe..90b3189 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( ) require ( + github.com/aead/siphash v1.0.1 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect @@ -27,6 +28,7 @@ require ( github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect diff --git a/go.sum b/go.sum index 578e29f..1bb4ce3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= @@ -74,6 +75,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=