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
This commit is contained in:
Louis Singer
2024-09-04 19:21:26 +02:00
committed by GitHub
parent 8cba9c9d42
commit 4da76ec88b
113 changed files with 5627 additions and 4430 deletions

View File

@@ -9,6 +9,7 @@ import (
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/flags"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/urfave/cli/v2"
)
@@ -21,18 +22,30 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
}
defer cancel()
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return err
}
network, err := utils.GetNetwork(ctx)
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return err
}
// No need to check for error here becuase this function is called also by getAddress().
// nolint:all
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return err
}
wg := &sync.WaitGroup{}
wg.Add(3)
@@ -54,19 +67,19 @@ func (*covenantLiquidCLI) Balance(ctx *cli.Context) error {
go func() {
defer wg.Done()
explorer := utils.NewExplorer(ctx)
balance, err := explorer.GetBalance(onchainAddr, toElementsNetwork(network).AssetID)
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr, int64(timeoutBoarding))
if err != nil {
chRes <- balanceRes{0, 0, nil, nil, err}
return
}
chRes <- balanceRes{0, balance, nil, nil, nil}
chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, nil}
}()
go func() {
defer wg.Done()
explorer := utils.NewExplorer(ctx)
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
redemptionAddr, unilateralExitDelay,
)
if err != nil {

148
client/covenant/claim.go Normal file
View File

@@ -0,0 +1,148 @@
package covenant
import (
"fmt"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/urfave/cli/v2"
)
func (c *covenantLiquidCLI) Claim(ctx *cli.Context) error {
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return err
}
explorer := utils.NewExplorer(ctx)
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
if err != nil {
return err
}
now := time.Now()
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
for _, utxo := range boardingUtxosFromExplorer {
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
if u.SpendableAt.Before(now) {
continue // cannot claim if onchain spendable
}
boardingUtxos = append(boardingUtxos, u)
}
var pendingBalance uint64
for _, utxo := range boardingUtxos {
pendingBalance += utxo.Amount
}
if pendingBalance == 0 {
return fmt.Errorf("no boarding utxos to claim")
}
receiver := receiver{
To: offchainAddr,
Amount: pendingBalance,
}
if len(ctx.String("password")) == 0 {
if ok := askForConfirmation(
fmt.Sprintf(
"claim %d satoshis from %d boarding utxos",
pendingBalance, len(boardingUtxos),
),
); !ok {
return nil
}
}
return selfTransferAllPendingPayments(
ctx, client, boardingUtxos, receiver, boardingDescriptor,
)
}
func selfTransferAllPendingPayments(
ctx *cli.Context,
client arkv1.ArkServiceClient,
boardingUtxos []utils.Utxo,
myself receiver,
desc string,
) error {
inputs := make([]*arkv1.Input, 0, len(boardingUtxos))
for _, outpoint := range boardingUtxos {
inputs = append(inputs, &arkv1.Input{
Input: &arkv1.Input_BoardingInput{
BoardingInput: &arkv1.BoardingInput{
Txid: outpoint.Txid,
Vout: outpoint.Vout,
Descriptor_: desc,
},
},
})
}
receiversOutput := []*arkv1.Output{
{
Address: myself.To,
Amount: myself.Amount,
},
}
secKey, err := utils.PrivateKeyFromPassword(ctx)
if err != nil {
return err
}
registerResponse, err := client.RegisterPayment(
ctx.Context, &arkv1.RegisterPaymentRequest{Inputs: inputs},
)
if err != nil {
return err
}
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{
Id: registerResponse.GetId(),
Outputs: []*arkv1.Output{{Address: myself.To, Amount: myself.Amount}},
})
if err != nil {
return err
}
poolTxID, err := handleRoundStream(
ctx, client, registerResponse.GetId(), make([]vtxo, 0),
len(boardingUtxos) > 0, secKey, receiversOutput,
)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"pool_txid": poolTxID,
})
}

View File

