Skip to content

Commit 898dc9e

Browse files
authored
Estimate redemption transaction fee during proposal (#3651)
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.
2 parents 2750d03 + dc866e4 commit 898dc9e

File tree

5 files changed

+185
-4
lines changed

5 files changed

+185
-4
lines changed

cmd/coordinator.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ var proposeRedemptionCommand = cobra.Command{
261261
)
262262
}
263263

264+
btcChain, err := electrum.Connect(cmd.Context(), clientConfig.Bitcoin.Electrum)
265+
if err != nil {
266+
return fmt.Errorf("could not connect to Electrum chain: [%v]", err)
267+
}
268+
264269
var walletPublicKeyHash [20]byte
265270
if len(wallet) > 0 {
266271
var err error
@@ -296,6 +301,7 @@ var proposeRedemptionCommand = cobra.Command{
296301

297302
return coordinator.ProposeRedemption(
298303
tbtcChain,
304+
btcChain,
299305
walletPublicKeyHash,
300306
fee,
301307
redemptions,

pkg/bitcoin/script.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import (
99
"github.com/btcsuite/btcutil"
1010
)
1111

12+
// ScriptType represents the possible types of Script.
13+
type ScriptType uint8
14+
15+
const (
16+
NonStandardScript ScriptType = iota
17+
P2PKHScript
18+
P2WPKHScript
19+
P2SHScript
20+
P2WSHScript
21+
)
22+
1223
// Script represents an arbitrary Bitcoin script, NOT prepended with the
1324
// byte-length of the script
1425
type Script []byte
@@ -119,3 +130,19 @@ func PayToScriptHash(scriptHash [20]byte) (Script, error) {
119130
AddOp(txscript.OP_EQUAL).
120131
Script()
121132
}
133+
134+
// GetScriptType gets the ScriptType of the given Script.
135+
func GetScriptType(script Script) ScriptType {
136+
switch txscript.GetScriptClass(script) {
137+
case txscript.PubKeyHashTy:
138+
return P2PKHScript
139+
case txscript.WitnessV0PubKeyHashTy:
140+
return P2WPKHScript
141+
case txscript.ScriptHashTy:
142+
return P2SHScript
143+
case txscript.WitnessV0ScriptHashTy:
144+
return P2WSHScript
145+
default:
146+
return NonStandardScript
147+
}
148+
}

pkg/bitcoin/script_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,56 @@ func TestPayToScriptHash(t *testing.T) {
333333

334334
testutils.AssertBytesEqual(t, expectedResult, result[:])
335335
}
336+
337+
func TestGetScriptType(t *testing.T) {
338+
fromHex := func(hexString string) []byte {
339+
bytes, err := hex.DecodeString(hexString)
340+
if err != nil {
341+
t.Fatal(err)
342+
}
343+
return bytes
344+
}
345+
346+
var tests = map[string]struct {
347+
script Script
348+
expectedType ScriptType
349+
}{
350+
"p2pkh script": {
351+
script: fromHex("76a9148db50eb52063ea9d98b3eac91489a90f738986f688ac"),
352+
expectedType: P2PKHScript,
353+
},
354+
"p2wpkh script": {
355+
script: fromHex("00148db50eb52063ea9d98b3eac91489a90f738986f6"),
356+
expectedType: P2WPKHScript,
357+
},
358+
"p2sh script": {
359+
script: fromHex("a9143ec459d0f3c29286ae5df5fcc421e2786024277e87"),
360+
expectedType: P2SHScript,
361+
},
362+
"p2wsh script": {
363+
script: fromHex("002086a303cdd2e2eab1d1679f1a813835dc5a1b65321077cdccaf08f98cbf04ca96"),
364+
expectedType: P2WSHScript,
365+
},
366+
"non-standard script": {
367+
script: fromHex(
368+
"14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d0003" +
369+
"95237576a9148db50eb52063ea9d98b3eac91489a90f738986f68763ac6776a" +
370+
"91428e081f285138ccbe389c1eb8985716230129f89880460bcea61b175ac68",
371+
),
372+
expectedType: NonStandardScript,
373+
},
374+
}
375+
376+
for testName, test := range tests {
377+
t.Run(testName, func(t *testing.T) {
378+
actualType := GetScriptType(test.script)
379+
380+
testutils.AssertIntsEqual(
381+
t,
382+
"script type",
383+
int(test.expectedType),
384+
int(actualType),
385+
)
386+
})
387+
}
388+
}

pkg/coordinator/redemptions.go

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import (
1414
)
1515

1616
type redemptionEntry struct {
17-
walletPublicKeyHash [20]byte
18-
17+
walletPublicKeyHash [20]byte
1918
redemptionKey string
2019
redeemerOutputScript bitcoin.Script
2120
requestedAt time.Time
@@ -143,6 +142,7 @@ func FindPendingRedemptions(
143142
// ProposeRedemption handles redemption proposal submission.
144143
func ProposeRedemption(
145144
chain Chain,
145+
btcChain bitcoin.Chain,
146146
walletPublicKeyHash [20]byte,
147147
fee int64,
148148
redeemersOutputScripts []bitcoin.Script,
@@ -152,9 +152,25 @@ func ProposeRedemption(
152152
return fmt.Errorf("redemptions list is empty")
153153
}
154154

155-
// Estimate fee if it's missing.
155+
// Estimate fee if it's missing. Do not check the estimated fee against
156+
// the maximum total and per-request fees allowed by the Bridge. This
157+
// is done during the on-chain validation of the proposal so there is no
158+
// need to do it here.
156159
if fee <= 0 {
157-
panic("fee estimation not implemented yet")
160+
logger.Infof("estimating redemption transaction fee...")
161+
162+
estimatedFee, err := EstimateRedemptionFee(
163+
btcChain,
164+
redeemersOutputScripts,
165+
)
166+
if err != nil {
167+
return fmt.Errorf(
168+
"cannot estimate redemption transaction fee: [%w]",
169+
err,
170+
)
171+
}
172+
173+
fee = estimatedFee
158174
}
159175

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

320336
return result, nil
321337
}
338+
339+
func EstimateRedemptionFee(
340+
btcChain bitcoin.Chain,
341+
redeemersOutputScripts []bitcoin.Script,
342+
) (int64, error) {
343+
sizeEstimator := bitcoin.NewTransactionSizeEstimator().
344+
// 1 P2WPKH main UTXO input.
345+
AddPublicKeyHashInputs(1, true).
346+
// 1 P2WPKH change output.
347+
AddPublicKeyHashOutputs(1, true)
348+
349+
for _, script := range redeemersOutputScripts {
350+
switch bitcoin.GetScriptType(script) {
351+
case bitcoin.P2PKHScript:
352+
sizeEstimator.AddPublicKeyHashOutputs(1, false)
353+
case bitcoin.P2WPKHScript:
354+
sizeEstimator.AddPublicKeyHashOutputs(1, true)
355+
case bitcoin.P2SHScript:
356+
sizeEstimator.AddScriptHashOutputs(1, false)
357+
case bitcoin.P2WSHScript:
358+
sizeEstimator.AddScriptHashOutputs(1, true)
359+
default:
360+
return 0, fmt.Errorf("non-standard redeemer output script type")
361+
}
362+
}
363+
364+
transactionSize, err := sizeEstimator.VirtualSize()
365+
if err != nil {
366+
return 0, fmt.Errorf("cannot estimate transaction virtual size: [%v]", err)
367+
}
368+
369+
feeEstimator := bitcoin.NewTransactionFeeEstimator(btcChain)
370+
371+
totalFee, err := feeEstimator.EstimateFee(transactionSize)
372+
if err != nil {
373+
return 0, fmt.Errorf("cannot estimate transaction fee: [%v]", err)
374+
}
375+
376+
return totalFee, nil
377+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package coordinator_test
2+
3+
import (
4+
"encoding/hex"
5+
"github.com/keep-network/keep-core/internal/testutils"
6+
"github.com/keep-network/keep-core/pkg/bitcoin"
7+
"github.com/keep-network/keep-core/pkg/coordinator"
8+
"testing"
9+
)
10+
11+
// Test based on example testnet redemption transaction:
12+
// https://live.blockcypher.com/btc-testnet/tx/2724545276df61f43f1e92c4b9f1dd3c9109595c022dbd9dc003efbad8ded38b
13+
func TestEstimateRedemptionFee(t *testing.T) {
14+
fromHex := func(hexString string) []byte {
15+
bytes, err := hex.DecodeString(hexString)
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
return bytes
20+
}
21+
22+
btcChain := newLocalBitcoinChain()
23+
btcChain.setEstimateSatPerVByteFee(1, 16)
24+
25+
redeemersOutputScripts := []bitcoin.Script{
26+
fromHex("76a9142cd680318747b720d67bf4246eb7403b476adb3488ac"), // P2PKH
27+
fromHex("0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0"), // P2WPKH
28+
fromHex("a914011beb6fb8499e075a57027fb0a58384f2d3f78487"), // P2SH
29+
fromHex("0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c"), // P2WSH
30+
}
31+
32+
actualFee, err := coordinator.EstimateRedemptionFee(btcChain, redeemersOutputScripts)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
37+
expectedFee := 4000 // transactionVirtualSize * satPerVByteFee = 250 * 16 = 4000
38+
testutils.AssertIntsEqual(t, "fee", expectedFee, int(actualFee))
39+
}

0 commit comments

Comments
 (0)