Skip to content

Commit b5b548f

Browse files
authored
Support redeem tx with 0 fee (arkade-os#437)
* ALLOW_ZERO_FEES flag * fix e2e tests * use zero fee in covenantless alice_to_bob.go
1 parent a7f9fbd commit b5b548f

File tree

14 files changed

+75
-31
lines changed

14 files changed

+75
-31
lines changed

client/main.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ var (
113113
Name: "amount",
114114
Usage: "amount to send in sats",
115115
}
116+
zeroFeesFlag = &cli.BoolFlag{
117+
Name: "zero-fees",
118+
Aliases: []string{"z"},
119+
Usage: "UNSAFE: allow sending offchain transactions with zero fees, disable unilateral exit",
120+
Value: false,
121+
}
116122
enableExpiryCoinselectFlag = &cli.BoolFlag{
117123
Name: "enable-expiry-coinselect",
118124
Usage: "select VTXOs about to expire first",
@@ -200,7 +206,7 @@ var (
200206
Action: func(ctx *cli.Context) error {
201207
return send(ctx)
202208
},
203-
Flags: []cli.Flag{receiversFlag, toFlag, amountFlag, enableExpiryCoinselectFlag, passwordFlag},
209+
Flags: []cli.Flag{receiversFlag, toFlag, amountFlag, enableExpiryCoinselectFlag, passwordFlag, zeroFeesFlag},
204210
}
205211
redeemCommand = cli.Command{
206212
Name: "redeem",
@@ -327,6 +333,7 @@ func send(ctx *cli.Context) error {
327333
receiversJSON := ctx.String(receiversFlag.Name)
328334
to := ctx.String(toFlag.Name)
329335
amount := ctx.Uint64(amountFlag.Name)
336+
zeroFees := ctx.Bool(zeroFeesFlag.Name)
330337
if receiversJSON == "" && to == "" && amount == 0 {
331338
return fmt.Errorf("missing destination, use --to and --amount or --receivers")
332339
}
@@ -366,7 +373,7 @@ func send(ctx *cli.Context) error {
366373
}
367374

368375
if isBitcoin {
369-
return sendCovenantLess(ctx, receivers)
376+
return sendCovenantLess(ctx, receivers, zeroFees)
370377
}
371378
return sendCovenant(ctx, receivers)
372379
}
@@ -526,7 +533,7 @@ func parseReceivers(receveirsJSON string, isBitcoin bool) ([]arksdk.Receiver, er
526533
return receivers, nil
527534
}
528535

529-
func sendCovenantLess(ctx *cli.Context, receivers []arksdk.Receiver) error {
536+
func sendCovenantLess(ctx *cli.Context, receivers []arksdk.Receiver, withZeroFees bool) error {
530537
var onchainReceivers, offchainReceivers []arksdk.Receiver
531538

532539
for _, receiver := range receivers {
@@ -547,7 +554,7 @@ func sendCovenantLess(ctx *cli.Context, receivers []arksdk.Receiver) error {
547554

548555
computeExpiration := ctx.Bool(enableExpiryCoinselectFlag.Name)
549556
redeemTx, err := arkSdkClient.SendOffChain(
550-
ctx.Context, computeExpiration, offchainReceivers,
557+
ctx.Context, computeExpiration, offchainReceivers, withZeroFees,
551558
)
552559
if err != nil {
553560
return err
@@ -581,7 +588,7 @@ func sendCovenant(ctx *cli.Context, receivers []arksdk.Receiver) error {
581588

582589
computeExpiration := ctx.Bool(enableExpiryCoinselectFlag.Name)
583590
txid, err := arkSdkClient.SendOffChain(
584-
ctx.Context, computeExpiration, offchainReceivers,
591+
ctx.Context, computeExpiration, offchainReceivers, false,
585592
)
586593
if err != nil {
587594
return err

pkg/client-sdk/ark_sdk.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type ArkClient interface {
1919
SendOnChain(ctx context.Context, receivers []Receiver) (string, error)
2020
SendOffChain(
2121
ctx context.Context, withExpiryCoinselect bool, receivers []Receiver,
22+
withZeroFees bool,
2223
) (string, error)
2324
UnilateralRedeem(ctx context.Context) error
2425
CollaborativeRedeem(

pkg/client-sdk/covenant_client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ func (a *covenantArkClient) SendOnChain(
420420
func (a *covenantArkClient) SendOffChain(
421421
ctx context.Context,
422422
withExpiryCoinselect bool, receivers []Receiver,
423+
_ bool,
423424
) (string, error) {
424425
for _, receiver := range receivers {
425426
if receiver.IsOnchain() {

pkg/client-sdk/covenantless_client.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,7 @@ func (a *covenantlessArkClient) SendOnChain(
707707
func (a *covenantlessArkClient) SendOffChain(
708708
ctx context.Context,
709709
withExpiryCoinselect bool, receivers []Receiver,
710+
withZeroFees bool,
710711
) (string, error) {
711712
if len(receivers) <= 0 {
712713
return "", fmt.Errorf("missing receivers")
@@ -812,7 +813,7 @@ func (a *covenantlessArkClient) SendOffChain(
812813
}
813814

814815
feeRate := chainfee.FeePerKwFloor
815-
redeemTx, err := buildRedeemTx(inputs, receivers, feeRate.FeePerVByte(), nil)
816+
redeemTx, err := buildRedeemTx(inputs, receivers, feeRate.FeePerVByte(), nil, withZeroFees)
816817
if err != nil {
817818
return "", err
818819
}
@@ -2666,6 +2667,7 @@ func buildRedeemTx(
26662667
receivers []Receiver,
26672668
feeRate chainfee.SatPerVByte,
26682669
extraWitnessSizes map[client.Outpoint]int,
2670+
withZeroFees bool,
26692671
) (string, error) {
26702672
if len(vtxos) <= 0 {
26712673
return "", fmt.Errorf("missing vtxos")
@@ -2757,11 +2759,13 @@ func buildRedeemTx(
27572759
return "", err
27582760
}
27592761

2760-
// Deduct the min relay fee from the very last receiver which is supposed
2761-
// to be the change in case it's not a send-all.
27622762
value := receiver.Amount()
2763-
if i == len(receivers)-1 {
2764-
value -= uint64(fees)
2763+
if !withZeroFees {
2764+
// Deduct the min relay fee from the very last receiver which is supposed
2765+
// to be the change in case it's not a send-all.
2766+
if i == len(receivers)-1 {
2767+
value -= uint64(fees)
2768+
}
27652769
}
27662770
outs = append(outs, &wire.TxOut{
27672771
Value: int64(value),

pkg/client-sdk/example/covenant/alice_to_bob.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func main() {
105105
fmt.Println("")
106106
log.Infof("alice is sending %d sats to bob offchain...", amount)
107107

108-
txid, err = aliceArkClient.SendOffChain(ctx, false, receivers)
108+
txid, err = aliceArkClient.SendOffChain(ctx, false, receivers, false)
109109
if err != nil {
110110
log.Fatal(err)
111111
}

pkg/client-sdk/example/covenantless/alice_to_bob.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func main() {
128128
fmt.Println("")
129129
log.Infof("alice is sending %d sats to bob offchain...", amount)
130130

131-
if _, err = aliceArkClient.SendOffChain(ctx, false, receivers); err != nil {
131+
if _, err = aliceArkClient.SendOffChain(ctx, false, receivers, true); err != nil {
132132
log.Fatal(err)
133133
}
134134

pkg/client-sdk/wasm/browser/wrappers.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ func SendOnChainWrapper() js.Func {
228228

229229
func SendOffChainWrapper() js.Func {
230230
return JSPromise(func(args []js.Value) (interface{}, error) {
231-
if len(args) != 2 {
231+
if len(args) != 2 && len(args) != 3 {
232232
return nil, errors.New("invalid number of args")
233233
}
234234

@@ -238,8 +238,13 @@ func SendOffChainWrapper() js.Func {
238238
return nil, err
239239
}
240240

241+
withZeroFees := false
242+
if len(args) == 3 {
243+
withZeroFees = args[2].Bool()
244+
}
245+
241246
txID, err := arkSdkClient.SendOffChain(
242-
context.Background(), withExpiryCoinselect, receivers,
247+
context.Background(), withExpiryCoinselect, receivers, withZeroFees,
243248
)
244249
if err != nil {
245250
return nil, err

server/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ run-neutrino: clean
6262
export ARK_ESPLORA_URL=http://localhost:3000; \
6363
export ARK_NEUTRINO_PEER=localhost:18444; \
6464
export ARK_DATADIR=./data/regtest; \
65+
export ARK_ALLOW_ZERO_FEES=true; \
6566
go run ./cmd/arkd
6667

6768
## run-neutrino-mutinynet: run in signet mode

server/cmd/arkd/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ func mainAction(_ *cli.Context) error {
104104
MarketHourPeriod: cfg.MarketHourPeriod,
105105
MarketHourRoundInterval: cfg.MarketHourRoundInterval,
106106
OtelCollectorEndpoint: cfg.OtelCollectorEndpoint,
107+
AllowZeroFees: cfg.AllowZeroFees,
107108
}
109+
110+
if cfg.AllowZeroFees {
111+
log.Warn("WARNING: AllowZeroFees is enabled")
112+
}
113+
108114
svc, err := grpcservice.NewService(svcConfig, appConfig)
109115
if err != nil {
110116
return err

server/internal/app-config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ type Config struct {
7676
MarketHourRoundInterval time.Duration
7777
OtelCollectorEndpoint string
7878

79+
AllowZeroFees bool
80+
7981
EsploraURL string
8082
NeutrinoPeer string
8183
BitcoindRpcUser string
@@ -374,6 +376,7 @@ func (c *Config) appService() error {
374376
c.Network, c.RoundInterval, c.VtxoTreeExpiry, c.UnilateralExitDelay, c.BoardingExitDelay, c.NostrDefaultRelays,
375377
c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler, c.NoteUriPrefix,
376378
c.MarketHourStartTime, c.MarketHourEndTime, c.MarketHourPeriod, c.MarketHourRoundInterval,
379+
c.AllowZeroFees,
377380
)
378381
if err != nil {
379382
return err

server/internal/config/config.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ type Config struct {
4848
MarketHourPeriod time.Duration
4949
MarketHourRoundInterval time.Duration
5050
OtelCollectorEndpoint string
51+
52+
// TODO remove with transactions version 3
53+
AllowZeroFees bool
5154
}
5255

5356
var (
@@ -88,6 +91,8 @@ var (
8891
MarketHourRoundInterval = "MARKET_HOUR_ROUND_INTERVAL"
8992
OtelCollectorEndpoint = "OTEL_COLLECTOR_ENDPOINT"
9093

94+
AllowZeroFees = "ALLOW_ZERO_FEES"
95+
9196
defaultDatadir = common.AppDataDir("arkd", false)
9297
defaultRoundInterval = 15
9398
DefaultPort = 7070
@@ -108,6 +113,8 @@ var (
108113
defaultMarketHourEndTime = defaultMarketHourStartTime.Add(time.Duration(defaultRoundInterval) * time.Second)
109114
defaultMarketHourPeriod = time.Duration(24) * time.Hour
110115
defaultMarketHourInterval = time.Duration(defaultRoundInterval) * time.Second
116+
117+
defaultAllowZeroFees = false
111118
)
112119

113120
func LoadConfig() (*Config, error) {
@@ -134,7 +141,7 @@ func LoadConfig() (*Config, error) {
134141
viper.SetDefault(MarketHourEndTime, defaultMarketHourEndTime)
135142
viper.SetDefault(MarketHourPeriod, defaultMarketHourPeriod)
136143
viper.SetDefault(MarketHourRoundInterval, defaultMarketHourInterval)
137-
144+
viper.SetDefault(AllowZeroFees, defaultAllowZeroFees)
138145
net, err := getNetwork()
139146
if err != nil {
140147
return nil, fmt.Errorf("error while getting network: %s", err)
@@ -180,6 +187,7 @@ func LoadConfig() (*Config, error) {
180187
MarketHourPeriod: viper.GetDuration(MarketHourPeriod),
181188
MarketHourRoundInterval: viper.GetDuration(MarketHourRoundInterval),
182189
OtelCollectorEndpoint: viper.GetString(OtelCollectorEndpoint),
190+
AllowZeroFees: viper.GetBool(AllowZeroFees),
183191
}, nil
184192
}
185193

server/internal/core/application/covenantless.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ type covenantlessService struct {
5757
currentRoundLock sync.Mutex
5858
currentRound *domain.Round
5959
treeSigningSessions map[string]*musigSigningSession
60+
61+
// allowZeroFees is a temporary flag letting to submit redeem txs with zero miner fees
62+
// this should be removed after we migrate to transactions version 3
63+
allowZeroFees bool
6064
}
6165

6266
func NewCovenantlessService(
@@ -70,6 +74,7 @@ func NewCovenantlessService(
7074
noteUriPrefix string,
7175
marketHourStartTime, marketHourEndTime time.Time,
7276
marketHourPeriod, marketHourRoundInterval time.Duration,
77+
allowZeroFees bool,
7378
) (Service, error) {
7479
pubkey, err := walletSvc.GetPubkey(context.Background())
7580
if err != nil {
@@ -108,6 +113,7 @@ func NewCovenantlessService(
108113
treeSigningSessions: make(map[string]*musigSigningSession),
109114
boardingExitDelay: boardingExitDelay,
110115
nostrDefaultRelays: nostrDefaultRelays,
116+
allowZeroFees: allowZeroFees,
111117
}
112118

113119
repoManager.RegisterEventsHandler(
@@ -370,15 +376,17 @@ func (s *covenantlessService) SubmitRedeemTx(
370376
return "", "", fmt.Errorf("invalid fees, inputs are less than outputs")
371377
}
372378

373-
minFeeRate := s.wallet.MinRelayFeeRate(ctx)
379+
if !s.allowZeroFees {
380+
minFeeRate := s.wallet.MinRelayFeeRate(ctx)
374381

375-
minFees, err := common.ComputeRedeemTxFee(chainfee.SatPerKVByte(minFeeRate), ins, len(outputs))
376-
if err != nil {
377-
return "", "", fmt.Errorf("failed to compute min fees: %s", err)
378-
}
382+
minFees, err := common.ComputeRedeemTxFee(chainfee.SatPerKVByte(minFeeRate), ins, len(outputs))
383+
if err != nil {
384+
return "", "", fmt.Errorf("failed to compute min fees: %s", err)
385+
}
379386

380-
if fees < minFees {
381-
return "", "", fmt.Errorf("min relay fee not met, %d < %d", fees, minFees)
387+
if fees < minFees {
388+
return "", "", fmt.Errorf("min relay fee not met, %d < %d", fees, minFees)
389+
}
382390
}
383391

384392
// recompute redeem tx

server/test/e2e/covenant/e2e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func TestReactToSpentVtxosRedemption(t *testing.T) {
175175

176176
vtxo := spendable[0]
177177

178-
_, err = client.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewLiquidReceiver(offchainAddress, 1000)})
178+
_, err = client.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewLiquidReceiver(offchainAddress, 1000)}, false)
179179
require.NoError(t, err)
180180

181181
round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid)

server/test/e2e/covenantless/e2e_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func TestReactToRedemptionOfVtxosSpentAsync(t *testing.T) {
248248
err = utils.GenerateBlock()
249249
require.NoError(t, err)
250250

251-
_, err = sdkClient.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)})
251+
_, err = sdkClient.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)}, false)
252252
require.NoError(t, err)
253253

254254
_, err = sdkClient.Settle(ctx)
@@ -395,7 +395,7 @@ func TestReactToRedemptionOfVtxosSpentAsync(t *testing.T) {
395395
bobAddrStr, err := bobAddr.Encode()
396396
require.NoError(t, err)
397397

398-
txid, err := alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddrStr, sendAmount)})
398+
txid, err := alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddrStr, sendAmount)}, false)
399399
require.NoError(t, err)
400400
require.NotEmpty(t, txid)
401401

@@ -574,7 +574,7 @@ func TestAliceSendsSeveralTimesToBob(t *testing.T) {
574574
bobAddress, _, err := bob.Receive(ctx)
575575
require.NoError(t, err)
576576

577-
_, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 1000)})
577+
_, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 1000)}, false)
578578
require.NoError(t, err)
579579

580580
time.Sleep(2 * time.Second)
@@ -583,7 +583,7 @@ func TestAliceSendsSeveralTimesToBob(t *testing.T) {
583583
require.NoError(t, err)
584584
require.Len(t, bobVtxos, 1)
585585

586-
_, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)})
586+
_, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)}, false)
587587
require.NoError(t, err)
588588

589589
time.Sleep(2 * time.Second)
@@ -592,7 +592,7 @@ func TestAliceSendsSeveralTimesToBob(t *testing.T) {
592592
require.NoError(t, err)
593593
require.Len(t, bobVtxos, 2)
594594

595-
_, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)})
595+
_, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)}, false)
596596
require.NoError(t, err)
597597

598598
time.Sleep(2 * time.Second)
@@ -601,7 +601,7 @@ func TestAliceSendsSeveralTimesToBob(t *testing.T) {
601601
require.NoError(t, err)
602602
require.Len(t, bobVtxos, 3)
603603

604-
_, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)})
604+
_, err = alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddress, 10000)}, false)
605605
require.NoError(t, err)
606606

607607
time.Sleep(2 * time.Second)
@@ -738,7 +738,7 @@ func TestSendToCLTVMultisigClosure(t *testing.T) {
738738
bobAddrStr, err := bobAddr.Encode()
739739
require.NoError(t, err)
740740

741-
txid, err := alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddrStr, sendAmount)})
741+
txid, err := alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddrStr, sendAmount)}, false)
742742
require.NoError(t, err)
743743
require.NotEmpty(t, txid)
744744

@@ -919,7 +919,7 @@ func TestSendToConditionMultisigClosure(t *testing.T) {
919919
bobAddrStr, err := bobAddr.Encode()
920920
require.NoError(t, err)
921921

922-
txid, err := alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddrStr, sendAmount)})
922+
txid, err := alice.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(bobAddrStr, sendAmount)}, false)
923923
require.NoError(t, err)
924924
require.NotEmpty(t, txid)
925925

0 commit comments

Comments
 (0)