@@ -9,16 +9,13 @@ import (
"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/descriptor"
"github.com/ark-network/ark/common/tree"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
)
const dust = 450
@@ -29,19 +26,15 @@ func (c *covenantLiquidCLI) SendAsync(ctx *cli.Context) error {
return fmt.Errorf("not implemented")
}
func (c *covenantLiquidCLI) ClaimAsync(ctx *cli.Context) error {
return fmt.Errorf("not implemented")
}
func (c *covenantLiquidCLI) Receive(ctx *cli.Context) error {
offchainAddr, onchainAddr, _, err := getAddress(ctx)
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"offchain_address": offchainAddr,
"onchain_address": onchainAddr,
"boarding_address": boardingAddr,
})
}
@@ -131,14 +124,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
explorer := utils.NewExplorer(ctx)
utxos, delayedUtxos, change, err := coinSelectOnchain(
utxos, change, err := coinSelectOnchain(
ctx, explorer, targetAmount, nil,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, utxos, delayedUtxos, &liquidNet); err != nil {
if err := addInputs(ctx, updater, utxos); err != nil {
return "", err
}
@@ -181,14 +174,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
updater.Pset.Outputs = updater.Pset.Outputs[:len(updater.Pset.Outputs)-1]
}
// reselect the difference
selected, delayedSelected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...),
selected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, utxos,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, selected, delayedSelected, &liquidNet); err != nil {
if err := addInputs(ctx, updater, selected); err != nil {
return "", err
}
@@ -243,20 +236,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
func coinSelectOnchain(
ctx *cli.Context,
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
) ([]utils.Utxo, []utils.Utxo, uint64, error) {
_, onchainAddr, _, err := getAddress(ctx)
) ([]utils.Utxo, uint64, error) {
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
fromExplorer, err := explorer.GetUtxos(onchainAddr)
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
utxos := make([]utils.Utxo, 0)
selectedAmount := uint64(0)
for _, utxo := range fromExplorer {
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
}
@@ -267,207 +277,99 @@ func coinSelectOnchain(
}
}
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
if utxo.SpendableAt.Before(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil
return utxos, selectedAmount - targetAmount, nil
}
userPubkey, err := utils.GetWalletPublicKey(ctx)
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, nil, 0, err
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil {
return nil, nil, 0, err
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return nil, nil, 0, err
}
liquidNet := toElementsNetwork(net)
pay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
if err != nil {
return nil, nil, 0, err
}
addr, err := pay.TaprootAddress()
if err != nil {
return nil, nil, 0, err
}
fromExplorer, err = explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
}
delayedUtxos := make([]utils.Utxo, 0)
for _, utxo := range fromExplorer {
for _, utxo := range redemptionUtxosFromExplorer {
if selectedAmount >= targetAmount {
break
}
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(unilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) {
continue
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
delayedUtxos = append(delayedUtxos, utxo)
selectedAmount += utxo.Amount
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
if utxo.SpendableAt.Before(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf(
return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
}
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
return utxos, selectedAmount - targetAmount, nil
}
func addInputs(
ctx *cli.Context,
updater *psetv2.Updater, utxos, delayedUtxos []utils.Utxo, net *network.Network,
updater *psetv2.Updater,
utxos []utils.Utxo,
) error {
_, onchainAddr, _, err := getAddress(ctx)
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
changeScript, err := address.ToOutputScript(onchainAddr)
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
for _, utxo := range utxos {
sequence, err := utxo.Sequence()
if err != nil {
return err
}
if err := updater.AddInputs([]psetv2.InputArgs{
{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
Txid: utxo.Txid,
TxIndex: utxo.Vout,
Sequence: sequence,
},
}); err != nil {
return err
}
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil {
return err
}
value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil {
return err
}
witnessUtxo := transaction.TxOutput{
Asset: assetID,
Value: value,
Script: changeScript,
Nonce: []byte{0x00},
}
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, &witnessUtxo,
); err != nil {
return err
}
}
if len(delayedUtxos) > 0 {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return err
}
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
_, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay,
)
if err != nil {
return err
}
pay, err := payment.FromTweakedKey(vtxoTapKey, net, nil)
if err != nil {
inputIndex := len(updater.Pset.Inputs) - 1
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
return err
}
addr, err := pay.TaprootAddress()
if err != nil {
return err
}
script, err := address.ToOutputScript(addr)
if err != nil {
return err
}
for _, utxo := range delayedUtxos {
if err := addVtxoInput(
updater,
psetv2.InputArgs{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
},
uint(unilateralExitDelay),
leafProof,
); err != nil {
return err
}
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil {
return err
}
value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil {
return err
}
witnessUtxo := transaction.NewTxOutput(assetID, value, script)
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, witnessUtxo,
); err != nil {
return err
}
}
}
return nil
@@ -503,32 +405,7 @@ func decodeReceiverAddress(addr string) (
return true, outputScript, nil, nil
}
func addVtxoInput(
updater *psetv2.Updater, inputArgs psetv2.InputArgs, exitDelay uint,
tapLeafProof *taproot.TapscriptElementsProof,
) error {
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
if err != nil {
return nil
}
nextInputIndex := len(updater.Pset.Inputs)
if err := updater.AddInputs([]psetv2.InputArgs{inputArgs}); err != nil {
return err
}
updater.Pset.Inputs[nextInputIndex].Sequence = sequence
return updater.AddInTapLeafScript(
nextInputIndex,
psetv2.NewTapLeafScript(
*tapLeafProof,
tree.UnspendableKey(),
),
)
}
func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr string, err error) {
func getAddress(ctx *cli.Context) (offchainAddr, boardingAddr, redemptionAddr string, err error) {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return
@@ -544,6 +421,21 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
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
@@ -556,12 +448,6 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
liquidNet := toElementsNetwork(arkNet)
p2wpkh := payment.FromPublicKey(userPubkey, &liquidNet, nil)
liquidAddr, err := p2wpkh.WitnessPubKeyHash()
if err != nil {
return
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
@@ -569,18 +455,34 @@ func getAddress(ctx *cli.Context) (offchainAddr, onchainAddr, redemptionAddr str
return
}
payment, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
redemptionPay, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
if err != nil {
return
}
redemptionAddr, err = payment.TaprootAddress()
redemptionAddr, err = redemptionPay.TaprootAddress()
if err != nil {
return
}
boardingTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(timeoutBoarding),
)
if err != nil {
return
}
boardingPay, err := payment.FromTweakedKey(boardingTapKey, &liquidNet, nil)
if err != nil {
return
}
boardingAddr, err = boardingPay.TaprootAddress()
if err != nil {
return
}
offchainAddr = arkAddr
onchainAddr = liquidAddr
return
}

View File

@@ -51,13 +51,16 @@ func getVtxos(
if v.Swept {
continue
}
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount,
txid: v.Outpoint.Txid,
vout: v.Outpoint.Vout,
poolTxid: v.PoolTxid,
expireAt: expireAt,
})
if v.Outpoint.GetVtxoInput() != nil {
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount,
txid: v.Outpoint.GetVtxoInput().GetTxid(),
vout: v.Outpoint.GetVtxoInput().GetVout(),
poolTxid: v.PoolTxid,
expireAt: expireAt,
})
}
}
if !computeExpiration {
@@ -193,32 +196,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
return levels, nil
}
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
for _, level := range congestionTree {
levelProto := &arkv1.TreeLevel{
Nodes: make([]*arkv1.Node, 0, len(level)),
}
for _, node := range level {
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
})
}
levels = append(levels, levelProto)
}
return &arkv1.Tree{
Levels: levels,
}
}
func handleRoundStream(
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
vtxosToSign []vtxo, mustSignRoundTx bool,
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
) (poolTxID string, err error) {
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
if err != nil {
@@ -254,8 +235,8 @@ func handleRoundStream(
pingStop()
fmt.Println("round finalization started")
poolTx := e.GetPoolTx()
ptx, err := psetv2.NewPsetFromBase64(poolTx)
roundTx := e.GetPoolTx()
ptx, err := psetv2.NewPsetFromBase64(roundTx)
if err != nil {
return "", err
}
@@ -280,13 +261,13 @@ func handleRoundStream(
if !isOnchainOnly(receivers) {
// validate the congestion tree
if err := tree.ValidateCongestionTree(
congestionTree, poolTx, aspPubkey, int64(roundLifetime),
congestionTree, roundTx, aspPubkey, int64(roundLifetime),
); err != nil {
return "", err
}
}
if err := common.ValidateConnectors(poolTx, connectors); err != nil {
if err := common.ValidateConnectors(roundTx, connectors); err != nil {
return "", err
}
@@ -379,78 +360,100 @@ func handleRoundStream(
fmt.Println("congestion tree validated")
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
fmt.Print("signing forfeit txs... ")
explorer := utils.NewExplorer(ctx)
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, _ := psetv2.NewPsetFromBase64(connector)
utx, _ := p.UnsignedTx()
txid := utx.TxHash().String()
if len(vtxosToSign) > 0 {
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
connectorsTxids = append(connectorsTxids, txid)
fmt.Print("signing forfeit txs... ")
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, _ := psetv2.NewPsetFromBase64(connector)
utx, _ := p.UnsignedTx()
txid := utx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
}
for _, forfeit := range forfeits {
pset, err := psetv2.NewPsetFromBase64(forfeit)
if err != nil {
return "", err
}
for _, input := range pset.Inputs {
inputTxid := chainhash.Hash(input.PreviousTxid).String()
for _, coin := range vtxosToSign {
// check if it contains one of the input to sign
if inputTxid == coin.txid {
// verify that the connector is in the connectors list
connectorTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
if err := signPset(ctx, pset, explorer, secKey); err != nil {
return "", err
}
signedPset, err := pset.ToBase64()
if err != nil {
return "", err
}
signedForfeits = append(signedForfeits, signedPset)
}
}
}
}
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx.Context, client, pingReq)
}
continue
}
fmt.Printf("%d signed\n", len(signedForfeits))
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
}
for _, forfeit := range forfeits {
pset, err := psetv2.NewPsetFromBase64(forfeit)
if mustSignRoundTx {
ptx, err := psetv2.NewPsetFromBase64(roundTx)
if err != nil {
return "", err
}
for _, input := range pset.Inputs {
inputTxid := chainhash.Hash(input.PreviousTxid).String()
for _, coin := range vtxosToSign {
// check if it contains one of the input to sign
if inputTxid == coin.txid {
// verify that the connector is in the connectors list
connectorTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
if err := signPset(ctx, pset, explorer, secKey); err != nil {
return "", err
}
signedPset, err := pset.ToBase64()
if err != nil {
return "", err
}
signedForfeits = append(signedForfeits, signedPset)
}
}
if err := signPset(ctx, ptx, explorer, secKey); err != nil {
return "", err
}
signedRoundTx, err := ptx.ToBase64()
if err != nil {
return "", err
}
fmt.Println("round tx signed")
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
}
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx.Context, client, pingReq)
}
continue
}
fmt.Printf("%d signed\n", len(signedForfeits))
fmt.Print("finalizing payment... ")
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeits,
})
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
if err != nil {
return "", err
}

