Skip to content

Commit

Permalink
Merge pull request #50 from renproject/fix/utxo-gas-estimation
Browse files Browse the repository at this point in the history
UTXO Gas Estimation
  • Loading branch information
jazg authored Oct 22, 2020
2 parents 96463dd + d66a680 commit 07b2020
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 60 deletions.
19 changes: 19 additions & 0 deletions chain/bitcoin/bitcoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ type Client interface {
Confirmations(ctx context.Context, txHash pack.Bytes) (int64, error)
// EstimateSmartFee
EstimateSmartFee(ctx context.Context, numBlocks int64) (float64, error)
// EstimateFeeLegacy
EstimateFeeLegacy(ctx context.Context, numBlocks int64) (float64, error)
}

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

func (client *client) EstimateFeeLegacy(ctx context.Context, numBlocks int64) (float64, error) {
var resp float64

switch numBlocks {
case int64(0):
if err := client.send(ctx, &resp, "estimatefee"); err != nil {
return 0.0, fmt.Errorf("estimating fee: %v", err)
}
default:
if err := client.send(ctx, &resp, "estimatefee", numBlocks); err != nil {
return 0.0, fmt.Errorf("estimating fee: %v", err)
}
}

return resp, nil
}

func (client *client) send(ctx context.Context, resp interface{}, method string, params ...interface{}) error {
// Encode the request.
data, err := encodeRequest(method, params)
Expand Down
19 changes: 13 additions & 6 deletions chain/bitcoin/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bitcoin

import (
"context"
"fmt"
"math"

"github.com/renproject/pack"
Expand All @@ -18,16 +19,18 @@ const (
// important that all nodes in the network have reached consensus on the
// SATs-per-byte.
type GasEstimator struct {
client Client
numBlocks int64
client Client
numBlocks int64
fallbackGas pack.U256
}

// NewGasEstimator returns a simple gas estimator that always returns the given
// number of SATs-per-byte.
func NewGasEstimator(client Client, numBlocks int64) GasEstimator {
func NewGasEstimator(client Client, numBlocks int64, fallbackGas pack.U256) GasEstimator {
return GasEstimator{
client: client,
numBlocks: numBlocks,
client: client,
numBlocks: numBlocks,
fallbackGas: fallbackGas,
}
}

Expand All @@ -42,7 +45,11 @@ func NewGasEstimator(client Client, numBlocks int64) GasEstimator {
func (gasEstimator GasEstimator) EstimateGas(ctx context.Context) (pack.U256, pack.U256, error) {
feeRate, err := gasEstimator.client.EstimateSmartFee(ctx, gasEstimator.numBlocks)
if err != nil {
return pack.NewU256([32]byte{}), pack.NewU256([32]byte{}), err
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, err
}

if feeRate <= 0.0 {
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, fmt.Errorf("invalid fee rate: %v", feeRate)
}

satsPerByte := uint64(math.Ceil(feeRate * btcToSatoshis / kilobyteToByte))
Expand Down
28 changes: 20 additions & 8 deletions chain/bitcoin/gas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/renproject/multichain/chain/bitcoin"
"github.com/renproject/pack"

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

// estimate fee to include tx within 1 block.
gasEstimator1 := bitcoin.NewGasEstimator(client, 1)
fallback1 := uint64(123)
gasEstimator1 := bitcoin.NewGasEstimator(client, 1, pack.NewU256FromUint64(fallback1))
gasPrice1, _, err := gasEstimator1.EstimateGas(ctx)
Expect(err).NotTo(HaveOccurred())
if err != nil {
Expect(gasPrice1).To(Equal(pack.NewU256FromUint64(fallback1)))
}

// estimate fee to include tx within 10 blocks.
gasEstimator2 := bitcoin.NewGasEstimator(client, 10)
fallback2 := uint64(234)
gasEstimator2 := bitcoin.NewGasEstimator(client, 10, pack.NewU256FromUint64(fallback2))
gasPrice2, _, err := gasEstimator2.EstimateGas(ctx)
Expect(err).NotTo(HaveOccurred())
if err != nil {
Expect(gasPrice2).To(Equal(pack.NewU256FromUint64(fallback2)))
}

// estimate fee to include tx within 100 blocks.
gasEstimator3 := bitcoin.NewGasEstimator(client, 100)
fallback3 := uint64(345)
gasEstimator3 := bitcoin.NewGasEstimator(client, 100, pack.NewU256FromUint64(fallback3))
gasPrice3, _, err := gasEstimator3.EstimateGas(ctx)
Expect(err).NotTo(HaveOccurred())
if err != nil {
Expect(gasPrice3).To(Equal(pack.NewU256FromUint64(fallback3)))
}

// expect fees in this order at the very least.
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
if err == nil {
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
}
})
})
})
46 changes: 43 additions & 3 deletions chain/bitcoincash/gas.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,54 @@
package bitcoincash

