Skip to content

Commit 2fc2930

Browse files
authored
Merge pull request #39 from renproject/feat/bitcoin-fee-estimation
bitcoin fee estimation
2 parents c1e7581 + 4d2ba3e commit 2fc2930

File tree

5 files changed

+95
-10
lines changed

5 files changed

+95
-10
lines changed

Diff for: chain/bitcoin/bitcoin.go

+20
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ type Client interface {
8888
UnspentOutputs(ctx context.Context, minConf, maxConf int64, address address.Address) ([]utxo.Output, error)
8989
// Confirmations of a transaction in the Bitcoin network.
9090
Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error)
91+
// EstimateSmartFee
92+
EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error)
9193
}
9294

9395
type client struct {
@@ -239,6 +241,24 @@ func (client *client) Confirmations(ctx context.Context, txHash pack.Bytes) (int
239241
return confirmations, nil
240242
}
241243

244+
// EstimateSmartFee fetches the estimated bitcoin network fees to be paid (in
245+
// BTC per kilobyte) needed for a transaction to be confirmed within `numBlocks`
246+
// blocks. An error will be returned if the bitcoin node hasn't observed enough
247+
// blocks to make an estimate for the provided target `numBlocks`.
248+
func (client *client) EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error) {
249+
resp := btcjson.EstimateSmartFeeResult{}
250+
251+
if err := client.send(ctx, &resp, "estimatesmartfee", numBlocks); err != nil {
252+
return 0.0, fmt.Errorf("estimating smart fee: %v", err)
253+
}
254+
255+
if resp.Errors != nil && len(resp.Errors) > 0 {
256+
return 0.0, fmt.Errorf("estimating smart fee: %v", resp.Errors[0])
257+
}
258+
259+
return *resp.FeeRate, nil
260+
}
261+
242262
func (client *client) send(ctx context.Context, resp interface{}, method string, params ...interface{}) error {
243263
// Encode the request.
244264
data, err := encodeRequest(method, params)

Diff for: chain/bitcoin/gas.go

+25-8
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,44 @@ import (
66
"github.com/renproject/pack"
77
)
88

9+
const (
10+
btcToSatoshis = 1e8
11+
kilobyteToByte = 1024
12+
)
13+
914
// A GasEstimator returns the SATs-per-byte that is needed in order to confirm
1015
// transactions with an estimated maximum delay of one block. In distributed
1116
// networks that collectively build, sign, and submit transactions, it is
1217
// important that all nodes in the network have reached consensus on the
1318
// SATs-per-byte.
1419
type GasEstimator struct {
15-
satsPerByte pack.U256
20+
client Client
21+
numBlocks int64
1622
}
1723

1824
// NewGasEstimator returns a simple gas estimator that always returns the given
1925
// number of SATs-per-byte.
20-
func NewGasEstimator(satsPerByte pack.U256) GasEstimator {
26+
func NewGasEstimator(client Client, numBlocks int64) GasEstimator {
2127
return GasEstimator{
22-
satsPerByte: satsPerByte,
28+
client: client,
29+
numBlocks: numBlocks,
2330
}
2431
}
2532

2633
// EstimateGasPrice returns the number of SATs-per-byte that is needed in order
27-
// to confirm transactions with an estimated maximum delay of one block. It is
28-
// the responsibility of the caller to know the number of bytes in their
29-
// transaction.
30-
func (gasEstimator GasEstimator) EstimateGasPrice(_ context.Context) (pack.U256, pack.U256, error) {
31-
return gasEstimator.satsPerByte, pack.NewU256([32]byte{}), nil
34+
// to confirm transactions with an estimated maximum delay of `numBlocks` block.
35+
// It is the responsibility of the caller to know the number of bytes in their
36+
// transaction. This method calls the `estimatesmartfee` RPC call to the node,
37+
// which based on a conservative (considering longer history) strategy returns
38+
// the estimated BTC per kilobyte of data in the transaction. An error will be
39+
// returned if the bitcoin node hasn't observed enough blocks to make an
40+
// estimate for the provided target `numBlocks`.
41+
func (gasEstimator GasEstimator) EstimateGasPrice(ctx context.Context) (pack.U256, pack.U256, error) {
42+
feeRate, err := gasEstimator.client.EstimateSmartFee(ctx, gasEstimator.numBlocks)
43+
if err != nil {
44+
return pack.NewU256([32]byte{}), pack.NewU256([32]byte{}), err
45+
}
46+
47+
satsPerByte := uint64(feeRate * btcToSatoshis / kilobyteToByte)
48+
return pack.NewU256FromU64(pack.NewU64(satsPerByte)), pack.NewU256([32]byte{}), nil
3249
}

Diff for: chain/bitcoin/gas_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -1 +1,40 @@
11
package bitcoin_test
2+
3+
import (
4+
"context"
5+
6+
"github.com/renproject/multichain/chain/bitcoin"
7+
8+
. "github.com/onsi/ginkgo"
9+
. "github.com/onsi/gomega"
10+
)
11+
12+
var _ = Describe("Gas", func() {
13+
Context("when estimating bitcoin network fee", func() {
14+
It("should work", func() {
15+
ctx, cancel := context.WithCancel(context.Background())
16+
defer cancel()
17+
18+
client := bitcoin.NewClient(bitcoin.DefaultClientOptions())
19+
20+
// estimate fee to include tx within 1 block.
21+
gasEstimator1 := bitcoin.NewGasEstimator(client, 1)
22+
gasPrice1, _, err := gasEstimator1.EstimateGasPrice(ctx)
23+
Expect(err).NotTo(HaveOccurred())
24+
25+
// estimate fee to include tx within 10 blocks.
26+
gasEstimator2 := bitcoin.NewGasEstimator(client, 10)
27+
gasPrice2, _, err := gasEstimator2.EstimateGasPrice(ctx)
28+
Expect(err).NotTo(HaveOccurred())
29+
30+
// estimate fee to include tx within 100 blocks.
31+
gasEstimator3 := bitcoin.NewGasEstimator(client, 100)
32+
gasPrice3, _, err := gasEstimator3.EstimateGasPrice(ctx)
33+
Expect(err).NotTo(HaveOccurred())
34+
35+
// expect fees in this order at the very least.
36+
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
37+
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
38+
})
39+
})
40+
})

Diff for: infra/bitcoin/run.sh

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/bin/bash
22
ADDRESS=$1
3+
PRIV_KEY=$2
34

45
# Start
56
/app/bin/bitcoind
@@ -11,12 +12,19 @@ echo "BITCOIN_ADDRESS=$ADDRESS"
1112
# Import the address
1213
/app/bin/bitcoin-cli importaddress $ADDRESS
1314

15+
# Import the private key to spend UTXOs
16+
/app/bin/bitcoin-cli importprivkey $PRIV_KEY
17+
1418
# Generate enough block to pass the maturation time
1519
/app/bin/bitcoin-cli generatetoaddress 101 $ADDRESS
1620

1721
# Simulate mining
1822
while :
1923
do
24+
# generate new btc to the address
2025
/app/bin/bitcoin-cli generatetoaddress 1 $ADDRESS
21-
sleep 10
22-
done
26+
sleep 5
27+
# send tx to own address while paying fee to the miner
28+
/app/bin/bitcoin-cli sendtoaddress $ADDRESS 0.5 "" "" true
29+
sleep 5
30+
done

Diff for: infra/docker-compose.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ services:
3737
entrypoint:
3838
- "./root/run.sh"
3939
- "${BITCOIN_ADDRESS}"
40+
- "${BITCOIN_PK}"
4041

4142
#
4243
# Bitcoin Cash

0 commit comments

Comments
 (0)