View File

@@ -104,6 +104,7 @@ func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
utils.ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
utils.EXPLORER: explorer,
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
})
}

View File

@@ -1,115 +0,0 @@
package covenant
import (
"encoding/hex"
"fmt"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/tree"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
)
const minRelayFee = 30
func (c *covenantLiquidCLI) Onboard(ctx *cli.Context) error {
amount := ctx.Uint64("amount")
if amount <= 0 {
return fmt.Errorf("missing amount flag (--amount)")
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return err
}
userPubKey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
roundLifetime, err := utils.GetRoundLifetime(ctx)
if err != nil {
return err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return err
}
congestionTreeLeaf := tree.Receiver{
Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
Amount: amount,
}
liquidNet := toElementsNetwork(net)
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
liquidNet.AssetID, aspPubkey, []tree.Receiver{congestionTreeLeaf},
minRelayFee, roundLifetime, unilateralExitDelay,
)
if err != nil {
return err
}
pay, err := payment.FromScript(sharedOutputScript, &liquidNet, nil)
if err != nil {
return err
}
address, err := pay.TaprootAddress()
if err != nil {
return err
}
onchainReceiver := receiver{
To: address,
Amount: sharedOutputAmount,
}
pset, err := sendOnchain(ctx, []receiver{onchainReceiver})
if err != nil {
return err
}
ptx, _ := psetv2.NewPsetFromBase64(pset)
utx, _ := ptx.UnsignedTx()
txid := utx.TxHash().String()
congestionTree, err := treeFactoryFn(psetv2.InputArgs{
Txid: txid,
TxIndex: 0,
})
if err != nil {
return err
}
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
BoardingTx: pset,
CongestionTree: castCongestionTree(congestionTree),
UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
})
if err != nil {
return err
}
fmt.Println("onboard_txid:", txid)
return nil
}

View File

@@ -78,8 +78,12 @@ func collaborativeRedeem(
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}
@@ -108,6 +112,7 @@ func collaborativeRedeem(
client,
registerResponse.GetId(),
selectedCoins,
false,
secKey,
receivers,
)

View File