import "github.com/renproject/multichain/chain/bitcoin"
import (
"context"
"fmt"
"math"

"github.com/renproject/pack"
)

const (
bchToSatoshis = 1e8
kilobyteToByte = 1024
)

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

// NewGasEstimator returns a simple gas estimator that always returns the given
// number of SATs-per-byte.
var NewGasEstimator = bitcoin.NewGasEstimator
func NewGasEstimator(client Client, fallbackGas pack.U256) GasEstimator {
return GasEstimator{
client: client,
fallbackGas: fallbackGas,
}
}

// EstimateGas returns the number of SATs-per-byte (for both price and cap) that
// is needed in order to confirm transactions with a minimal delay. It is the
// responsibility of the caller to know the number of bytes in their
// transaction. This method calls the `estimatefee` RPC call to the node, which
// based on a conservative (considering longer history) strategy returns the
// estimated BCH per kilobyte of data in the transaction. An error will be
// returned if the node hasn't observed enough blocks to make an estimate.
func (gasEstimator GasEstimator) EstimateGas(ctx context.Context) (pack.U256, pack.U256, error) {
feeRate, err := gasEstimator.client.EstimateFeeLegacy(ctx, int64(0))
if err != nil {
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, err
}

if feeRate <= 0.0 {
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, fmt.Errorf("invalid fee rate: %v", feeRate)
}

satsPerByte := uint64(math.Ceil(feeRate * bchToSatoshis / kilobyteToByte))
return pack.NewU256FromUint64(satsPerByte), pack.NewU256FromUint64(satsPerByte), nil
}
30 changes: 30 additions & 0 deletions chain/bitcoincash/gas_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
package bitcoincash_test

import (
"context"

"github.com/renproject/multichain/chain/bitcoincash"
"github.com/renproject/pack"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Gas", func() {
Context("when estimating bitcoincash network fee", func() {
It("should work", func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

client := bitcoincash.NewClient(bitcoincash.DefaultClientOptions())

fallbackGas := uint64(123)
gasEstimator := bitcoincash.NewGasEstimator(client, pack.NewU256FromUint64(fallbackGas))
gasPrice, _, err := gasEstimator.EstimateGas(ctx)
if err != nil {
Expect(gasPrice).To(Equal(pack.NewU256FromUint64(fallbackGas)))
} else {
Expect(gasPrice.Int().Uint64()).To(BeNumerically(">", 0))
}
})
})
})
51 changes: 51 additions & 0 deletions chain/dogecoin/gas_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
package dogecoin_test

import (
"context"

"github.com/renproject/multichain/chain/dogecoin"
"github.com/renproject/pack"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Gas", func() {
Context("when estimating dogecoin network fee", func() {
It("should work", func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

client := dogecoin.NewClient(dogecoin.DefaultClientOptions())

// estimate fee to include tx within 1 block.
fallback1 := uint64(123)
gasEstimator1 := dogecoin.NewGasEstimator(client, 1, pack.NewU256FromUint64(fallback1))
gasPrice1, _, err := gasEstimator1.EstimateGas(ctx)
if err != nil {
Expect(gasPrice1).To(Equal(pack.NewU256FromUint64(fallback1)))
}

// estimate fee to include tx within 10 blocks.
fallback2 := uint64(234)
gasEstimator2 := dogecoin.NewGasEstimator(client, 10, pack.NewU256FromUint64(fallback2))
gasPrice2, _, err := gasEstimator2.EstimateGas(ctx)
if err != nil {
Expect(gasPrice2).To(Equal(pack.NewU256FromUint64(fallback2)))
}

// estimate fee to include tx within 100 blocks.
fallback3 := uint64(345)
gasEstimator3 := dogecoin.NewGasEstimator(client, 100, pack.NewU256FromUint64(fallback3))
gasPrice3, _, err := gasEstimator3.EstimateGas(ctx)
if err != nil {
Expect(gasPrice3).To(Equal(pack.NewU256FromUint64(fallback3)))
}

// expect fees in this order at the very least.
if err == nil {
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
}
})
})
})
58 changes: 53 additions & 5 deletions chain/zcash/gas.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,57 @@
package zcash

