Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.0.7 #7

Merged
merged 17 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions btc/address.go
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddressSegwit is not being used anywhere and p2wpkh is already a segwit address. Any reason to keep it ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddressSegwit is mostly for future proof and currently is not used anywhere. The whole address file is still experimental. And yeah p2wpkh is a special type address of segwit. Since it is commonly used in a lot places, so it's worth to have a separate type for it, so we know how to how to estimate fees/sign sigature/decode address from key.

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")
}
}
111 changes: 71 additions & 40 deletions btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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
}
142 changes: 127 additions & 15 deletions btc/btc_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
})
})
Loading
Loading