@@ -143,8 +143,12 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}
@@ -170,7 +174,7 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
poolTxID, err := handleRoundStream(
ctx, client, registerResponse.GetId(),
selectedCoins, secKey, receiversOutput,
selectedCoins, false, secKey, receiversOutput,
)
if err != nil {
return err

View File

@@ -6,14 +6,11 @@ import (
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
@@ -50,13 +47,7 @@ func signPset(
return err
}
sighashType := txscript.SigHashAll
if utxo.Script[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(i, sighashType); err != nil {
if err := updater.AddInSighashType(i, txscript.SigHashDefault); err != nil {
return err
}
}
@@ -66,16 +57,6 @@ func signPset(
return err
}
_, onchainAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
onchainWalletScript, err := address.ToOutputScript(onchainAddr)
if err != nil {
return err
}
utx, err := pset.UnsignedTx()
if err != nil {
return err
@@ -99,34 +80,6 @@ func signPset(
liquidNet := toElementsNetwork(net)
for i, input := range pset.Inputs {
if bytes.Equal(input.WitnessUtxo.Script, onchainWalletScript) {
p, err := payment.FromScript(input.WitnessUtxo.Script, &liquidNet, nil)
if err != nil {
return err
}
preimage := utx.HashForWitnessV0(
i,
p.Script,
input.WitnessUtxo.Value,
txscript.SigHashAll,
)
sig := ecdsa.Sign(
prvKey,
preimage[:],
)
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
err = signer.SignInput(i, signatureWithSighashType, prvKey.PubKey().SerializeCompressed(), nil, nil)
if err != nil {
fmt.Println("error signing input: ", err)
return err
}
continue
}
if len(input.TapLeafScript) > 0 {
genesis, err := chainhash.NewHashFromStr(liquidNet.GenesisBlockHash)
if err != nil {

View File

@@ -9,6 +9,7 @@ import (
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/flags"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/urfave/cli/v2"
)
@@ -21,7 +22,7 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
}
defer cancel()
offchainAddr, onchainAddr, redemptionAddr, err := getAddress(ctx)
offchainAddr, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return err
}
@@ -29,6 +30,21 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
// nolint:all
unilateralExitDelay, _ := utils.GetUnilateralExitDelay(ctx)
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return err
}
wg := &sync.WaitGroup{}
wg.Add(3)
@@ -50,19 +66,19 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
go func() {
defer wg.Done()
explorer := utils.NewExplorer(ctx)
balance, err := explorer.GetBalance(onchainAddr.EncodeAddress(), "")
balance, lockedBalance, err := explorer.GetDelayedBalance(boardingAddr.EncodeAddress(), int64(timeoutBoarding))
if err != nil {
chRes <- balanceRes{0, 0, nil, nil, err}
return
}
chRes <- balanceRes{0, balance, nil, nil, nil}
chRes <- balanceRes{0, balance, lockedBalance, nil, nil}
}()
go func() {
defer wg.Done()
explorer := utils.NewExplorer(ctx)
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
spendableBalance, lockedBalance, err := explorer.GetDelayedBalance(
redemptionAddr.EncodeAddress(), unilateralExitDelay,
)
if err != nil {
@@ -126,6 +142,7 @@ func (*clArkBitcoinCLI) Balance(ctx *cli.Context) error {
}
response := make(map[string]interface{})
response["onchain_balance"] = map[string]interface{}{
"spendable_amount": onchainBalance,
}

View File

@@ -2,26 +2,62 @@ package covenantless
import (
"encoding/hex"
"fmt"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/descriptor"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
)
func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
func (c *clArkBitcoinCLI) Claim(ctx *cli.Context) error {
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
myselfOffchain, _, _, err := getAddress(ctx)
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
vtxos, err := getVtxos(ctx, nil, client, myselfOffchain, false)
boardingDescriptor, err := utils.GetBoardingDescriptor(ctx)
if err != nil {
return err
}
desc, err := descriptor.ParseTaprootDescriptor(boardingDescriptor)
if err != nil {
return err
}
_, timeoutBoarding, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return err
}
explorer := utils.NewExplorer(ctx)
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
if err != nil {
return err
}
now := time.Now()
boardingUtxos := make([]utils.Utxo, 0, len(boardingUtxosFromExplorer))
for _, utxo := range boardingUtxosFromExplorer {
u := utils.NewUtxo(utxo, uint(timeoutBoarding))
if u.SpendableAt.Before(now) {
continue // cannot claim if onchain spendable
}
boardingUtxos = append(boardingUtxos, u)
}
vtxos, err := getVtxos(ctx, nil, client, offchainAddr, false)
if err != nil {
return err
}
@@ -34,32 +70,71 @@ func (c *clArkBitcoinCLI) ClaimAsync(ctx *cli.Context) error {
pendingVtxos = append(pendingVtxos, vtxo)
}
}
for _, utxo := range boardingUtxos {
pendingBalance += utxo.Amount
}
if pendingBalance == 0 {
return nil
}
receiver := receiver{
To: myselfOffchain,
To: offchainAddr,
Amount: pendingBalance,
}
if len(ctx.String("password")) == 0 {
if ok := askForConfirmation(
fmt.Sprintf(
"claim %d satoshis from %d pending payments and %d boarding utxos",
pendingBalance, len(pendingVtxos), len(boardingUtxos),
),
); !ok {
return nil
}
}
return selfTransferAllPendingPayments(
ctx, client, pendingVtxos, receiver,
ctx, client, pendingVtxos, boardingUtxos, receiver, boardingDescriptor,
)
}
func selfTransferAllPendingPayments(
ctx *cli.Context, client arkv1.ArkServiceClient,
pendingVtxos []vtxo, myself receiver,
ctx *cli.Context,
client arkv1.ArkServiceClient,
pendingVtxos []vtxo,
boardingUtxos []utils.Utxo,
myself receiver,
desc string,
) error {
inputs := make([]*arkv1.Input, 0, len(pendingVtxos))
inputs := make([]*arkv1.Input, 0, len(pendingVtxos)+len(boardingUtxos))
for _, coin := range pendingVtxos {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}
if len(boardingUtxos) > 0 {
for _, outpoint := range boardingUtxos {
inputs = append(inputs, &arkv1.Input{
Input: &arkv1.Input_BoardingInput{
BoardingInput: &arkv1.BoardingInput{
Txid: outpoint.Txid,
Vout: outpoint.Vout,
Descriptor_: desc,
},
},
})
}
}
receiversOutput := []*arkv1.Output{
{
Address: myself.To,
@@ -99,8 +174,8 @@ func selfTransferAllPendingPayments(
}
poolTxID, err := handleRoundStream(
ctx, client, registerResponse.GetId(),
pendingVtxos, secKey, receiversOutput, ephemeralKey,
ctx, client, registerResponse.GetId(), pendingVtxos,
len(boardingUtxos) > 0, secKey, receiversOutput, ephemeralKey,
)
if err != nil {
return err

View File

@@ -9,10 +9,10 @@ import (
"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"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
@@ -25,14 +25,14 @@ const dust = 450
type clArkBitcoinCLI struct{}
func (c *clArkBitcoinCLI) Receive(ctx *cli.Context) error {
offchainAddr, onchainAddr, _, err := getAddress(ctx)
offchainAddr, boardingAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"offchain_address": offchainAddr,
"onchain_address": onchainAddr.EncodeAddress(),
"boarding_address": boardingAddr.EncodeAddress(),
})
}
@@ -79,11 +79,6 @@ type receiver struct {
Amount uint64 `json:"amount"`
}
// func (r *receiver) isOnchain() bool {
// _, err := btcutil.DecodeAddress(r.To, nil)
// return err == nil
// }
func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
ptx, err := psbt.New(nil, nil, 2, 0, nil)
if err != nil {
@@ -128,14 +123,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
explorer := utils.NewExplorer(ctx)
utxos, delayedUtxos, change, err := coinSelectOnchain(
utxos, change, err := coinSelectOnchain(
ctx, explorer, targetAmount, nil,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, utxos, delayedUtxos, &netParams); err != nil {
if err := addInputs(ctx, updater, utxos); err != nil {
return "", err
}
@@ -174,14 +169,14 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
updater.Upsbt.UnsignedTx.TxOut = updater.Upsbt.UnsignedTx.TxOut[:len(updater.Upsbt.UnsignedTx.TxOut)-1]
}
// reselect the difference
selected, delayedSelected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, append(utxos, delayedUtxos...),
selected, newChange, err := coinSelectOnchain(
ctx, explorer, feeAmount-change, utxos,
)
if err != nil {
return "", err
}
if err := addInputs(ctx, updater, selected, delayedSelected, &netParams); err != nil {
if err := addInputs(ctx, updater, selected); err != nil {
return "", err
}
@@ -225,20 +220,37 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
func coinSelectOnchain(
ctx *cli.Context,
explorer utils.Explorer, targetAmount uint64, exclude []utils.Utxo,
) ([]utils.Utxo, []utils.Utxo, uint64, error) {
_, onchainAddr, _, err := getAddress(ctx)
) ([]utils.Utxo, uint64, error) {
_, boardingAddr, redemptionAddr, err := getAddress(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
fromExplorer, err := explorer.GetUtxos(onchainAddr.EncodeAddress())
boardingUtxosFromExplorer, err := explorer.GetUtxos(boardingAddr.EncodeAddress())
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
utxos := make([]utils.Utxo, 0)
selectedAmount := uint64(0)
for _, utxo := range fromExplorer {
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
}
@@ -249,102 +261,67 @@ func coinSelectOnchain(
}
}
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
utxo := utils.NewUtxo(utxo, uint(timeoutBoarding))
if utxo.SpendableAt.After(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil
return utxos, selectedAmount - targetAmount, nil
}
userPubkey, err := utils.GetWalletPublicKey(ctx)
redemptionUtxosFromExplorer, err := explorer.GetUtxos(redemptionAddr.EncodeAddress())
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
vtxoExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, nil, 0, err
return nil, 0, err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return nil, nil, 0, err
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil {
return nil, nil, 0, err
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return nil, nil, 0, err
}
liquidNet := toChainParams(net)
p2tr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey),
&liquidNet,
)
if err != nil {
return nil, nil, 0, err
}
addr := p2tr.EncodeAddress()
fromExplorer, err = explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
}
delayedUtxos := make([]utils.Utxo, 0)
for _, utxo := range fromExplorer {
for _, utxo := range redemptionUtxosFromExplorer {
if selectedAmount >= targetAmount {
break
}
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(unilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) {
continue
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
delayedUtxos = append(delayedUtxos, utxo)
selectedAmount += utxo.Amount
utxo := utils.NewUtxo(utxo, uint(vtxoExitDelay))
if utxo.SpendableAt.After(now) {
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
}
if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf(
return nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
}
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
return utxos, selectedAmount - targetAmount, nil
}
func addInputs(
ctx *cli.Context,
updater *psbt.Updater,
utxos, delayedUtxos []utils.Utxo,
net *chaincfg.Params,
utxos []utils.Utxo,
) error {
_, onchainAddr, _, err := getAddress(ctx)
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
changeScript, err := txscript.PayToAddrScript(onchainAddr)
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
@@ -355,87 +332,41 @@ func addInputs(
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,
})
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
if err := updater.AddInWitnessUtxo(
&wire.TxOut{
Value: int64(utxo.Amount),
PkScript: changeScript,
},
len(updater.Upsbt.UnsignedTx.TxIn)-1,
); err != nil {
return err
}
}
if len(delayedUtxos) > 0 {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return err
}
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
_, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay,
)
if err != nil {
return err
}
p2tr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(vtxoTapKey), net)
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
script, err := txscript.PayToAddrScript(p2tr)
if err != nil {
return err
}
for _, utxo := range delayedUtxos {
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil {
return err
}
if err := addVtxoInput(
updater,
&wire.OutPoint{
Hash: *previousHash,
Index: utxo.Vout,
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
{
ControlBlock: controlBlockBytes,
Script: leafProof.Script,
LeafVersion: leafProof.LeafVersion,
},
uint(unilateralExitDelay),
leafProof,
); err != nil {
return err
}
if err := updater.AddInWitnessUtxo(
&wire.TxOut{
Value: int64(utxo.Amount),
PkScript: script,
},
len(updater.Upsbt.Inputs)-1,
); err != nil {
return err
}
}
},
})
}
return nil
@@ -461,40 +392,7 @@ func decodeReceiverAddress(addr string) (
return true, pkscript, nil, nil
}
func addVtxoInput(
updater *psbt.Updater, inputArgs *wire.OutPoint, exitDelay uint,
tapLeafProof *txscript.TapscriptProof,
) error {
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
if err != nil {
return nil
}
nextInputIndex := len(updater.Upsbt.Inputs)
updater.Upsbt.UnsignedTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *inputArgs,
Sequence: sequence,
})
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{})
controlBlock := tapLeafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return err
}
updater.Upsbt.Inputs[nextInputIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
{
ControlBlock: controlBlockBytes,
Script: tapLeafProof.Script,
LeafVersion: tapLeafProof.LeafVersion,
},
}
return nil
}
func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionAddr btcutil.Address, err error) {
func getAddress(ctx *cli.Context) (offchainAddr string, boardingAddr, redemptionAddr btcutil.Address, err error) {
userPubkey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return
@@ -510,6 +408,21 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
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
@@ -522,11 +435,6 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
netParams := toChainParams(arkNet)
p2wpkh, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(userPubkey.SerializeCompressed()), &netParams)
if err != nil {
return
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
@@ -534,7 +442,7 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
return
}
p2tr, err := btcutil.NewAddressTaproot(
redemptionP2TR, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey),
&netParams,
)
@@ -542,8 +450,20 @@ func getAddress(ctx *cli.Context) (offchainAddr string, onchainAddr, redemptionA
return
}
redemptionAddr = p2tr
onchainAddr = p2wpkh
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