import "github.com/renproject/multichain/chain/bitcoin"
import (
"context"
"fmt"
"math"

// GasEstimator re-exports bitcoin.GasEstimator
type GasEstimator = bitcoin.GasEstimator
"github.com/renproject/pack"
)

// NewGasEstimator re-exports bitcoin.NewGasEstimator
var NewGasEstimator = bitcoin.NewGasEstimator
const (
multiplier = 1e8
kilobyteToByte = 1024
)

// A GasEstimator returns the SATs-per-byte that is needed in order to confirm
// transactions with an estimated maximum delay of one block. In distributed
// networks that collectively build, sign, and submit transactions, it is
// important that all nodes in the network have reached consensus on the
// SATs-per-byte.
type GasEstimator struct {
client Client
numBlocks int64
fallbackGas pack.U256
}

// NewGasEstimator returns a simple gas estimator that always returns the given
// number of SATs-per-byte.
func NewGasEstimator(client Client, numBlocks int64, fallbackGas pack.U256) GasEstimator {
return GasEstimator{
client: client,
numBlocks: numBlocks,
fallbackGas: fallbackGas,
}
}

// EstimateGas returns the number of SATs-per-byte (for both price and cap) that
// is needed in order to confirm transactions with an estimated maximum delay of
// `numBlocks` block. It is the responsibility of the caller to know the number
// of bytes in their transaction. This method calls the `estimatesmartfee` RPC
// call to the node, which based on a conservative (considering longer history)
// strategy returns the estimated BTC per kilobyte of data in the transaction.
// An error will be returned if the bitcoin node hasn't observed enough blocks
// to make an estimate for the provided target `numBlocks`.
func (gasEstimator GasEstimator) EstimateGas(ctx context.Context) (pack.U256, pack.U256, error) {
feeRate, err := gasEstimator.client.EstimateFeeLegacy(ctx, gasEstimator.numBlocks)
if err != nil {
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, err
}

if feeRate <= 0.0 {
return gasEstimator.fallbackGas, gasEstimator.fallbackGas, fmt.Errorf("invalid fee rate: %v", feeRate)
}

satsPerByte := uint64(math.Ceil(feeRate * multiplier / kilobyteToByte))
return pack.NewU256FromUint64(satsPerByte), pack.NewU256FromUint64(satsPerByte), nil
}
51 changes: 51 additions & 0 deletions chain/zcash/gas_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
package zcash_test

import (
"context"

"github.com/renproject/multichain/chain/zcash"
"github.com/renproject/pack"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Gas", func() {
Context("when estimating zcash network fee", func() {
It("should work", func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

client := zcash.NewClient(zcash.DefaultClientOptions())

// estimate fee to include tx within 1 block.
fallback1 := uint64(123)
gasEstimator1 := zcash.NewGasEstimator(client, 1, pack.NewU256FromUint64(fallback1))
gasPrice1, _, err := gasEstimator1.EstimateGas(ctx)
if err != nil {
Expect(gasPrice1).To(Equal(pack.NewU256FromUint64(fallback1)))
}

// estimate fee to include tx within 10 blocks.
fallback2 := uint64(234)
gasEstimator2 := zcash.NewGasEstimator(client, 10, pack.NewU256FromUint64(fallback2))
gasPrice2, _, err := gasEstimator2.EstimateGas(ctx)
if err != nil {
Expect(gasPrice2).To(Equal(pack.NewU256FromUint64(fallback2)))
}

// estimate fee to include tx within 100 blocks.
fallback3 := uint64(345)
gasEstimator3 := zcash.NewGasEstimator(client, 100, pack.NewU256FromUint64(fallback3))
gasPrice3, _, err := gasEstimator3.EstimateGas(ctx)
if err != nil {
Expect(gasPrice3).To(Equal(pack.NewU256FromUint64(fallback3)))
}

// expect fees in this order at the very least.
if err == nil {
Expect(gasPrice1.GreaterThanEqual(gasPrice2)).To(BeTrue())
Expect(gasPrice2.GreaterThanEqual(gasPrice3)).To(BeTrue())
}
})
})
})
Loading

0 comments on commit 07b2020

Please sign in to comment.