Skip to content

Commit

Permalink
修改灰尘的计算逻辑
Browse files Browse the repository at this point in the history
  • Loading branch information
yangyile committed Nov 21, 2024
1 parent e61067b commit a56ce48
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 76 deletions.
18 changes: 17 additions & 1 deletion dogecoin/dogedust.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package dogecoin

import "github.com/yyle88/gobtcsign/internal/dusts"
import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/yyle88/gobtcsign/internal/dusts"
)

const (
// MinDustOutput 硬性灰尘数量,详见 https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md
Expand All @@ -15,9 +19,21 @@ const (

type DustFee = dusts.DustFee

// NewDogeDustFee 配置狗狗币的dust费用
// 具体参考链接在
// https://github.com/dogecoin/dogecoin/blob/b4a5d2bef20f5cca54d9c14ca118dec259e47bb4/doc/fee-recommendation.md
// DOGECOIN 简单规定了软灰尘和硬灰尘,假如是硬灰尘会被拒绝,假如是软灰尘会收取额外的费用
func NewDogeDustFee() DustFee {
res := dusts.NewDustFee()
res.SoftDustSize = SoftDustLimit
res.ExtraDustFee = ExtraDustsFee
return res
}

type DustLimit = dusts.DustLimit

func NewDogeDustLimit() *DustLimit {
return dusts.NewDustLimit(func(output *wire.TxOut, relayFeePerKb btcutil.Amount) bool {
return output.Value < MinDustOutput //在dogecoin中的灰尘规定比较简单,它不依赖于费率,而是直接和常量比较,逻辑简单
})
}
18 changes: 18 additions & 0 deletions internal/dusts/txout_dust.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dusts

import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
)

type DustLimit struct {
check func(output *wire.TxOut, relayFeePerKb btcutil.Amount) bool
}

func NewDustLimit(check func(output *wire.TxOut, relayFeePerKb btcutil.Amount) bool) *DustLimit {
return &DustLimit{check: check}
}

func (D *DustLimit) IsDustOutput(output *wire.TxOut, relayFeePerKb btcutil.Amount) bool {
return D.check(output, relayFeePerKb)
}
18 changes: 18 additions & 0 deletions txdust.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gobtcsign

import (
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/yyle88/gobtcsign/internal/dusts"
)

type DustFee = dusts.DustFee

func NewDustFee() DustFee {
return dusts.NewDustFee() //比特币没有软灰尘收费,这里配置个空的(因为doge里有,这里为了逻辑相通,而给个空的)
}

type DustLimit = dusts.DustLimit

func NewDustLimit() *DustLimit {
return dusts.NewDustLimit(txrules.IsDustOutput)
}
112 changes: 112 additions & 0 deletions txdust_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package gobtcsign

import (
"testing"

"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/require"
"github.com/yyle88/gobtcsign/dogecoin"
)

func TestIsDustOutput_BTC(t *testing.T) {
netParams := chaincfg.MainNetParams

dustLimit := NewDustLimit()

const address = "1MAgFFbMpgx6hTPvK3HY348Bbnwk6RFHm5"
const feeRate = 1234000 //假设手续费是这个数

amount := int64(100000)
for {
output := wire.NewTxOut(amount, MustGetPkScript(MustNewAddress(address, &netParams)))
if !dustLimit.IsDustOutput(output, feeRate) {
break
}
amount++
}
t.Log("amount:", amount, "IS NOT DUST IN BTC")
t.Log("amount:", amount-1, "IS DUST IN BTC")
}

func TestIsDustOutput_BTC_2(t *testing.T) {
netParams := chaincfg.MainNetParams

dustLimit := NewDustLimit()

const address = "1MAgFFbMpgx6hTPvK3HY348Bbnwk6RFHm5"
const feeRate = 1000 //假设手续费是这个数

amount := int64(1)
for {
output := wire.NewTxOut(amount, MustGetPkScript(MustNewAddress(address, &netParams)))
if !dustLimit.IsDustOutput(output, feeRate) {
break
}
amount++
}
require.Equal(t, int64(546), amount) //这就是有的教程和代码里写 546 的依据
// 546 是比特币网络中一个常见的 Dust Threshold(灰尘阈值),其来源与比特币的交易手续费和 UTXO(未花费交易输出,Unspent Transaction Output)管理策略相关。
// 具体来说,546 是在默认设置下,比特币 Core 客户端用于计算 Dust Output(灰尘输出)的默认值。
// 比特币网络中最早的版本采用的是类似值,后来进行了微调,但很多第三方实现(例如钱包)仍然保留 546 的惯例。
t.Log("amount:", amount, "IS NOT DUST IN BTC")
t.Log("amount:", amount-1, "IS DUST IN BTC")
}

func TestIsDustOutput_BTC_3(t *testing.T) {
netParams := chaincfg.TestNet3Params

dustLimit := NewDustLimit()

const address = "tb1qy2f7svy0hp57wz3p6hvu0vf5fys750932ct3q5"
const feeRate = 1000 //假设手续费是这个数

amount := int64(1)
for {
output := wire.NewTxOut(amount, MustGetPkScript(MustNewAddress(address, &netParams)))
if !dustLimit.IsDustOutput(output, feeRate) {
break
}
amount++
}
require.Equal(t, int64(294), amount) //当地址类型为 P2WPKH 时由于其 输入大小比 P2PKH 小得多,因此灰尘阈值也更小些
// 因此有的代码里会这样写
// DustLimit returns the output dust limit (lowest possible satoshis in a UTXO) for the address type.
// func (a Address) DustLimit() int64 {
// switch a.encodedType {
// case AddressP2TR:
// return 330
// case AddressP2WPKH:
// return 294
// default:
// return 546
// }
// }
// 但实际上大家还都是使用 out >= 546 作为限制
t.Log("amount:", amount, "IS NOT DUST IN BTC")
t.Log("amount:", amount-1, "IS DUST IN BTC")
}

func TestIsDustOutput_DOGE(t *testing.T) {
netParams := dogecoin.TestNetParams

dustLimit := dogecoin.NewDogeDustLimit()
{
const amount = 500
const address = "nr2XmwqixAdXwkgVyshx3HPFRMfXugM8Zi"
const feeRate = 0 //这个不重要

output := wire.NewTxOut(amount, MustGetPkScript(MustNewAddress(address, &netParams)))
require.True(t, dustLimit.IsDustOutput(output, feeRate))
t.Log("amount:", amount, "IS DUST IN DOGE")
}
{
const amount = 100000
const address = "nqedQEDCgwrXqLd2JrrpCfD9Tcz384rdHA"
const feeRate = 0 //这个不重要

output := wire.NewTxOut(amount, MustGetPkScript(MustNewAddress(address, &netParams)))
require.False(t, dustLimit.IsDustOutput(output, feeRate))
t.Log("amount:", amount, "IS NOT DUST IN DOGE")
}
}
21 changes: 14 additions & 7 deletions txfee.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,15 @@ import (
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/btcsuite/btcwallet/wallet/txsizes"
"github.com/pkg/errors"
"github.com/yyle88/gobtcsign/internal/dusts"
)

type DustFee = dusts.DustFee

func NewDustFee() DustFee {
return dusts.NewDustFee() //比特币没有软灰尘收费,这里配置个空的(因为doge里有,这里为了逻辑相通,而给个空的)
}

// EstimateTxFee 通过未签名且未找零的交易,预估出需要的费用
// 代码基本是仿照这里的 github.com/btcsuite/btcwallet/wallet/[email protected]/author.go 里面 NewUnsignedTransaction 的逻辑
// 具体参考链接在
// https://github.com/btcsuite/btcwallet/blob/b4ff60753aaa3cf885fb09586755f67d41954942/wallet/txauthor/author.go#L132
// 由于是计算手续费的,因为这个交易里不应该包含找零的 output 信息,否则结果是无意义的
func EstimateTxFee(param *CustomParam, netParams *chaincfg.Params, change *ChangeTo, feeRatePerKb btcutil.Amount, dustFee DustFee) (btcutil.Amount, error) {
//通过未签名的交易预估出签名后的交易大小,这里预估值会比线上的值略微大些,误差在个位数(具体看vin和out的个数)
maxSignedSize, err := EstimateTxSize(param, netParams, change)
if err != nil {
return 0, errors.WithMessage(err, "wrong estimate-tx-size")
Expand Down Expand Up @@ -55,6 +51,8 @@ func EstimateTxSize(param *CustomParam, netParams *chaincfg.Params, change *Chan
// EstimateSize 计算交易的预估大小(在最坏情况下的预估大小)
// 这个函数还是抄的 github.com/btcsuite/btcwallet/wallet/[email protected]/author.go 里面 NewUnsignedTransaction 的逻辑
// 详细细节见 https://github.com/btcsuite/btcwallet/blob/master/wallet/txauthor/author.go 这里的逻辑
// 具体参考链接在
// https://github.com/btcsuite/btcwallet/blob/b4ff60753aaa3cf885fb09586755f67d41954942/wallet/txauthor/author.go#L93
// 是否填写找零信息,得依据 outputs 里面是否已经包含找零信息
func EstimateSize(scripts [][]byte, outputs []*wire.TxOut, change *ChangeTo) (int, error) {
changeScriptSize, err := change.GetChangeScriptSize()
Expand All @@ -81,21 +79,25 @@ func EstimateSize(scripts [][]byte, outputs []*wire.TxOut, change *ChangeTo) (in
}

// 仿照这个函数 txauthor.NewUnsignedTransaction() 里的预估逻辑
// 通过未签名的交易信息,估算出要发送上链的交易体的大小,其中每个vin/out的误差至多是个位数的,累计起来误差不大,能够用来预估交易费用
maxSignedSize := txsizes.EstimateVirtualSize(
p2pkh, p2tr, p2wpkh, nested, outputs, changeScriptSize,
)
return maxSignedSize, nil
}

// ChangeTo 找零信息,这里为了方便使用,就设置两个属性二选一即可,优先使用公钥哈希,其次使用钱包地址
type ChangeTo struct {
PkScript []byte //允许为空,当两者皆为空时表示没有找零输出
AddressX btcutil.Address //允许为空,当两者皆为空时表示没有找零输出
}

// NewNoChange 当不需要找零时两个成员都是空
func NewNoChange() *ChangeTo {
return &ChangeTo{}
}

// GetChangeScriptSize 计算出找零输出的size
func (T *ChangeTo) GetChangeScriptSize() (int, error) {
if T.PkScript != nil { //优先使用找零脚本进行计算
return CalculateChangePkScriptSize(T.PkScript)
Expand All @@ -106,6 +108,7 @@ func (T *ChangeTo) GetChangeScriptSize() (int, error) {
return 0, nil //说明不需要找零输出,就返回0
}

// CalculateChangeAddressSize 根据钱包地址计算出找零输出的size
func CalculateChangeAddressSize(address btcutil.Address) (int, error) {
pkScript, err := txscript.PayToAddrScript(address)
if err != nil {
Expand All @@ -114,6 +117,10 @@ func CalculateChangeAddressSize(address btcutil.Address) (int, error) {
return CalculateChangePkScriptSize(pkScript)
}

// CalculateChangePkScriptSize 根据公钥哈希计算出找零输出的size
// 具体参考链接在
// https://github.com/btcsuite/btcwallet/blob/b4ff60753aaa3cf885fb09586755f67d41954942/wallet/createtx.go#L457
// 当然这里代码略有差异,但含义是相同的
func CalculateChangePkScriptSize(pkScript []byte) (int, error) {
var size int
switch {
Expand Down
2 changes: 2 additions & 0 deletions txfee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func TestEstimateTxSize(t *testing.T) {

func TestEstimateTxSize_VIN_1_P2PKH(t *testing.T) {
{
// 这个就是从官方包里拷贝的
// https://github.com/btcsuite/btcwallet/blob/b4ff60753aaa3cf885fb09586755f67d41954942/wallet/txsizes/size_test.go#L145
const txHex = "0100000001a4c91c9720157a5ee582a7966471d9c70d0a860fa7757b4c42a535a12054a4c9000000006c493046022100d49c452a00e5b1213ac84d92269510a05a584a4d0949bd7d0ad4e3408ac8e80a022100bf98707ffaf1eb9dff146f7da54e68651c0a27e3653ec3882b7a95202328579c01210332d98672a4246fe917b9c724c339e757d46b1ffde3fb27fdc680b4bb29b6ad59ffffffff02a0860100000000001976a9144fb55ee0524076acd4c14e7773561e4c298c8e2788ac20688a0b000000001976a914cb7f6bb8e95a2cd06423932cfbbce73d16a18df088ac00000000"

mstTx, err := NewMsgTxFromHex(txHex)
Expand Down
31 changes: 0 additions & 31 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -79,36 +78,6 @@ func MustGetPkScript(address btcutil.Address) []byte {
return pkScript
}

// IsDustOutputBtc 检查是不是灰尘输出,链不允许灰尘输出,避免到处都是粉尘
func IsDustOutputBtc(netParam *chaincfg.Params, address string, amount int64, dustLimitCoin float64, feePerKb btcutil.Amount) (bool, error) {
dustLimitAmount, err := btcutil.NewAmount(dustLimitCoin)
if err != nil {
return false, errors.WithMessage(err, "wrong dust limit coin to amount")
}
if btcutil.Amount(amount) < dustLimitAmount { //当小于硬性灰尘数量时,就肯定是灰尘的
return true, nil
}
pkScript, err := GetAddressPkScript(address, netParam)
if err != nil {
return false, errors.WithMessage(err, "wrong address->pk-script")
}
output := wire.NewTxOut(amount, pkScript)
isDust := txrules.IsDustOutput(output, feePerKb)
return isDust, nil
}

// IsDustOutputDoge 根据 https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md 这个看到只有两个规则
func IsDustOutputDoge(amount int64, dustLimitCoin float64) (bool, error) {
dustLimitAmount, err := btcutil.NewAmount(dustLimitCoin)
if err != nil {
return false, errors.WithMessage(err, "wrong dust limit coin to amount")
}
if btcutil.Amount(amount) < dustLimitAmount { //当小于硬性灰尘数量时,就肯定是灰尘的
return true, nil
}
return false, nil
}

// NewInputOuts 因为 SignParam 的成员里有 []*wire.TxOut 类型的前置输出字段
// 但教程常用的是 pkScripts [][]byte 和 amounts []int64 两个属性
// 因此这里写个转换逻辑
Expand Down
35 changes: 0 additions & 35 deletions utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,6 @@ import (
"github.com/yyle88/gobtcsign/dogecoin"
)

func TestIsDustOutputDoge(t *testing.T) {
const dustLimitCoin = 0.001 //THE HARD DUST LIMIT IS SET AT 0.001 DOGE - OUTPUTS UNDER THIS VALUE ARE INVALID AND REJECTED. (https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md)
{
const amount = 500
isDust, err := IsDustOutputDoge(amount, dustLimitCoin)
require.NoError(t, err)
require.True(t, isDust)
t.Log("amount:", amount, "IS DUST IN DOGE")
}
{
const amount = 100000
isDust, err := IsDustOutputDoge(amount, dustLimitCoin)
require.NoError(t, err)
require.False(t, isDust)
t.Log("amount:", amount, "IS NOT DUST IN DOGE")
}
}

func TestIsDustOutputBtc(t *testing.T) {
amount := int64(100000)
for {
const address = "1MAgFFbMpgx6hTPvK3HY348Bbnwk6RFHm5"
const feePerKb = 1234 * 1000 //假设手续费是这个数
const dustLimitCoin = 546e-8 //BTC CHAIN: CONSIDERS ANYTHING BELOW 546 SATOSHIS (PARTS OF A BITCOIN) TO BE DUST.
isDust, err := IsDustOutputBtc(&chaincfg.MainNetParams, address, amount, dustLimitCoin, feePerKb)
require.NoError(t, err)
if !isDust {
break
}
amount++
}
t.Log("amount:", amount, "IS NOT DUST IN BTC")
t.Log("amount:", amount-1, "IS DUST IN BTC")
}

func TestGetAddressPkScript(t *testing.T) {
netParams := dogecoin.TestNetParams
pkScript := caseGetAddressPkScript(t, "nXMSrjEQXUJ77TQSeErpJMySy3kfSfwSCP", &netParams)
Expand Down
15 changes: 13 additions & 2 deletions verifytx.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,24 @@ func VerifySignV2(msgTx *wire.MsgTx, inputList []*VerifyTxInputParam, netParams
}

func VerifySignV3(msgTx *wire.MsgTx, inputsItem *VerifyTxInputsType) error {
inputFetcher, err := txauthor.TXPrevOutFetcher(msgTx, inputsItem.PkScripts, inputsItem.InAmounts)
prevScripts := inputsItem.PkScripts
inputValues := inputsItem.InAmounts

return VerifySignV4(msgTx, prevScripts, inputValues)
}

// VerifySignV4 这是验证签名的函数,代码主要参考这里
// https://github.com/btcsuite/btcwallet/blob/b4ff60753aaa3cf885fb09586755f67d41954942/wallet/createtx.go#L503
// 这个 github 官方包 是非常重要的参考资料
// https://github.com/btcsuite/btcwallet/blob/master/wallet/createtx.go
func VerifySignV4(msgTx *wire.MsgTx, prevScripts [][]byte, inputValues []btcutil.Amount) error {
inputFetcher, err := txauthor.TXPrevOutFetcher(msgTx, prevScripts, inputValues)
if err != nil {
return errors.WithMessage(err, "wrong cannot-create-pre-out-cache")
}
sigHashCache := txscript.NewTxSigHashes(msgTx, inputFetcher)

inputOuts := NewInputOutsV2(inputsItem.PkScripts, inputsItem.InAmounts)
inputOuts := NewInputOutsV2(prevScripts, inputValues)

return VerifySign(msgTx, inputOuts, inputFetcher, sigHashCache)
}

0 comments on commit a56ce48

Please sign in to comment.