View File

@@ -53,14 +53,16 @@ func getVtxos(
if v.GetSwept() {
continue
}
vtxos = append(vtxos, vtxo{
amount: v.GetReceiver().GetAmount(),
txid: v.GetOutpoint().GetTxid(),
vout: v.GetOutpoint().GetVout(),
poolTxid: v.GetPoolTxid(),
expireAt: expireAt,
pending: v.GetPending(),
})
if v.Outpoint.GetVtxoInput() != nil {
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount,
txid: v.Outpoint.GetVtxoInput().GetTxid(),
vout: v.Outpoint.GetVtxoInput().GetVout(),
poolTxid: v.PoolTxid,
expireAt: expireAt,
pending: v.GetPending(),
})
}
}
if !computeExpiration {
@@ -196,32 +198,10 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
return levels, nil
}
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
for _, level := range congestionTree {
levelProto := &arkv1.TreeLevel{
Nodes: make([]*arkv1.Node, 0, len(level)),
}
for _, node := range level {
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
})
}
levels = append(levels, levelProto)
}
return &arkv1.Tree{
Levels: levels,
}
}
func handleRoundStream(
ctx *cli.Context, client arkv1.ArkServiceClient, paymentID string,
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
vtxosToSign []vtxo, mustSignRoundTx bool,
secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
ephemeralKey *secp256k1.PrivateKey,
) (poolTxID string, err error) {
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
@@ -398,8 +378,8 @@ func handleRoundStream(
// stop pinging as soon as we receive some forfeit txs
pingStop()
poolTx := e.GetPoolTx()
ptx, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
roundTx := e.GetPoolTx()
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
if err != nil {
return "", err
}
@@ -428,7 +408,7 @@ func handleRoundStream(
if !isOnchainOnly(receivers) {
if err := bitcointree.ValidateCongestionTree(
congestionTree, poolTx, aspPubkey, int64(roundLifetime), int64(minRelayFee),
congestionTree, roundTx, aspPubkey, int64(roundLifetime), int64(minRelayFee),
); err != nil {
return "", err
}
@@ -529,80 +509,103 @@ func handleRoundStream(
fmt.Println("congestion tree validated")
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
fmt.Print("signing forfeit txs... ")
explorer := utils.NewExplorer(ctx)
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return "", err
}
txid := p.UnsignedTx.TxHash().String()
finalizePaymentRequest := &arkv1.FinalizePaymentRequest{}
connectorsTxids = append(connectorsTxids, txid)
}
if len(vtxosToSign) > 0 {
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
for _, forfeit := range forfeits {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
if err != nil {
return "", err
fmt.Print("signing forfeit txs... ")
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return "", err
}
txid := p.UnsignedTx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
}
for _, input := range ptx.UnsignedTx.TxIn {
inputTxid := input.PreviousOutPoint.Hash.String()
for _, forfeit := range forfeits {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
if err != nil {
return "", err
}
for _, coin := range vtxosToSign {
// check if it contains one of the input to sign
if inputTxid == coin.txid {
// verify that the connector is in the connectors list
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
for _, input := range ptx.UnsignedTx.TxIn {
inputTxid := input.PreviousOutPoint.Hash.String()
for _, coin := range vtxosToSign {
// check if it contains one of the input to sign
if inputTxid == coin.txid {
// verify that the connector is in the connectors list
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
}
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
return "", err
}
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
return "", err
}
signedPset, err := ptx.B64Encode()
if err != nil {
return "", err
}
signedPset, err := ptx.B64Encode()
if err != nil {
return "", err
}
signedForfeits = append(signedForfeits, signedPset)
signedForfeits = append(signedForfeits, signedPset)
}
}
}
}
}
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx.Context, client, pingReq)
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(vtxosToSign) > 0 && len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx.Context, client, pingReq)
}
continue
}
continue
fmt.Printf("%d signed\n", len(signedForfeits))
finalizePaymentRequest.SignedForfeitTxs = signedForfeits
}
if mustSignRoundTx {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(roundTx), true)
if err != nil {
return "", err
}
if err := signPsbt(ctx, ptx, explorer, secKey); err != nil {
return "", err
}
signedRoundTx, err := ptx.B64Encode()
if err != nil {
return "", err
}
fmt.Println("round tx signed")
finalizePaymentRequest.SignedRoundTx = &signedRoundTx
}
fmt.Printf("%d signed\n", len(signedForfeits))
fmt.Print("finalizing payment... ")
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeits,
})
_, err = client.FinalizePayment(ctx.Context, finalizePaymentRequest)
if err != nil {
return "", err
}

