Files
ark/client/covenantless/cli.go
Louis Singer 4da76ec88b New boarding protocol (#279)
* [domain] add reverse boarding inputs in Payment struct

* [tx-builder] support reverse boarding script

* [wallet] add GetTransaction

* [api-spec][application] add reverse boarding support in covenantless

* [config] add reverse boarding config

* [api-spec] add ReverseBoardingAddress RPC

* [domain][application] support empty forfeits txs in EndFinalization events

* [tx-builder] optional connector output in round tx

* [btc-embedded] fix getTx and taproot finalizer

* whitelist ReverseBoardingAddress RPC

* [test] add reverse boarding integration test

* [client] support reverse boarding

* [sdk] support reverse boarding

* [e2e] add sleep time after faucet

* [test] run using bitcoin-core RPC

* [tx-builder] fix GetSweepInput

* [application][tx-builder] support reverse onboarding in covenant

* [cli] support reverse onboarding in covenant CLI

* [test] rework integration tests

* [sdk] remove onchain wallet, replace by onboarding address

* remove old onboarding protocols

* [sdk] Fix RegisterPayment

* [e2e] add more funds to covenant ASP

* [e2e] add sleeping time

* several fixes

* descriptor boarding

* remove boarding delay from info

* [sdk] implement descriptor boarding

* go mod tidy

* fixes and revert error msgs

* move descriptor pkg to common

* add replace in go.mod

* [sdk] fix unit tests

* rename DescriptorInput --> BoardingInput

* genrest in SDK

* remove boarding input from domain

* remove all "reverse boarding"

* rename "onboarding" ==> "boarding"

* remove outdate payment unit test

* use tmpfs docker volument for compose testing files

* several fixes
2024-09-04 19:21:26 +02:00

471 lines
9.9 KiB
Go

package covenantless
import (
"fmt"
"math"
"time"
"github.com/ark-network/ark/client/interfaces"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
)
const dust = 450
type clArkBitcoinCLI struct{}
func (c *clArkBitcoinCLI) Receive(ctx *cli.Context) error {
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"offchain_address": offchainAddr,
"boarding_address": boardingAddr.EncodeAddress(),
})
}
func (c *clArkBitcoinCLI) Redeem(ctx *cli.Context) error {
addr := ctx.String("address")
amount := ctx.Uint64("amount")
force := ctx.Bool("force")
if len(addr) <= 0 && !force {
return fmt.Errorf("missing address flag (--address)")
}
if !force && amount <= 0 {
return fmt.Errorf("missing amount flag (--amount)")
}
client, clean, err := getClientFromState(ctx)
if err != nil {
return err
}
defer clean()
if force {
if amount > 0 {
fmt.Printf("WARNING: unilateral exit (--force) ignores --amount flag, it will redeem all your VTXOs\n")
}
return unilateralRedeem(ctx, client)
}
return collaborativeRedeem(ctx, client, addr, amount)
}
func New() interfaces.CLI {
return &clArkBitcoinCLI{}
}
func (c *clArkBitcoinCLI) Send(ctx *cli.Context) error {
return fmt.Errorf("not implemented")
}
type receiver struct {
To string `json:"to"`
Amount uint64 `json:"amount"`
}
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
ptx, err := psbt.New(nil, nil, 2, 0, nil)
if err != nil {
return "", err
}
updater, err := psbt.NewUpdater(ptx)
if err != nil {
return "", err
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return "", err
}
netParams := toChainParams(net)
targetAmount := uint64(0)
for _, receiver := range receivers {
targetAmount += receiver.Amount
if receiver.Amount < dust {
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, dust)
}
rcvAddr, err := btcutil.DecodeAddress(receiver.To, &netParams)
if err != nil {
return "", err
}
pkscript, err := txscript.PayToAddrScript(rcvAddr)
if err != nil {
return "", err
}
updater.Upsbt.UnsignedTx.AddTxOut(&wire.TxOut{
Value: int64(receiver.Amount),
PkScript: pkscript,
})
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
}
explorer := utils.NewExplorer(ctx)
utxos, change, err := coinSelectOnchain(
ctx, explorer, targetAmount, nil,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, utxos); err != nil {
return "", err
}
if change > 0 {
_, changeAddr, _, err := getAddress(ctx)
if err != nil {
return "", err
}
pkscript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
return "", err
}
updater.Upsbt.UnsignedTx.AddTxOut(&wire.TxOut{
Value: int64(change),
PkScript: pkscript,
})
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
}
size := updater.Upsbt.UnsignedTx.SerializeSize()
feeRate, err := explorer.GetFeeRate()
if err != nil {
return "", err
}
feeAmount := uint64(math.Ceil(float64(size)*feeRate) + 50)
if change > feeAmount {
updater.Upsbt.UnsignedTx.TxOut[len(updater.Upsbt.Outputs)-1].Value = int64(change - feeAmount)
} else if change == feeAmount {
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
} else { // change < feeAmount
if change > 0 {
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
}
// reselect the difference
selected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, utxos,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, selected); err != nil {
return "", err
}
if newChange > 0 {
_, changeAddr, _, err := getAddress(ctx)
if err != nil {
return "", err
}
pkscript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
return "", err
}
updater.Upsbt.UnsignedTx.AddTxOut(&wire.TxOut{
Value: int64(newChange),
PkScript: pkscript,
})
updater.Upsbt.Outputs = append(updater.Upsbt.Outputs, psbt.POutput{})
}
}
prvKey, err := utils.PrivateKeyFromPassword(ctx)
if err != nil {
return "", err
}
if err := signPsbt(ctx, updater.Upsbt, explorer, prvKey); err != nil {
return "", err
}
for i := range updater.Upsbt.Inputs {
if err := psbt.Finalize(updater.Upsbt, i); err != nil {
return "", err
}
}
return updater.Upsbt.B64Encode()
}
func coinSelectOnchain(
ctx *cli.Context,
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
) ([]utils.Utxo, uint64, error) {
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return nil, 0, err
}
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
if err != nil {
return nil, 0, err
}
utxos := make([]utils.Utxo, 0)
selectedAmount := uint64(0)
now := time.Now()
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return nil, 0, err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return nil, 0, err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return nil, 0, err
}
for _, utxo := range boardingUtxosFromExplorer {
if selectedAmount >= targetAmount {
break
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
if utxo.SpendableAt.After(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount >= targetAmount {
return utxos, selectedAmount - targetAmount, nil
}
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr.EncodeAddress())
if err != nil {
return nil, 0, err
}
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, 0, err
}
for _, utxo := range redemptionUtxosFromExplorer {
if selectedAmount >= targetAmount {
break
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
if utxo.SpendableAt.After(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount < targetAmount {
return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
}
return utxos, selectedAmount - targetAmount, nil
}
func addInputs(
ctx *cli.Context,
updater *psbt.Updater,
utxos []utils.Utxo,
) error {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
for _, utxo := range utxos {
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil {
return err
}
sequence, err := utxo.Sequence()
if err != nil {
return err
}
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *previousHash,
Index: utxo.Vout,
},
Sequence: sequence,
})
_, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay,
)
if err != nil {
return err
}
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
{
ControlBlock: controlBlockBytes,
Script: leafProof.Script,
LeafVersion: leafProof.LeafVersion,
},
},
})
}
return nil
}
func decodeReceiverAddress(addr string) (
bool, []byte, *secp256k1.PublicKey, error,
) {
decoded, err := btcutil.DecodeAddress(addr, nil)
if err != nil {
_, userPubkey, _, err := common.DecodeAddress(addr)
if err != nil {
return false, nil, nil, err
}
return false, nil, userPubkey, nil
}
pkscript, err := txscript.PayToAddrScript(decoded)
if err != nil {
return false, nil, nil, err
}
return true, pkscript, nil, nil
}
func getAddress(ctx *cli.Context) (offchainAddr string, boardingAddr, redemptionAddr btcutil.Address, err error) {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return
}
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return
}
arkNet, err := utils.GetNetwork(ctx)
if err != nil {
return
}
arkAddr, err := common.EncodeAddress(arkNet.Addr, userPubkey, aspPubkey)
if err != nil {
return
}
netParams := toChainParams(arkNet)
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil {
return
}
redemptionP2TR, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey),
&netParams,
)
if err != nil {
return
}
boardingTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(timeoutBoarding),
)
if err != nil {
return
}
boardingP2TR, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(boardingTapKey),
&netParams,
)
redemptionAddr = redemptionP2TR
boardingAddr = boardingP2TR
offchainAddr = arkAddr
return
}