Skip to content

Commit

Permalink
Estimate redemption transaction fee during proposal (#3651)
Browse files Browse the repository at this point in the history
Here we add a function allowing us to estimate the redemption
transaction total fee (`coordinator.EstimateRedemptionFee`) and we
integrate it with the existing `coordinator.ProposeRedemption` function.
  • Loading branch information
pdyraga authored Jun 28, 2023
2 parents 2750d03 + dc866e4 commit 898dc9e
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 4 deletions.
6 changes: 6 additions & 0 deletions cmd/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ var proposeRedemptionCommand = cobra.Command{
)
}

btcChain, err := electrum.Connect(cmd.Context(), clientConfig.Bitcoin.Electrum)
if err != nil {
return fmt.Errorf("could not connect to Electrum chain: [%v]", err)
}

var walletPublicKeyHash [20]byte
if len(wallet) > 0 {
var err error
Expand Down Expand Up @@ -296,6 +301,7 @@ var proposeRedemptionCommand = cobra.Command{

return coordinator.ProposeRedemption(
tbtcChain,
btcChain,
walletPublicKeyHash,
fee,
redemptions,
Expand Down
27 changes: 27 additions & 0 deletions pkg/bitcoin/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import (
"github.com/btcsuite/btcutil"
)

// ScriptType represents the possible types of Script.
type ScriptType uint8

const (
NonStandardScript ScriptType = iota
P2PKHScript
P2WPKHScript
P2SHScript
P2WSHScript
)

// Script represents an arbitrary Bitcoin script, NOT prepended with the
// byte-length of the script
type Script []byte
Expand Down Expand Up @@ -119,3 +130,19 @@ func PayToScriptHash(scriptHash [20]byte) (Script, error) {
AddOp(txscript.OP_EQUAL).
Script()
}

// GetScriptType gets the ScriptType of the given Script.
func GetScriptType(script Script) ScriptType {
switch txscript.GetScriptClass(script) {
case txscript.PubKeyHashTy:
return P2PKHScript
case txscript.WitnessV0PubKeyHashTy:
return P2WPKHScript
case txscript.ScriptHashTy:
return P2SHScript
case txscript.WitnessV0ScriptHashTy:
return P2WSHScript
default:
return NonStandardScript
}
}
53 changes: 53 additions & 0 deletions pkg/bitcoin/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,56 @@ func TestPayToScriptHash(t *testing.T) {

testutils.AssertBytesEqual(t, expectedResult, result[:])
}

func TestGetScriptType(t *testing.T) {
fromHex := func(hexString string) []byte {
bytes, err := hex.DecodeString(hexString)
if err != nil {
t.Fatal(err)
}
return bytes
}

var tests = map[string]struct {
script Script
expectedType ScriptType
}{
"p2pkh script": {
script: fromHex("76a9148db50eb52063ea9d98b3eac91489a90f738986f688ac"),
expectedType: P2PKHScript,
},
"p2wpkh script": {
script: fromHex("00148db50eb52063ea9d98b3eac91489a90f738986f6"),
expectedType: P2WPKHScript,
},
"p2sh script": {
script: fromHex("a9143ec459d0f3c29286ae5df5fcc421e2786024277e87"),
expectedType: P2SHScript,
},
"p2wsh script": {
script: fromHex("002086a303cdd2e2eab1d1679f1a813835dc5a1b65321077cdccaf08f98cbf04ca96"),
expectedType: P2WSHScript,
},
"non-standard script": {
script: fromHex(
"14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d0003" +
"95237576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776a" +
"91428e081f285138ccbe389c1eb8985716230129f89880460bcea61b175ac68",
),
expectedType: NonStandardScript,
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
actualType := GetScriptType(test.script)

testutils.AssertIntsEqual(
t,
"script type",
int(test.expectedType),
int(actualType),
)
})
}
}
64 changes: 60 additions & 4 deletions pkg/coordinator/redemptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import (
)

type redemptionEntry struct {
walletPublicKeyHash [20]byte

walletPublicKeyHash [20]byte
redemptionKey string
redeemerOutputScript bitcoin.Script
requestedAt time.Time
Expand Down Expand Up @@ -143,6 +142,7 @@ func FindPendingRedemptions(
// ProposeRedemption handles redemption proposal submission.
func ProposeRedemption(
chain Chain,
btcChain bitcoin.Chain,
walletPublicKeyHash [20]byte,
fee int64,
redeemersOutputScripts []bitcoin.Script,
Expand All @@ -152,9 +152,25 @@ func ProposeRedemption(
return fmt.Errorf("redemptions list is empty")
}

// Estimate fee if it's missing.
// Estimate fee if it's missing. Do not check the estimated fee against
// the maximum total and per-request fees allowed by the Bridge. This
// is done during the on-chain validation of the proposal so there is no
// need to do it here.
if fee <= 0 {
panic("fee estimation not implemented yet")
logger.Infof("estimating redemption transaction fee...")

estimatedFee, err := EstimateRedemptionFee(
btcChain,
redeemersOutputScripts,
)
if err != nil {
return fmt.Errorf(
"cannot estimate redemption transaction fee: [%w]",
err,
)
}

fee = estimatedFee
}

logger.Infof("redemption transaction fee: [%d]", fee)
Expand Down Expand Up @@ -319,3 +335,43 @@ redemptionRequestedLoop:

return result, nil
}

func EstimateRedemptionFee(
btcChain bitcoin.Chain,
redeemersOutputScripts []bitcoin.Script,
) (int64, error) {
sizeEstimator := bitcoin.NewTransactionSizeEstimator().
// 1 P2WPKH main UTXO input.
AddPublicKeyHashInputs(1, true).
// 1 P2WPKH change output.
AddPublicKeyHashOutputs(1, true)

for _, script := range redeemersOutputScripts {
switch bitcoin.GetScriptType(script) {
case bitcoin.P2PKHScript:
sizeEstimator.AddPublicKeyHashOutputs(1, false)
case bitcoin.P2WPKHScript:
sizeEstimator.AddPublicKeyHashOutputs(1, true)
case bitcoin.P2SHScript:
sizeEstimator.AddScriptHashOutputs(1, false)
case bitcoin.P2WSHScript:
sizeEstimator.AddScriptHashOutputs(1, true)
default:
return 0, fmt.Errorf("non-standard redeemer output script type")
}
}

transactionSize, err := sizeEstimator.VirtualSize()
if err != nil {
return 0, fmt.Errorf("cannot estimate transaction virtual size: [%v]", err)
}

feeEstimator := bitcoin.NewTransactionFeeEstimator(btcChain)

totalFee, err := feeEstimator.EstimateFee(transactionSize)
if err != nil {
return 0, fmt.Errorf("cannot estimate transaction fee: [%v]", err)
}

return totalFee, nil
}
39 changes: 39 additions & 0 deletions pkg/coordinator/redemptions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package coordinator_test

import (
"encoding/hex"
"github.com/keep-network/keep-core/internal/testutils"
"github.com/keep-network/keep-core/pkg/bitcoin"
"github.com/keep-network/keep-core/pkg/coordinator"
"testing"
)

// Test based on example testnet redemption transaction:
// https://live.blockcypher.com/btc-testnet/tx/2724545276df61f43f1e92c4b9f1dd3c9109595c022dbd9dc003efbad8ded38b
func TestEstimateRedemptionFee(t *testing.T) {
fromHex := func(hexString string) []byte {
bytes, err := hex.DecodeString(hexString)
if err != nil {
t.Fatal(err)
}
return bytes
}

btcChain := newLocalBitcoinChain()
btcChain.setEstimateSatPerVByteFee(1, 16)

redeemersOutputScripts := []bitcoin.Script{
fromHex("76a9142cd680318747b720d67bf4246eb7403b476adb3488ac"), // P2PKH
fromHex("0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0"), // P2WPKH
fromHex("a914011beb6fb8499e075a57027fb0a58384f2d3f78487"), // P2SH
fromHex("0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c"), // P2WSH
}

actualFee, err := coordinator.EstimateRedemptionFee(btcChain, redeemersOutputScripts)
if err != nil {
t.Fatal(err)
}

expectedFee := 4000 // transactionVirtualSize * satPerVByteFee = 250 * 16 = 4000
testutils.AssertIntsEqual(t, "fee", expectedFee, int(actualFee))
}

0 comments on commit 898dc9e

Please sign in to comment.