View File

@@ -82,6 +82,7 @@ func connectToAsp(ctx *cli.Context, net, url, explorer string) error {
utils.UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
utils.MIN_RELAY_FEE: strconv.Itoa(int(resp.MinRelayFee)),
utils.EXPLORER: explorer,
utils.BOARDING_TEMPLATE: resp.GetBoardingDescriptorTemplate(),
})
}

View File

@@ -1,205 +0,0 @@
package covenantless
import (
"encoding/hex"
"fmt"
"strings"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/bitcointree"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
)
func (c *clArkBitcoinCLI) Onboard(ctx *cli.Context) error {
amount := ctx.Uint64("amount")
if amount <= 0 {
return fmt.Errorf("missing amount flag (--amount)")
}
net, err := utils.GetNetwork(ctx)
if err != nil {
return err
}
userPubKey, err := utils.GetWalletPublicKey(ctx)
if err != nil {
return err
}
client, cancel, err := getClientFromState(ctx)
if err != nil {
return err
}
defer cancel()
aspPubkey, err := utils.GetAspPublicKey(ctx)
if err != nil {
return err
}
roundLifetime, err := utils.GetRoundLifetime(ctx)
if err != nil {
return err
}
unilateralExitDelay, err := utils.GetUnilateralExitDelay(ctx)
if err != nil {
return err
}
minRelayFee, err := utils.GetMinRelayFee(ctx)
if err != nil {
return err
}
congestionTreeLeaf := bitcointree.Receiver{
Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
Amount: uint64(amount), // Convert amount to uint64
}
leaves := []bitcointree.Receiver{congestionTreeLeaf}
ephemeralKey, err := secp256k1.GeneratePrivateKey()
if err != nil {
return err
}
cosigners := []*secp256k1.PublicKey{ephemeralKey.PubKey()} // TODO asp as cosigner
sharedOutputScript, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
cosigners,
aspPubkey,
leaves,
uint64(minRelayFee),
roundLifetime,
unilateralExitDelay,
)
if err != nil {
return err
}
netParams := toChainParams(net)
address, err := btcutil.NewAddressTaproot(sharedOutputScript[2:], &netParams)
if err != nil {
return err
}
onchainReceiver := receiver{
To: address.EncodeAddress(),
Amount: uint64(sharedOutputAmount),
}
partialTx, err := sendOnchain(ctx, []receiver{onchainReceiver})
if err != nil {
return err
}
ptx, err := psbt.NewFromRawBytes(strings.NewReader(partialTx), true)
if err != nil {
return err
}
txid := ptx.UnsignedTx.TxHash().String()
congestionTree, err := bitcointree.CraftCongestionTree(
&wire.OutPoint{
Hash: ptx.UnsignedTx.TxHash(),
Index: 0,
},
cosigners,
aspPubkey,
leaves,
uint64(minRelayFee),
roundLifetime,
unilateralExitDelay,
)
if err != nil {
return err
}
sweepClosure := bitcointree.CSVSigClosure{
Pubkey: aspPubkey,
Seconds: uint(roundLifetime),
}
sweepTapLeaf, err := sweepClosure.Leaf()
if err != nil {
return err
}
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
root := sweepTapTree.RootNode.TapHash()
signer := bitcointree.NewTreeSignerSession(
ephemeralKey,
congestionTree,
minRelayFee,
root.CloneBytes(),
)
nonces, err := signer.GetNonces() // TODO send nonces to ASP
if err != nil {
return err
}
coordinator, err := bitcointree.NewTreeCoordinatorSession(
congestionTree,
minRelayFee,
root.CloneBytes(),
cosigners,
)
if err != nil {
return err
}
if err := coordinator.AddNonce(ephemeralKey.PubKey(), nonces); err != nil {
return err
}
aggregatedNonces, err := coordinator.AggregateNonces()
if err != nil {
return err
}
if err := signer.SetKeys(cosigners); err != nil {
return err
}
if err := signer.SetAggregatedNonces(aggregatedNonces); err != nil {
return err
}
sigs, err := signer.Sign()
if err != nil {
return err
}
if err := coordinator.AddSig(ephemeralKey.PubKey(), sigs); err != nil {
return err
}
signedTree, err := coordinator.SignTree()
if err != nil {
return err
}
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
BoardingTx: partialTx,
CongestionTree: castCongestionTree(signedTree),
UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
})
if err != nil {
return err
}
fmt.Println("onboard_txid:", txid)
return nil
}

