diff --git a/dogecoin/dogedust.go b/dogecoin/dogedust.go index b0dc27a..e6b8746 100644 --- a/dogecoin/dogedust.go +++ b/dogecoin/dogedust.go @@ -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 @@ -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中的灰尘规定比较简单,它不依赖于费率,而是直接和常量比较,逻辑简单 + }) +} diff --git a/internal/dusts/txout_dust.go b/internal/dusts/txout_dust.go new file mode 100644 index 0000000..57da45e --- /dev/null +++ b/internal/dusts/txout_dust.go @@ -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) +} diff --git a/txdust.go b/txdust.go new file mode 100644 index 0000000..fe7e732 --- /dev/null +++ b/txdust.go @@ -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) +} diff --git a/txdust_test.go b/txdust_test.go new file mode 100644 index 0000000..4a565b5 --- /dev/null +++ b/txdust_test.go @@ -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") + } +} diff --git a/txfee.go b/txfee.go index 754e02c..e040291 100644 --- a/txfee.go +++ b/txfee.go @@ -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/txauthor@v1.3.4/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") @@ -55,6 +51,8 @@ func EstimateTxSize(param *CustomParam, netParams *chaincfg.Params, change *Chan // EstimateSize 计算交易的预估大小(在最坏情况下的预估大小) // 这个函数还是抄的 github.com/btcsuite/btcwallet/wallet/txauthor@v1.3.4/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() @@ -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) @@ -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 { @@ -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 { diff --git a/txfee_test.go b/txfee_test.go index ab34741..82e6413 100644 --- a/txfee_test.go +++ b/txfee_test.go @@ -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) diff --git a/utils.go b/utils.go index 70d5199..55a09c3 100644 --- a/utils.go +++ b/utils.go @@ -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" ) @@ -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 两个属性 // 因此这里写个转换逻辑 diff --git a/utils_test.go b/utils_test.go index 7d8b458..33004a4 100644 --- a/utils_test.go +++ b/utils_test.go @@ -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) diff --git a/verifytx.go b/verifytx.go index 1a95662..a02482c 100644 --- a/verifytx.go +++ b/verifytx.go @@ -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) }