Add support for unilateral exit (#79)

* v0 unilateral redemption

* add fee outputs to congestion tree

* unilateral exit

* rework unilateral exit verbosity

* substract fee from vtxo amount

* remove unused functions and variables

* fix after reviews

* Update noah/explorer.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* remove bufferutils

---------

Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>
Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
This commit is contained in:
Louis Singer
2024-01-16 14:13:47 +01:00
committed by GitHub
parent b438eb638f
commit cf78fc1ab3
33 changed files with 1240 additions and 630 deletions

View File

@@ -1,8 +1,14 @@
package main
import (
"bufio"
"fmt"
"io"
"log"
"math"
"os"
"strings"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -66,7 +72,11 @@ func redeemAction(ctx *cli.Context) error {
}
if force {
return unilateralRedeem(addr)
if amount > 0 {
fmt.Printf("WARNING: unilateral exit (--force) ignores --amount flag, it will redeem all your VTXOs\n")
}
return unilateralRedeem(ctx, addr)
}
return collaborativeRedeem(ctx, addr, amount)
@@ -217,7 +227,206 @@ func collaborativeRedeem(ctx *cli.Context, addr string, amount uint64) error {
return nil
}
func unilateralRedeem(address string) error {
fmt.Println("unilateral redeem is not implemented yet")
func unilateralRedeem(ctx *cli.Context, addr string) error {
onchainScript, err := address.ToOutputScript(addr)
if err != nil {
return err
}
client, close, err := getClientFromState(ctx)
if err != nil {
return err
}
defer close()
offchainAddr, _, err := getAddress()
if err != nil {
return err
}
vtxos, err := getVtxos(ctx, client, offchainAddr)
if err != nil {
return err
}
totalVtxosAmount := uint64(0)
for _, vtxo := range vtxos {
totalVtxosAmount += vtxo.amount
}
ok := askForConfirmation(fmt.Sprintf("redeem %d sats to %s ?", totalVtxosAmount, addr))
if !ok {
return fmt.Errorf("aborting unilateral exit")
}
finalPset, err := psetv2.New(nil, nil, nil)
if err != nil {
return err
}
updater, err := psetv2.NewUpdater(finalPset)
if err != nil {
return err
}
congestionTrees := make(map[string]*arkv1.Tree, 0)
transactionsMap := make(map[string]struct{}, 0)
transactions := make([]string, 0)
for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.poolTxid]; !ok {
round, err := client.GetRound(ctx.Context, &arkv1.GetRoundRequest{
Txid: vtxo.poolTxid,
})
if err != nil {
return err
}
congestionTrees[vtxo.poolTxid] = round.GetRound().GetCongestionTree()
}
redeemBranch, err := newRedeemBranch(ctx, congestionTrees[vtxo.poolTxid], vtxo)
if err != nil {
return err
}
if err := redeemBranch.UpdatePath(); err != nil {
return err
}
branchTxs, err := redeemBranch.RedeemPath()
if err != nil {
return err
}
if err := redeemBranch.AddVtxoInput(updater); err != nil {
return err
}
for _, txHex := range branchTxs {
if _, ok := transactionsMap[txHex]; !ok {
transactions = append(transactions, txHex)
transactionsMap[txHex] = struct{}{}
}
}
}
_, net, err := getNetwork()
if err != nil {
return err
}
outputs := []psetv2.OutputArgs{
{
Asset: net.AssetID,
Amount: totalVtxosAmount,
Script: onchainScript,
},
}
if err := updater.AddOutputs(outputs); err != nil {
return err
}
utx, err := updater.Pset.UnsignedTx()
if err != nil {
return err
}
vBytes := utx.VirtualSize()
feeAmount := uint64(math.Ceil(float64(vBytes) * 0.2))
if totalVtxosAmount-feeAmount <= 0 {
return fmt.Errorf("not enough VTXOs to pay the fees (%d sats), aborting unilateral exit", feeAmount)
}
updater.Pset.Outputs[0].Value = totalVtxosAmount - feeAmount
if err := updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: net.AssetID,
Amount: feeAmount,
},
}); err != nil {
return err
}
prvKey, err := privateKeyFromPassword()
if err != nil {
return err
}
explorer := NewExplorer()
for i, txHex := range transactions {
for {
txid, err := explorer.Broadcast(txHex)
if err != nil {
if strings.Contains(err.Error(), "bad-txns-inputs-missingorspent") {
time.Sleep(1 * time.Second)
} else {
return err
}
}
if len(txid) > 0 {
fmt.Printf("(%d/%d) broadcasted tx %s\n", i+1, len(transactions), txid)
break
}
}
}
if err := signPset(finalPset, explorer, prvKey); err != nil {
return err
}
for i, input := range finalPset.Inputs {
if len(input.TapScriptSig) > 0 || len(input.PartialSigs) > 0 {
if err := psetv2.Finalize(finalPset, i); err != nil {
return err
}
}
}
signedTx, err := psetv2.Extract(finalPset)
if err != nil {
return err
}
hex, err := signedTx.ToHex()
if err != nil {
return err
}
id, err := explorer.Broadcast(hex)
if err != nil {
return err
}
fmt.Printf("(final) redeem tx %s\n", id)
return nil
}
// askForConfirmation asks the user for confirmation. A user must type in "yes" or "no" and then press enter.
// if the input is not recognized, it will ask again.
func askForConfirmation(s string) bool {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s [y/n]: ", s)
response, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" {
return true
} else if response == "n" || response == "no" {
return false
}
}
}