Skip to content

Commit 07b2020

Browse files
authored
Merge pull request #50 from renproject/fix/utxo-gas-estimation
UTXO Gas Estimation
2 parents 96463dd + d66a680 commit 07b2020

File tree

12 files changed

+313
-60
lines changed

12 files changed

+313
-60
lines changed

chain/bitcoin/bitcoin.go

+19
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ type Client interface {
9090
Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error)
9191
// EstimateSmartFee
9292
EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error)
93+
// EstimateFeeLegacy
94+
EstimateFeeLegacy(ctx context.Context, numBlocks int64) (float64, error)
9395
}
9496

9597
type client struct {
@@ -259,6 +261,23 @@ func (client *client) EstimateSmartFee(ctx context.Context, numBlocks int64) (fl
259261
return *resp.FeeRate, nil
260262
}
261263

264+
func (client *client) EstimateFeeLegacy(ctx context.Context, numBlocks int64) (float64, error) {
265+
var resp float64
266+
267+
switch numBlocks {
268+
case int64(0):
269+
if err := client.send(ctx, &resp, "estimatefee"); err != nil {
270+
return 0.0, fmt.Errorf("estimating fee: %v", err)
271+
}
272+
default:
273+
if err := client.send(ctx, &resp, "estimatefee", numBlocks); err != nil {
274+
return 0.0, fmt.Errorf("estimating fee: %v", err)
275+
}
276+
}
277+
278+
return resp, nil
279+
}
280+
262281
func (client *client) send(ctx context.Context, resp interface{}, method string, params ...interface{}) error {
263282
// Encode the request.
264283
data, err := encodeRequest(method, params)

chain/bitcoin/gas.go

+13-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package bitcoin
22

33
import (
44
"context"
5+
"fmt"
56
"math"
67

78
"github.com/renproject/pack"
@@ -18,16 +19,18 @@ const (
1819
// important that all nodes in the network have reached consensus on the
1920
// SATs-per-byte.
2021
type GasEstimator struct {
21-
client Client
22-
numBlocks int64
22+
client Client
23+
numBlocks int64
24+
fallbackGas pack.U256
2325
}
2426

2527
// NewGasEstimator returns a simple gas estimator that always returns the given
2628
// number of SATs-per-byte.
27-
func NewGasEstimator(client Client, numBlocks int64) GasEstimator {
29+
func NewGasEstimator(client Client, numBlocks int64, fallbackGas pack.U256) GasEstimator {
2830
return GasEstimator{
29-
client: client,
30-
numBlocks: numBlocks,
31+
client: client,
32+
numBlocks: numBlocks,
33+
fallbackGas: fallbackGas,
3134
}
3235
}
3336

@@ -42,7 +45,11 @@ func NewGasEstimator(client Client, numBlocks int64) GasEstimator {
4245
func (gasEstimator GasEstimator) EstimateGas(ctx context.Context) (pack.U256, pack.U256, error) {
4346
feeRate, err := gasEstimator.client.EstimateSmartFee(ctx, gasEstimator.numBlocks)
4447
if err != nil {
45-
return pack.NewU256([32]byte{}), pack.NewU256([32]byte{}), err
48+
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, err
49+
}
50+
51+
if feeRate <= 0.0 {
52+
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, fmt.Errorf("invalid fee rate: %v", feeRate)
4653
}
4754

4855
satsPerByte := uint64(math.Ceil(feeRate * btcToSatoshis / kilobyteToByte))

chain/bitcoin/gas_test.go

+20-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/renproject/multichain/chain/bitcoin"
7+
"github.com/renproject/pack"
78

89
. "github.com/onsi/ginkgo"
910
. "github.com/onsi/gomega"
@@ -18,23 +19,34 @@ var _ = Describe("Gas", func() {
1819
client := bitcoin.NewClient(bitcoin.DefaultClientOptions())
1920

2021
// estimate fee to include tx within 1 block.
21-
gasEstimator1 := bitcoin.NewGasEstimator(client, 1)
22+
fallback1 := uint64(123)
23+
gasEstimator1 := bitcoin.NewGasEstimator(client, 1, pack.NewU256FromUint64(fallback1))
2224
gasPrice1, _, err := gasEstimator1.EstimateGas(ctx)
23-
Expect(err).NotTo(HaveOccurred())
25+
if err != nil {
26+
Expect(gasPrice1).To(Equal(pack.NewU256FromUint64(fallback1)))
27+
}
2428

2529
// estimate fee to include tx within 10 blocks.
26-
gasEstimator2 := bitcoin.NewGasEstimator(client, 10)
30+
fallback2 := uint64(234)
31+
gasEstimator2 := bitcoin.NewGasEstimator(client, 10, pack.NewU256FromUint64(fallback2))
2732
gasPrice2, _, err := gasEstimator2.EstimateGas(ctx)
28-
Expect(err).NotTo(HaveOccurred())
33+
if err != nil {
34+
Expect(gasPrice2).To(Equal(pack.NewU256FromUint64(fallback2)))
35+
}
2936

3037
// estimate fee to include tx within 100 blocks.
31-
gasEstimator3 := bitcoin.NewGasEstimator(client, 100)
38+
fallback3 := uint64(345)
39+
gasEstimator3 := bitcoin.NewGasEstimator(client, 100, pack.NewU256FromUint64(fallback3))
3240
gasPrice3, _, err := gasEstimator3.EstimateGas(ctx)
33-
Expect(err).NotTo(HaveOccurred())
41+
if err != nil {
42+
Expect(gasPrice3).To(Equal(pack.NewU256FromUint64(fallback3)))
43+
}
3444

3545
// expect fees in this order at the very least.
36-
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
37-
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
46+
if err == nil {
47+
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
48+
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
49+
}
3850
})
3951
})
4052
})

chain/bitcoincash/gas.go

+43-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,54 @@
11
package bitcoincash
22

3-
import "github.com/renproject/multichain/chain/bitcoin"
3+
import (
4+
"context"
5+
"fmt"
6+
"math"
7+
8+
"github.com/renproject/pack"
9+
)
10+
11+
const (
12+
bchToSatoshis = 1e8
13+
kilobyteToByte = 1024
14+
)
415

516
// A GasEstimator returns the SATs-per-byte that is needed in order to confirm
617
// transactions with an estimated maximum delay of one block. In distributed
718
// networks that collectively build, sign, and submit transactions, it is
819
// important that all nodes in the network have reached consensus on the
920
// SATs-per-byte.
10-
type GasEstimator = bitcoin.GasEstimator
21+
type GasEstimator struct {
22+
client Client
23+
fallbackGas pack.U256
24+
}
1125

1226
// NewGasEstimator returns a simple gas estimator that always returns the given
1327
// number of SATs-per-byte.
14-
var NewGasEstimator = bitcoin.NewGasEstimator
28+
func NewGasEstimator(client Client, fallbackGas pack.U256) GasEstimator {
29+
return GasEstimator{
30+
client: client,
31+
fallbackGas: fallbackGas,
32+
}
33+
}
34+
35+
// EstimateGas returns the number of SATs-per-byte (for both price and cap) that
36+
// is needed in order to confirm transactions with a minimal delay. It is the
37+
// responsibility of the caller to know the number of bytes in their
38+
// transaction. This method calls the `estimatefee` RPC call to the node, which
39+
// based on a conservative (considering longer history) strategy returns the
40+
// estimated BCH per kilobyte of data in the transaction. An error will be
41+
// returned if the node hasn't observed enough blocks to make an estimate.
42+
func (gasEstimator GasEstimator) EstimateGas(ctx context.Context) (pack.U256, pack.U256, error) {
43+
feeRate, err := gasEstimator.client.EstimateFeeLegacy(ctx, int64(0))
44+
if err != nil {
45+
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, err
46+
}
47+
48+
if feeRate <= 0.0 {
49+
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, fmt.Errorf("invalid fee rate: %v", feeRate)
50+
}
51+
52+
satsPerByte := uint64(math.Ceil(feeRate * bchToSatoshis / kilobyteToByte))
53+
return pack.NewU256FromUint64(satsPerByte), pack.NewU256FromUint64(satsPerByte), nil
54+
}

chain/bitcoincash/gas_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -1 +1,31 @@
11
package bitcoincash_test
2+
3+
import (
4+
"context"
5+
6+
"github.com/renproject/multichain/chain/bitcoincash"
7+
"github.com/renproject/pack"
8+
9+
. "github.com/onsi/ginkgo"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
var _ = Describe("Gas", func() {
14+
Context("when estimating bitcoincash network fee", func() {
15+
It("should work", func() {
16+
ctx, cancel := context.WithCancel(context.Background())
17+
defer cancel()
18+
19+
client := bitcoincash.NewClient(bitcoincash.DefaultClientOptions())
20+
21+
fallbackGas := uint64(123)
22+
gasEstimator := bitcoincash.NewGasEstimator(client, pack.NewU256FromUint64(fallbackGas))
23+
gasPrice, _, err := gasEstimator.EstimateGas(ctx)
24+
if err != nil {
25+
Expect(gasPrice).To(Equal(pack.NewU256FromUint64(fallbackGas)))
26+
} else {
27+
Expect(gasPrice.Int().Uint64()).To(BeNumerically(">", 0))
28+
}
29+
})
30+
})
31+
})

chain/dogecoin/gas_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -1 +1,52 @@
11
package dogecoin_test
2+
3+
import (
4+
"context"
5+
6+
"github.com/renproject/multichain/chain/dogecoin"
7+
"github.com/renproject/pack"
8+
9+
. "github.com/onsi/ginkgo"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
var _ = Describe("Gas", func() {
14+
Context("when estimating dogecoin network fee", func() {
15+
It("should work", func() {
16+
ctx, cancel := context.WithCancel(context.Background())
17+
defer cancel()
18+
19+
client := dogecoin.NewClient(dogecoin.DefaultClientOptions())
20+
21+
// estimate fee to include tx within 1 block.
22+
fallback1 := uint64(123)
23+
gasEstimator1 := dogecoin.NewGasEstimator(client, 1, pack.NewU256FromUint64(fallback1))
24+
gasPrice1, _, err := gasEstimator1.EstimateGas(ctx)
25+
if err != nil {
26+
Expect(gasPrice1).To(Equal(pack.NewU256FromUint64(fallback1)))
27+
}
28+
29+
// estimate fee to include tx within 10 blocks.
30+
fallback2 := uint64(234)
31+
gasEstimator2 := dogecoin.NewGasEstimator(client, 10, pack.NewU256FromUint64(fallback2))
32+
gasPrice2, _, err := gasEstimator2.EstimateGas(ctx)
33+
if err != nil {
34+
Expect(gasPrice2).To(Equal(pack.NewU256FromUint64(fallback2)))
35+
}
36+
37+
// estimate fee to include tx within 100 blocks.
38+
fallback3 := uint64(345)
39+
gasEstimator3 := dogecoin.NewGasEstimator(client, 100, pack.NewU256FromUint64(fallback3))
40+
gasPrice3, _, err := gasEstimator3.EstimateGas(ctx)
41+
if err != nil {
42+
Expect(gasPrice3).To(Equal(pack.NewU256FromUint64(fallback3)))
43+
}
44+
45+
// expect fees in this order at the very least.
46+
if err == nil {
47+
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
48+
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
49+
}
50+
})
51+
})
52+
})

chain/zcash/gas.go

+53-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,57 @@
11
package zcash
22

3-
import "github.com/renproject/multichain/chain/bitcoin"
3+
import (
4+
"context"
5+
"fmt"
6+
"math"
47

5-
// GasEstimator re-exports bitcoin.GasEstimator
6-
type GasEstimator = bitcoin.GasEstimator
8+
"github.com/renproject/pack"
9+
)
710

8-
// NewGasEstimator re-exports bitcoin.NewGasEstimator
9-
var NewGasEstimator = bitcoin.NewGasEstimator
11+
const (
12+
multiplier = 1e8
13+
kilobyteToByte = 1024
14+
)
15+
16+
// A GasEstimator returns the SATs-per-byte that is needed in order to confirm
17+
// transactions with an estimated maximum delay of one block. In distributed
18+
// networks that collectively build, sign, and submit transactions, it is
19+
// important that all nodes in the network have reached consensus on the
20+
// SATs-per-byte.
21+
type GasEstimator struct {
22+
client Client
23+
numBlocks int64
24+
fallbackGas pack.U256
25+
}
26+
27+
// NewGasEstimator returns a simple gas estimator that always returns the given
28+
// number of SATs-per-byte.
29+
func NewGasEstimator(client Client, numBlocks int64, fallbackGas pack.U256) GasEstimator {
30+
return GasEstimator{
31+
client: client,
32+
numBlocks: numBlocks,
33+
fallbackGas: fallbackGas,
34+
}
35+
}
36+
37+
// EstimateGas returns the number of SATs-per-byte (for both price and cap) that
38+
// is needed in order to confirm transactions with an estimated maximum delay of
39+
// `numBlocks` block. It is the responsibility of the caller to know the number
40+
// of bytes in their transaction. This method calls the `estimatesmartfee` RPC
41+
// call to the node, which based on a conservative (considering longer history)
42+
// strategy returns the estimated BTC per kilobyte of data in the transaction.
43+
// An error will be returned if the bitcoin node hasn't observed enough blocks
44+
// to make an estimate for the provided target `numBlocks`.
45+
func (gasEstimator GasEstimator) EstimateGas(ctx context.Context) (pack.U256, pack.U256, error) {
46+
feeRate, err := gasEstimator.client.EstimateFeeLegacy(ctx, gasEstimator.numBlocks)
47+
if err != nil {
48+
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, err
49+
}
50+
51+
if feeRate <= 0.0 {
52+
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, fmt.Errorf("invalid fee rate: %v", feeRate)
53+
}
54+
55+
satsPerByte := uint64(math.Ceil(feeRate * multiplier / kilobyteToByte))
56+
return pack.NewU256FromUint64(satsPerByte), pack.NewU256FromUint64(satsPerByte), nil
57+
}

chain/zcash/gas_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -1 +1,52 @@
11
package zcash_test
2+
3+
import (
4+
"context"
5+
6+
"github.com/renproject/multichain/chain/zcash"
7+
"github.com/renproject/pack"
8+
9+
. "github.com/onsi/ginkgo"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
var _ = Describe("Gas", func() {
14+
Context("when estimating zcash network fee", func() {
15+
It("should work", func() {
16+
ctx, cancel := context.WithCancel(context.Background())
17+
defer cancel()
18+
19+
client := zcash.NewClient(zcash.DefaultClientOptions())
20+
21+
// estimate fee to include tx within 1 block.
22+
fallback1 := uint64(123)
23+
gasEstimator1 := zcash.NewGasEstimator(client, 1, pack.NewU256FromUint64(fallback1))
24+
gasPrice1, _, err := gasEstimator1.EstimateGas(ctx)
25+
if err != nil {
26+
Expect(gasPrice1).To(Equal(pack.NewU256FromUint64(fallback1)))
27+
}
28+
29+
// estimate fee to include tx within 10 blocks.
30+
fallback2 := uint64(234)
31+
gasEstimator2 := zcash.NewGasEstimator(client, 10, pack.NewU256FromUint64(fallback2))
32+
gasPrice2, _, err := gasEstimator2.EstimateGas(ctx)
33+
if err != nil {
34+
Expect(gasPrice2).To(Equal(pack.NewU256FromUint64(fallback2)))
35+
}
36+
37+
// estimate fee to include tx within 100 blocks.
38+
fallback3 := uint64(345)
39+
gasEstimator3 := zcash.NewGasEstimator(client, 100, pack.NewU256FromUint64(fallback3))
40+
gasPrice3, _, err := gasEstimator3.EstimateGas(ctx)
41+
if err != nil {
42+
Expect(gasPrice3).To(Equal(pack.NewU256FromUint64(fallback3)))
43+
}
44+
45+
// expect fees in this order at the very least.
46+
if err == nil {
47+
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
48+
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
49+
}
50+
})
51+
})
52+
})

0 commit comments

Comments
 (0)