View File

@@ -72,8 +72,12 @@ func collaborativeRedeem(
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}
@@ -110,6 +114,7 @@ func collaborativeRedeem(
client,
registerResponse.GetId(),
selectedCoins,
false,
secKey,
receivers,
ephemeralKey,

View File

@@ -14,7 +14,7 @@ import (
)
func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
receiver := ctx.String("to")
receiverAddr := ctx.String("to")
amount := ctx.Uint64("amount")
withExpiryCoinselect := ctx.Bool("enable-expiry-coinselect")
@@ -22,15 +22,22 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", amount, dust)
}
if receiver == "" {
if receiverAddr == "" {
return fmt.Errorf("receiver address is required")
}
isOnchain, _, _, err := decodeReceiverAddress(receiver)
isOnchain, _, _, err := decodeReceiverAddress(receiverAddr)
if err != nil {
return err
}
if isOnchain {
return fmt.Errorf("receiver address is onchain")
txid, err := sendOnchain(ctx, []receiver{{receiverAddr, amount}})
if err != nil {
return err
}
return utils.PrintJSON(map[string]interface{}{
"txid": txid,
})
}
offchainAddr, _, _, err := getAddress(ctx)
@@ -41,20 +48,20 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
if err != nil {
return err
}
_, _, aspKey, err := common.DecodeAddress(receiver)
_, _, aspKey, err := common.DecodeAddress(receiverAddr)
if err != nil {
return fmt.Errorf("invalid receiver address: %s", err)
}
if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver)
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiverAddr)
}
receiversOutput := make([]*arkv1.Output, 0)
sumOfReceivers := uint64(0)
receiversOutput = append(receiversOutput, &arkv1.Output{
Address: receiver,
Address: receiverAddr,
Amount: amount,
})
sumOfReceivers += amount
@@ -88,8 +95,12 @@ func (c *clArkBitcoinCLI) SendAsync(ctx *cli.Context) error {
for _, coin := range selectedCoins {
inputs = append(inputs, &arkv1.Input{
Txid: coin.txid,
Vout: coin.vout,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: coin.txid,
Vout: coin.vout,
},
},
})
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/ark-network/ark/client/utils"
"github.com/ark-network/ark/common/bitcointree"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
@@ -18,7 +17,7 @@ import (
)
func signPsbt(
ctx *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
_ *cli.Context, ptx *psbt.Packet, explorer utils.Explorer, prvKey *secp256k1.PrivateKey,
) error {
updater, err := psbt.NewUpdater(ptx)
if err != nil {
@@ -50,27 +49,11 @@ func signPsbt(
return err
}
sighashType := txscript.SigHashAll
if utxo.PkScript[0] == txscript.OP_1 {
sighashType = txscript.SigHashDefault
}
if err := updater.AddInSighashType(sighashType, i); err != nil {
if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil {
return err
}
}
_, onchainAddr, _, err := getAddress(ctx)
if err != nil {
return err
}
onchainWalletScript, err := txscript.PayToAddrScript(onchainAddr)
if err != nil {
return err
}
prevouts := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range updater.Upsbt.Inputs {
@@ -85,40 +68,6 @@ func signPsbt(
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
for i, input := range ptx.Inputs {
if bytes.Equal(input.WitnessUtxo.PkScript, onchainWalletScript) {
if err := updater.AddInSighashType(txscript.SigHashAll, i); err != nil {
return err
}
preimage, err := txscript.CalcWitnessSigHash(
input.WitnessUtxo.PkScript,
txsighashes,
txscript.SigHashAll,
updater.Upsbt.UnsignedTx,
i,
int64(input.WitnessUtxo.Value),
)
if err != nil {
return err
}
sig := ecdsa.Sign(
prvKey,
preimage,
)
signatureWithSighashType := append(sig.Serialize(), byte(txscript.SigHashAll))
updater.Upsbt.Inputs[i].PartialSigs = []*psbt.PartialSig{
{
PubKey: prvKey.PubKey().SerializeCompressed(),
Signature: signatureWithSighashType,
},
}
continue
}
if len(input.TaprootLeafScript) > 0 {
pubkey := prvKey.PubKey()
for _, leaf := range input.TaprootLeafScript {
@@ -178,7 +127,6 @@ func signPsbt(
}
}
}
}
return nil

View File

@@ -21,10 +21,6 @@ var (
Required: false,
Hidden: true,
}
AmountOnboardFlag = cli.Uint64Flag{
Name: "amount",
Usage: "amount to onboard in sats",
}
ExpiryDetailsFlag = cli.BoolFlag{
Name: "compute-expiry-details",
Usage: "compute client-side the VTXOs expiry time",

View File

@@ -2,6 +2,8 @@ module github.com/ark-network/ark/client
go 1.22.6
replace github.com/ark-network/ark/common => ../common
replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3
require (
@@ -19,9 +21,7 @@ require (
require (
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
)

View File

@@ -1,8 +1,6 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899 h1:PJL9Pam042F790x3mMovaIIkgeKIVaWm1aFOyH0k4PY=
github.com/ark-network/ark/api-spec v0.0.0-20240812233307-18e343b31899/go.mod h1:0B5seq/gzuGL8OZGUaO12yj73ZJKAde8L+nmLQAZ7IA=
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899 h1:PxcHv+KaBdfrZCHoNYSUiCdI2wNIZ3Oxx8ZUewcEesg=
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=

View File

@@ -8,7 +8,6 @@ type CLI interface {
Receive(ctx *cli.Context) error
Redeem(ctx *cli.Context) error
Send(ctx *cli.Context) error
ClaimAsync(ctx *cli.Context) error
Claim(ctx *cli.Context) error
SendAsync(ctx *cli.Context) error
Onboard(ctx *cli.Context) error
}

View File

@@ -74,19 +74,6 @@ var (
Flags: []cli.Flag{&flags.PasswordFlag, &flags.PrivateKeyFlag, &flags.NetworkFlag, &flags.UrlFlag, &flags.ExplorerFlag},
}
onboardCommand = cli.Command{
Name: "onboard",
Usage: "Onboard the Ark by lifting your funds",
Action: func(ctx *cli.Context) error {
cli, err := getCLIFromState(ctx)
if err != nil {
return err
}
return cli.Onboard(ctx)
},
Flags: []cli.Flag{&flags.AmountOnboardFlag, &flags.PasswordFlag},
}
sendCommand = cli.Command{
Name: "send",
Usage: "Send your onchain or offchain funds to one or many receivers",
@@ -117,7 +104,7 @@ var (
if err != nil {
return err
}
return cli.ClaimAsync(ctx)
return cli.Claim(ctx)
},
Flags: []cli.Flag{&flags.PasswordFlag},
}
@@ -167,7 +154,6 @@ func main() {
&redeemCommand,
&sendCommand,
&claimCommand,
&onboardCommand,
)
app.Flags = []cli.Flag{
flags.DatadirFlag,

View File

@@ -18,7 +18,7 @@ import (
"github.com/vulpemventures/go-elements/transaction"
)
type Utxo struct {
type ExplorerUtxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount uint64 `json:"value"`
@@ -32,9 +32,9 @@ type Utxo struct {
type Explorer interface {
GetTxHex(txid string) (string, error)
Broadcast(txHex string) (string, error)
GetUtxos(addr string) ([]Utxo, error)
GetUtxos(addr string) ([]ExplorerUtxo, error)
GetBalance(addr, asset string) (uint64, error)
GetRedeemedVtxosBalance(
GetDelayedBalance(
addr string, unilateralExitDelay int64,
) (uint64, map[int64]uint64, error)
GetTxBlocktime(txid string) (confirmed bool, blocktime int64, err error)
@@ -130,7 +130,7 @@ func (e *explorer) Broadcast(txStr string) (string, error) {
return txid, nil
}
func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
func (e *explorer) GetUtxos(addr string) ([]ExplorerUtxo, error) {
endpoint, err := url.JoinPath(e.baseUrl, "address", addr, "utxo")
if err != nil {
return nil, err
@@ -149,7 +149,7 @@ func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(string(body))
}
payload := []Utxo{}
payload := []ExplorerUtxo{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
@@ -175,7 +175,7 @@ func (e *explorer) GetBalance(addr, asset string) (uint64, error) {
return balance, nil
}
func (e *explorer) GetRedeemedVtxosBalance(
func (e *explorer) GetDelayedBalance(
addr string, unilateralExitDelay int64,
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
utxos, err := e.GetUtxos(addr)

View File

@@ -7,8 +7,10 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
)
@@ -18,6 +20,7 @@ const (
ASP_PUBKEY = "asp_public_key"
ROUND_LIFETIME = "round_lifetime"
UNILATERAL_EXIT_DELAY = "unilateral_exit_delay"
BOARDING_TEMPLATE = "boarding_template"
ENCRYPTED_PRVKEY = "encrypted_private_key"
PASSWORD_HASH = "password_hash"
PUBKEY = "public_key"
@@ -109,6 +112,27 @@ func GetUnilateralExitDelay(ctx *cli.Context) (int64, error) {
return int64(redeemDelay), nil
}
func GetBoardingDescriptor(ctx *cli.Context) (string, error) {
state, err := GetState(ctx)
if err != nil {
return "", err
}
pubkey, err := GetWalletPublicKey(ctx)
if err != nil {
return "", err
}
template := state[BOARDING_TEMPLATE]
if len(template) <= 0 {
return "", fmt.Errorf("missing boarding descriptor template")
}
pubkeyhex := hex.EncodeToString(schnorr.SerializePubKey(pubkey))
return strings.ReplaceAll(template, "USER", pubkeyhex), nil
}
func GetWalletPublicKey(ctx *cli.Context) (*secp256k1.PublicKey, error) {
state, err := GetState(ctx)
if err != nil {

36
client/utils/types.go Normal file
View File

@@ -0,0 +1,36 @@
package utils
import (
"time"
"github.com/ark-network/ark/common"
)
type Utxo struct {
Txid string
Vout uint32
Amount uint64
Asset string // optional
Delay uint
SpendableAt time.Time
}
func (u *Utxo) Sequence() (uint32, error) {
return common.BIP68EncodeAsNumber(u.Delay)
}
func NewUtxo(explorerUtxo ExplorerUtxo, delay uint) Utxo {
utxoTime := explorerUtxo.Status.Blocktime
if utxoTime == 0 {
utxoTime = time.Now().Unix()
}
return Utxo{
Txid: explorerUtxo.Txid,
Vout: explorerUtxo.Vout,
Amount: explorerUtxo.Amount,
Asset: explorerUtxo.Asset,
Delay: delay,
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
}
}