Support onboarding & Drop faucet (#119)

* Renaming

* Add server-side support for onboarding

* add onboard --amount command

* support client side onboarding

* Drop dummy tx builder

* Drop faucet

* Fixes

* fix public key encoding

* fix schnorr pub key check in validation

* fix server/README to accomodate onboarding

---------

Co-authored-by: Louis <louis@vulpem.com>
Co-authored-by: João Bordalo <bordalix@users.noreply.github.com>
This commit is contained in:
Pietralberto Mazza
2024-02-23 16:24:00 +01:00
committed by GitHub
parent a95a829b20
commit 1650ea5935
37 changed files with 1190 additions and 2079 deletions

View File

@@ -18,7 +18,7 @@ var expiryDetailsFlag = cli.BoolFlag{
var balanceCommand = cli.Command{ var balanceCommand = cli.Command{
Name: "balance", Name: "balance",
Usage: "Print balance of the Ark wallet", Usage: "Shows the onchain and offchain balance of the Ark wallet",
Action: balanceAction, Action: balanceAction,
Flags: []cli.Flag{&expiryDetailsFlag}, Flags: []cli.Flag{&expiryDetailsFlag},
} }

View File

@@ -15,6 +15,7 @@ import (
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@@ -596,7 +597,7 @@ func handleRoundStream(
if len(output.Script) == 0 { if len(output.Script) == 0 {
continue continue
} }
if bytes.Equal(output.Script[2:], outputTapKey.SerializeCompressed()) { if bytes.Equal(output.Script[2:], schnorr.SerializePubKey(outputTapKey)) {
if output.Value != receiver.Amount { if output.Value != receiver.Amount {
continue continue
} }
@@ -733,6 +734,29 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
return levels, nil 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 decodeReceiverAddress(addr string) ( func decodeReceiverAddress(addr string) (
isOnChainAddress bool, isOnChainAddress bool,
onchainScript []byte, onchainScript []byte,

View File

@@ -6,7 +6,7 @@ import (
var configCommand = cli.Command{ var configCommand = cli.Command{
Name: "config", Name: "config",
Usage: "Print local configuration of the Ark CLI", Usage: "Shows configuration of the Ark wallet",
Action: printConfigAction, Action: printConfigAction,
} }

View File

@@ -8,12 +8,12 @@ import (
var dumpCommand = cli.Command{ var dumpCommand = cli.Command{
Name: "dump-privkey", Name: "dump-privkey",
Usage: "Dump private key of the Ark wallet", Usage: "Dumps private key of the Ark wallet",
Action: dumpAction, Action: dumpAction,
} }
func dumpAction(ctx *cli.Context) error { func dumpAction(ctx *cli.Context) error {
privateKey, err := privateKeyFromPassword() privateKey, err := privateKeyFromPassword()
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,71 +0,0 @@
package main
import (
"context"
"fmt"
"io"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/urfave/cli/v2"
)
var faucetCommand = cli.Command{
Name: "faucet",
Usage: "Faucet your wallet",
Action: faucetAction,
}
func faucetAction(ctx *cli.Context) error {
addr, _, err := getAddress()
if err != nil {
return err
}
client, close, err := getClientFromState(ctx)
if err != nil {
return err
}
defer close()
_, err = client.Faucet(ctx.Context, &arkv1.FaucetRequest{
Address: addr,
})
if err != nil {
return err
}
eventStream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{})
if err != nil {
return err
}
for {
event, err := eventStream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
if event.GetRoundFinalization() != nil {
if _, err := client.FinalizePayment(context.Background(), &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: event.GetRoundFinalization().GetForfeitTxs(),
}); err != nil {
return err
}
}
if event.GetRoundFailed() != nil {
return fmt.Errorf("faucet failed: %s", event.GetRoundFailed().GetReason())
}
if event.GetRoundFinalized() != nil {
return printJSON(map[string]interface{}{
"pool_txid": event.GetRoundFinalized().GetPoolTxid(),
})
}
}
return nil
}

View File

@@ -36,7 +36,7 @@ var (
var initCommand = cli.Command{ var initCommand = cli.Command{
Name: "init", Name: "init",
Usage: "initialize the wallet with an encryption password, and connect it to an ASP", Usage: "Initialize your Ark wallet with an encryption password, and connect it to an ASP",
Action: initAction, Action: initAction,
Flags: []cli.Flag{&passwordFlag, &privateKeyFlag, &networkFlag, &urlFlag}, Flags: []cli.Flag{&passwordFlag, &privateKeyFlag, &networkFlag, &urlFlag},
} }

View File

@@ -57,17 +57,17 @@ func main() {
app.Version = version app.Version = version
app.Name = "Ark CLI" app.Name = "Ark CLI"
app.Usage = "command line interface for Ark wallet" app.Usage = "ark wallet command line interface"
app.Commands = append( app.Commands = append(
app.Commands, app.Commands,
&balanceCommand, &balanceCommand,
&configCommand, &configCommand,
&dumpCommand, &dumpCommand,
&faucetCommand,
&initCommand, &initCommand,
&receiveCommand, &receiveCommand,
&redeemCommand, &redeemCommand,
&sendCommand, &sendCommand,
&onboardCommand,
) )
app.Before = func(ctx *cli.Context) error { app.Before = func(ctx *cli.Context) error {

147
client/onboard.go Normal file
View File

@@ -0,0 +1,147 @@
package main
import (
"encoding/hex"
"fmt"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"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
)
var (
amountOnboardFlag = cli.Uint64Flag{
Name: "amount",
Usage: "amount to onboard in sats",
Required: true,
}
)
var onboardCommand = cli.Command{
Name: "onboard",
Usage: "Onboard the Ark by lifting your funds",
Action: onboardAction,
Flags: []cli.Flag{&amountOnboardFlag},
}
func onboardAction(ctx *cli.Context) error {
amount := ctx.Uint64("amount")
if amount <= 0 {
return fmt.Errorf("missing amount flag (--amount)")
}
_, net := getNetwork()
aspPubkey, err := getServiceProviderPublicKey()
if err != nil {
return err
}
lifetime, err := getLifetime()
if err != nil {
return err
}
exitDelay, err := getExitDelay()
if err != nil {
return err
}
userPubKey, err := getWalletPublicKey()
if err != nil {
return err
}
congestionTreeLeaf := tree.Receiver{
Pubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
Amount: amount,
}
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
net.AssetID, aspPubkey, []tree.Receiver{congestionTreeLeaf}, minRelayFee, lifetime, exitDelay,
)
if err != nil {
return err
}
pay, err := payment.FromScript(sharedOutputScript, net, 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
}
txid, err := broadcastPset(pset)
if err != nil {
return err
}
fmt.Println("onboard txid:", txid)
fmt.Println("waiting for confirmation... (this may take a while, do not cancel the process)")
// wait for the transaction to be confirmed
if err := waitForTxConfirmation(ctx, txid); err != nil {
return err
}
fmt.Println("transaction confirmed")
congestionTree, err := treeFactoryFn(psetv2.InputArgs{
Txid: txid,
TxIndex: 0,
})
if err != nil {
return err
}
client, close, err := getClientFromState(ctx)
if err != nil {
return err
}
defer close()
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
BoardingTx: pset,
CongestionTree: castCongestionTree(congestionTree),
UserPubkey: hex.EncodeToString(userPubKey.SerializeCompressed()),
})
if err != nil {
return err
}
return nil
}
func waitForTxConfirmation(ctx *cli.Context, txid string) error {
isConfirmed := false
for !isConfirmed {
time.Sleep(5 * time.Second)
isConfirmed, _, _ = getTxBlocktime(txid)
}
return nil
}

View File

@@ -6,7 +6,7 @@ import (
var receiveCommand = cli.Command{ var receiveCommand = cli.Command{
Name: "receive", Name: "receive",
Usage: "Print the Ark address associated with your wallet and the connected Ark", Usage: "Shows both onchain and offchain addresses",
Action: receiveAction, Action: receiveAction,
} }

View File

@@ -38,7 +38,7 @@ var (
var redeemCommand = cli.Command{ var redeemCommand = cli.Command{
Name: "redeem", Name: "redeem",
Usage: "Redeem VTXO(s) to onchain", Usage: "Redeem your offchain funds, either collaboratively or unilaterally",
Flags: []cli.Flag{&addressFlag, &amountToRedeemFlag, &forceFlag}, Flags: []cli.Flag{&addressFlag, &amountToRedeemFlag, &forceFlag},
Action: redeemAction, Action: redeemAction,
} }

View File

@@ -40,7 +40,7 @@ var (
var sendCommand = cli.Command{ var sendCommand = cli.Command{
Name: "send", Name: "send",
Usage: "Send VTXOs to a list of addresses", Usage: "Send your onchain or offchain funds to one or many receivers",
Action: sendAction, Action: sendAction,
Flags: []cli.Flag{&receiversFlag, &toFlag, &amountFlag}, Flags: []cli.Flag{&receiversFlag, &toFlag, &amountFlag},
} }
@@ -83,9 +83,19 @@ func sendAction(ctx *cli.Context) error {
} }
if len(onchainReceivers) > 0 { if len(onchainReceivers) > 0 {
if err := sendOnchain(ctx, onchainReceivers); err != nil { pset, err := sendOnchain(ctx, onchainReceivers)
if err != nil {
return err return err
} }
txid, err := broadcastPset(pset)
if err != nil {
return err
}
return printJSON(map[string]interface{}{
"txid": txid,
})
} }
if len(offchainReceivers) > 0 { if len(offchainReceivers) > 0 {
@@ -203,14 +213,14 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
}) })
} }
func sendOnchain(ctx *cli.Context, receivers []receiver) error { func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
pset, err := psetv2.New(nil, nil, nil) pset, err := psetv2.New(nil, nil, nil)
if err != nil { if err != nil {
return err return "", err
} }
updater, err := psetv2.NewUpdater(pset) updater, err := psetv2.NewUpdater(pset)
if err != nil { if err != nil {
return err return "", err
} }
_, net := getNetwork() _, net := getNetwork()
@@ -219,12 +229,12 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
for _, receiver := range receivers { for _, receiver := range receivers {
targetAmount += receiver.Amount targetAmount += receiver.Amount
if receiver.Amount < DUST { if receiver.Amount < DUST {
return fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, DUST) return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount, DUST)
} }
script, err := address.ToOutputScript(receiver.To) script, err := address.ToOutputScript(receiver.To)
if err != nil { if err != nil {
return err return "", err
} }
if err := updater.AddOutputs([]psetv2.OutputArgs{ if err := updater.AddOutputs([]psetv2.OutputArgs{
@@ -234,28 +244,28 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
Script: script, Script: script,
}, },
}); err != nil { }); err != nil {
return err return "", err
} }
} }
selected, delayedSelected, change, err := coinSelectOnchain(targetAmount, nil) selected, delayedSelected, change, err := coinSelectOnchain(targetAmount, nil)
if err != nil { if err != nil {
return err return "", err
} }
if err := addInputs(updater, selected, delayedSelected, net); err != nil { if err := addInputs(updater, selected, delayedSelected, net); err != nil {
return err return "", err
} }
if change > 0 { if change > 0 {
_, changeAddr, err := getAddress() _, changeAddr, err := getAddress()
if err != nil { if err != nil {
return err return "", err
} }
changeScript, err := address.ToOutputScript(changeAddr) changeScript, err := address.ToOutputScript(changeAddr)
if err != nil { if err != nil {
return err return "", err
} }
if err := updater.AddOutputs([]psetv2.OutputArgs{ if err := updater.AddOutputs([]psetv2.OutputArgs{
@@ -265,13 +275,13 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
Script: changeScript, Script: changeScript,
}, },
}); err != nil { }); err != nil {
return err return "", err
} }
} }
utx, err := updater.Pset.UnsignedTx() utx, err := pset.UnsignedTx()
if err != nil { if err != nil {
return err return "", err
} }
vBytes := utx.VirtualSize() vBytes := utx.VirtualSize()
@@ -291,22 +301,22 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
append(selected, delayedSelected...), append(selected, delayedSelected...),
) )
if err != nil { if err != nil {
return err return "", err
} }
if err := addInputs(updater, selected, delayedSelected, net); err != nil { if err := addInputs(updater, selected, delayedSelected, net); err != nil {
return err return "", err
} }
if newChange > 0 { if newChange > 0 {
_, changeAddr, err := getAddress() _, changeAddr, err := getAddress()
if err != nil { if err != nil {
return err return "", err
} }
changeScript, err := address.ToOutputScript(changeAddr) changeScript, err := address.ToOutputScript(changeAddr)
if err != nil { if err != nil {
return err return "", err
} }
if err := updater.AddOutputs([]psetv2.OutputArgs{ if err := updater.AddOutputs([]psetv2.OutputArgs{
@@ -316,7 +326,7 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
Script: changeScript, Script: changeScript,
}, },
}); err != nil { }); err != nil {
return err return "", err
} }
} }
} }
@@ -327,40 +337,42 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) error {
Amount: feeAmount, Amount: feeAmount,
}, },
}); err != nil { }); err != nil {
return err return "", err
} }
prvKey, err := privateKeyFromPassword() prvKey, err := privateKeyFromPassword()
if err != nil { if err != nil {
return err return "", err
} }
explorer := NewExplorer() explorer := NewExplorer()
if err := signPset(updater.Pset, explorer, prvKey); err != nil { if err := signPset(updater.Pset, explorer, prvKey); err != nil {
return err return "", err
} }
if err := psetv2.FinalizeAll(updater.Pset); err != nil { if err := psetv2.FinalizeAll(updater.Pset); err != nil {
return err return "", err
}
return updater.Pset.ToBase64()
}
func broadcastPset(psetB64 string) (string, error) {
pset, err := psetv2.NewPsetFromBase64(psetB64)
if err != nil {
return "", err
} }
extracted, err := psetv2.Extract(pset) extracted, err := psetv2.Extract(pset)
if err != nil { if err != nil {
return err return "", err
} }
hex, err := extracted.ToHex() hex, err := extracted.ToHex()
if err != nil { if err != nil {
return err return "", err
} }
txid, err := explorer.Broadcast(hex) return NewExplorer().Broadcast(hex)
if err != nil {
return err
}
return printJSON(map[string]interface{}{
"txid": txid,
})
} }

View File

@@ -1,22 +1,49 @@
package txbuilder package tree
import ( import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common/tree" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/ark-network/ark/internal/core/domain"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot" "github.com/vulpemventures/go-elements/taproot"
) )
type treeFactory func(outpoint psetv2.InputArgs) (tree.CongestionTree, error) func CraftCongestionTree(
asset string, aspPublicKey *secp256k1.PublicKey,
receivers []Receiver, feeSatsPerNode uint64, roundLifetime int64, exitDelay int64,
) (
buildCongestionTree TreeFactory,
sharedOutputScript []byte, sharedOutputAmount uint64, err error,
) {
root, err := createPartialCongestionTree(
receivers, aspPublicKey, asset, feeSatsPerNode, roundLifetime, exitDelay,
)
if err != nil {
return
}
taprootKey, _, err := root.getWitnessData()
if err != nil {
return
}
sharedOutputScript, err = taprootOutputScript(taprootKey)
if err != nil {
return
}
sharedOutputAmount = root.getAmount() + root.feeSats
buildCongestionTree = root.createFinalCongestionTree()
return
}
type node struct { type node struct {
sweepKey *secp256k1.PublicKey sweepKey *secp256k1.PublicKey
receivers []domain.Receiver receivers []Receiver
left *node left *node
right *node right *node
asset string asset string
@@ -131,7 +158,7 @@ func (n *node) getWitnessData() (
return n._inputTaprootKey, n._inputTaprootTree, nil return n._inputTaprootKey, n._inputTaprootTree, nil
} }
sweepClosure := &tree.CSVSigClosure{ sweepClosure := &CSVSigClosure{
Pubkey: n.sweepKey, Pubkey: n.sweepKey,
Seconds: uint(n.roundLifetime), Seconds: uint(n.roundLifetime),
} }
@@ -147,7 +174,7 @@ func (n *node) getWitnessData() (
return nil, nil, err return nil, nil, err
} }
unrollClosure := &tree.UnrollClosure{ unrollClosure := &UnrollClosure{
LeftKey: taprootKey, LeftKey: taprootKey,
LeftAmount: n.getAmount(), LeftAmount: n.getAmount(),
} }
@@ -163,7 +190,7 @@ func (n *node) getWitnessData() (
root := branchTaprootTree.RootNode.TapHash() root := branchTaprootTree.RootNode.TapHash()
inputTapkey := taproot.ComputeTaprootOutputKey( inputTapkey := taproot.ComputeTaprootOutputKey(
tree.UnspendableKey(), UnspendableKey(),
root[:], root[:],
) )
@@ -186,7 +213,7 @@ func (n *node) getWitnessData() (
leftAmount := n.left.getAmount() + n.feeSats leftAmount := n.left.getAmount() + n.feeSats
rightAmount := n.right.getAmount() + n.feeSats rightAmount := n.right.getAmount() + n.feeSats
unrollClosure := &tree.UnrollClosure{ unrollClosure := &UnrollClosure{
LeftKey: leftKey, LeftKey: leftKey,
LeftAmount: leftAmount, LeftAmount: leftAmount,
RightKey: rightKey, RightKey: rightKey,
@@ -204,7 +231,7 @@ func (n *node) getWitnessData() (
root := branchTaprootTree.RootNode.TapHash() root := branchTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey( taprootKey := taproot.ComputeTaprootOutputKey(
tree.UnspendableKey(), UnspendableKey(),
root[:], root[:],
) )
@@ -231,7 +258,7 @@ func (n *node) getVtxoWitnessData() (
return nil, nil, err return nil, nil, err
} }
redeemClosure := &tree.CSVSigClosure{ redeemClosure := &CSVSigClosure{
Pubkey: pubkey, Pubkey: pubkey,
Seconds: uint(n.exitDelay), Seconds: uint(n.exitDelay),
} }
@@ -241,7 +268,7 @@ func (n *node) getVtxoWitnessData() (
return nil, nil, err return nil, nil, err
} }
forfeitClosure := &tree.ForfeitClosure{ forfeitClosure := &ForfeitClosure{
Pubkey: pubkey, Pubkey: pubkey,
AspPubkey: n.sweepKey, AspPubkey: n.sweepKey,
} }
@@ -257,7 +284,7 @@ func (n *node) getVtxoWitnessData() (
root := leafTaprootTree.RootNode.TapHash() root := leafTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey( taprootKey := taproot.ComputeTaprootOutputKey(
tree.UnspendableKey(), UnspendableKey(),
root[:], root[:],
) )
@@ -266,25 +293,24 @@ func (n *node) getVtxoWitnessData() (
func (n *node) getTreeNode( func (n *node) getTreeNode(
input psetv2.InputArgs, tapTree *taproot.IndexedElementsTapScriptTree, input psetv2.InputArgs, tapTree *taproot.IndexedElementsTapScriptTree,
) (tree.Node, error) { ) (Node, error) {
pset, err := n.getTx(input, tapTree) pset, err := n.getTx(input, tapTree)
if err != nil { if err != nil {
return tree.Node{}, err return Node{}, err
} }
txid, err := getPsetId(pset) txid, err := getPsetId(pset)
if err != nil { if err != nil {
return tree.Node{}, err return Node{}, err
} }
tx, err := pset.ToBase64() tx, err := pset.ToBase64()
if err != nil { if err != nil {
return tree.Node{}, err return Node{}, err
} }
parentTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String() parentTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
return tree.Node{ return Node{
Txid: txid, Txid: txid,
Tx: tx, Tx: tx,
ParentTxid: parentTxid, ParentTxid: parentTxid,
@@ -306,7 +332,7 @@ func (n *node) getTx(
} }
if err := addTaprootInput( if err := addTaprootInput(
updater, input, tree.UnspendableKey(), inputTapTree, updater, input, UnspendableKey(), inputTapTree,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -328,9 +354,9 @@ func (n *node) getTx(
return pset, nil return pset, nil
} }
func (n *node) createFinalCongestionTree() treeFactory { func (n *node) createFinalCongestionTree() TreeFactory {
return func(poolTxInput psetv2.InputArgs) (tree.CongestionTree, error) { return func(poolTxInput psetv2.InputArgs) (CongestionTree, error) {
congestionTree := make(tree.CongestionTree, 0) congestionTree := make(CongestionTree, 0)
_, taprootTree, err := n.getWitnessData() _, taprootTree, err := n.getWitnessData()
if err != nil { if err != nil {
@@ -346,7 +372,7 @@ func (n *node) createFinalCongestionTree() treeFactory {
nextInputsArgs := make([]psetv2.InputArgs, 0) nextInputsArgs := make([]psetv2.InputArgs, 0)
nextTaprootTrees := make([]*taproot.IndexedElementsTapScriptTree, 0) nextTaprootTrees := make([]*taproot.IndexedElementsTapScriptTree, 0)
treeLevel := make([]tree.Node, 0) treeLevel := make([]Node, 0)
for i, node := range nodes { for i, node := range nodes {
treeNode, err := node.getTreeNode(ins[i], inTrees[i]) treeNode, err := node.getTreeNode(ins[i], inTrees[i])
@@ -385,38 +411,8 @@ func (n *node) createFinalCongestionTree() treeFactory {
} }
} }
func craftCongestionTree(
asset string, aspPublicKey *secp256k1.PublicKey,
payments []domain.Payment, feeSatsPerNode uint64, roundLifetime int64, exitDelay int64,
) (
buildCongestionTree treeFactory,
sharedOutputScript []byte, sharedOutputAmount uint64, err error,
) {
receivers := getOffchainReceivers(payments)
root, err := createPartialCongestionTree(
receivers, aspPublicKey, asset, feeSatsPerNode, roundLifetime, exitDelay,
)
if err != nil {
return
}
taprootKey, _, err := root.getWitnessData()
if err != nil {
return
}
sharedOutputScript, err = taprootOutputScript(taprootKey)
if err != nil {
return
}
sharedOutputAmount = root.getAmount() + root.feeSats
buildCongestionTree = root.createFinalCongestionTree()
return
}
func createPartialCongestionTree( func createPartialCongestionTree(
receivers []domain.Receiver, receivers []Receiver,
aspPublicKey *secp256k1.PublicKey, aspPublicKey *secp256k1.PublicKey,
asset string, asset string,
feeSatsPerNode uint64, feeSatsPerNode uint64,
@@ -431,7 +427,7 @@ func createPartialCongestionTree(
for _, r := range receivers { for _, r := range receivers {
leafNode := &node{ leafNode := &node{
sweepKey: aspPublicKey, sweepKey: aspPublicKey,
receivers: []domain.Receiver{r}, receivers: []Receiver{r},
asset: asset, asset: asset,
feeSats: feeSatsPerNode, feeSats: feeSatsPerNode,
roundLifetime: roundLifetime, roundLifetime: roundLifetime,
@@ -478,3 +474,45 @@ func createUpperLevel(nodes []*node) ([]*node, error) {
} }
return pairs, nil return pairs, nil
} }
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
}
func getPsetId(pset *psetv2.Pset) (string, error) {
utx, err := pset.UnsignedTx()
if err != nil {
return "", err
}
return utx.TxHash().String(), nil
}
// wrapper of updater methods adding a taproot input to the pset with all the necessary data to spend it via any taproot script
func addTaprootInput(
updater *psetv2.Updater,
input psetv2.InputArgs,
internalTaprootKey *secp256k1.PublicKey,
taprootTree *taproot.IndexedElementsTapScriptTree,
) error {
if err := updater.AddInputs([]psetv2.InputArgs{input}); err != nil {
return err
}
if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil {
return err
}
for _, proof := range taprootTree.LeafMerkleProofs {
controlBlock := proof.ToControlBlock(internalTaprootKey)
if err := updater.AddInTapLeafScript(0, psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(proof.Script),
ControlBlock: controlBlock,
}); err != nil {
return err
}
}
return nil
}

10
common/tree/type.go Normal file
View File

@@ -0,0 +1,10 @@
package tree
import "github.com/vulpemventures/go-elements/psetv2"
type TreeFactory func(outpoint psetv2.InputArgs) (CongestionTree, error)
type Receiver struct {
Pubkey string
Amount uint64
}

View File

@@ -236,7 +236,7 @@ func validateNodeTransaction(
switch c := close.(type) { switch c := close.(type) {
case *CSVSigClosure: case *CSVSigClosure:
isASP := c.Pubkey.IsEqual(expectedPublicKeyASP) isASP := bytes.Equal(schnorr.SerializePubKey(c.Pubkey), schnorr.SerializePubKey(expectedPublicKeyASP))
isSweepDelay := int64(c.Seconds) == expectedSequenceSeconds isSweepDelay := int64(c.Seconds) == expectedSequenceSeconds
if isASP && !isSweepDelay { if isASP && !isSweepDelay {

View File

@@ -73,22 +73,34 @@ This will add a `state.json` file to the following directory:
```bash ```bash
$ export ARK_WALLET_DATADIR=path/to/custom $ export ARK_WALLET_DATADIR=path/to/custom
$ ark init --password <password> --ark-url localhost:6000 $ ark init --password <password> --ark-url localhost:6000
```
Add funds to the ark wallet: Add funds to the ark wallet:
``` ```
$ ark faucet $ ark receive
# ark now has 10000 sats on its offchain balance {
"offchain_address": <address starting with "tark1q...">,
"onchain_address": <address starting with "tex1q...">
}
``` ```
Fund the `onchain_address` with https://liquidtestnet.com/faucet.
Onboard the ark:
```
$ ark onboard --amount 21000
```
After confirmation, ark wallet will be funded and ready to spend offchain.
In **another tab**, setup another ark wallet with: In **another tab**, setup another ark wallet with:
``` ```
$ export ARK_WALLET_DATADIR=./datadir $ export ARK_WALLET_DATADIR=./datadir
$ alias ark2=$(pwd)/build/ark-cli-<os>-<arch> $ alias ark2=$(pwd)/build/ark-cli-<os>-<arch>
$ ark2 init --password <password> --ark-url localhost:6000 $ ark2 init --password <password> --ark-url localhost:6000
$ ark2 faucet
# ark2 now has 10000 sats on ark
``` ```
**Note:** `ark2` should always run in the second tab. **Note:** `ark2` should always run in the second tab.
@@ -98,7 +110,7 @@ $ ark2 faucet
You can now make ark payments between the 2 ark wallets: You can now make ark payments between the 2 ark wallets:
``` ```
$ ark receive $ ark2 receive
{ {
"offchain_address": <address starting with "tark1q...">, "offchain_address": <address starting with "tark1q...">,
"onchain_address": <address starting with "tex1q...">, "onchain_address": <address starting with "tex1q...">,
@@ -109,7 +121,7 @@ $ ark receive
``` ```
``` ```
$ ark2 send --to <offchain_address> --amount 2100 $ ark send --to <ark2 offchain address> --amount 2100
``` ```
Both balances should reflect the payment: Both balances should reflect the payment:
@@ -117,15 +129,15 @@ Both balances should reflect the payment:
``` ```
$ ark balance $ ark balance
{ {
"offchain_balance": 12100, "offchain_balance": 18900,
"onchain_balance": 0 "onchain_balance": 78872
} }
``` ```
``` ```
$ ark2 balance $ ark2 balance
{ {
"offchain_balance": 7900, "offchain_balance": 2100,
"onchain_balance": 0 "onchain_balance": 0
} }
``` ```

View File

@@ -47,36 +47,6 @@
] ]
} }
}, },
"/v1/faucet/{address}": {
"post": {
"operationId": "ArkService_Faucet",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1FaucetResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "address",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"ArkService"
]
}
},
"/v1/info": { "/v1/info": {
"get": { "get": {
"operationId": "ArkService_GetInfo", "operationId": "ArkService_GetInfo",
@@ -99,6 +69,38 @@
] ]
} }
}, },
"/v1/onboard": {
"post": {
"operationId": "ArkService_Onboard",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1OnboardResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1OnboardRequest"
}
}
],
"tags": [
"ArkService"
]
}
},
"/v1/payment/claim": { "/v1/payment/claim": {
"post": { "post": {
"operationId": "ArkService_ClaimPayment", "operationId": "ArkService_ClaimPayment",
@@ -335,9 +337,6 @@
"v1ClaimPaymentResponse": { "v1ClaimPaymentResponse": {
"type": "object" "type": "object"
}, },
"v1FaucetResponse": {
"type": "object"
},
"v1FinalizePaymentRequest": { "v1FinalizePaymentRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -429,6 +428,23 @@
} }
} }
}, },
"v1OnboardRequest": {
"type": "object",
"properties": {
"boardingTx": {
"type": "string"
},
"congestionTree": {
"$ref": "#/definitions/v1Tree"
},
"userPubkey": {
"type": "string"
}
}
},
"v1OnboardResponse": {
"type": "object"
},
"v1Output": { "v1Output": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -38,11 +38,6 @@ service ArkService {
get: "/v1/ping/{payment_id}" get: "/v1/ping/{payment_id}"
}; };
}; };
rpc Faucet(FaucetRequest) returns (FaucetResponse) {
option (google.api.http) = {
post: "/v1/faucet/{address}"
};
}
rpc ListVtxos(ListVtxosRequest) returns (ListVtxosResponse) { rpc ListVtxos(ListVtxosRequest) returns (ListVtxosResponse) {
option (google.api.http) = { option (google.api.http) = {
get: "/v1/vtxos/{address}" get: "/v1/vtxos/{address}"
@@ -53,6 +48,21 @@ service ArkService {
get: "/v1/info" get: "/v1/info"
}; };
} }
rpc Onboard(OnboardRequest) returns (OnboardResponse) {
option (google.api.http) = {
post: "/v1/onboard"
body: "*"
};
}
}
message OnboardRequest {
string boarding_tx = 1;
Tree congestion_tree = 2;
string user_pubkey = 3;
}
message OnboardResponse {
} }
message RegisterPaymentRequest { message RegisterPaymentRequest {
@@ -151,12 +161,6 @@ message PingRequest {
message PingResponse {} message PingResponse {}
message FaucetRequest {
string address = 1;
}
message FaucetResponse {}
message ListVtxosRequest { message ListVtxosRequest {
string address = 1; string address = 1;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -230,58 +230,6 @@ func local_request_ArkService_Ping_0(ctx context.Context, marshaler runtime.Mars
} }
func request_ArkService_Faucet_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq FaucetRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["address"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "address")
}
protoReq.Address, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "address", err)
}
msg, err := client.Faucet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_ArkService_Faucet_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq FaucetRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["address"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "address")
}
protoReq.Address, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "address", err)
}
msg, err := server.Faucet(ctx, &protoReq)
return msg, metadata, err
}
func request_ArkService_ListVtxos_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { func request_ArkService_ListVtxos_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ListVtxosRequest var protoReq ListVtxosRequest
var metadata runtime.ServerMetadata var metadata runtime.ServerMetadata
@@ -352,6 +300,32 @@ func local_request_ArkService_GetInfo_0(ctx context.Context, marshaler runtime.M
} }
func request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.Marshaler, client ArkServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq OnboardRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Onboard(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_ArkService_Onboard_0(ctx context.Context, marshaler runtime.Marshaler, server ArkServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq OnboardRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Onboard(ctx, &protoReq)
return msg, metadata, err
}
// RegisterArkServiceHandlerServer registers the http handlers for service ArkService to "mux". // RegisterArkServiceHandlerServer registers the http handlers for service ArkService to "mux".
// UnaryRPC :call ArkServiceServer directly. // UnaryRPC :call ArkServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@@ -490,31 +464,6 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
}) })
mux.Handle("POST", pattern_ArkService_Faucet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/Faucet", runtime.WithHTTPPathPattern("/v1/faucet/{address}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_ArkService_Faucet_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_Faucet_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_ArkService_ListVtxos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("GET", pattern_ArkService_ListVtxos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
@@ -565,6 +514,31 @@ func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux,
}) })
mux.Handle("POST", pattern_ArkService_Onboard_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ark.v1.ArkService/Onboard", runtime.WithHTTPPathPattern("/v1/onboard"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_ArkService_Onboard_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_Onboard_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil return nil
} }
@@ -738,28 +712,6 @@ func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux,
}) })
mux.Handle("POST", pattern_ArkService_Faucet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/Faucet", runtime.WithHTTPPathPattern("/v1/faucet/{address}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_ArkService_Faucet_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_Faucet_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_ArkService_ListVtxos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("GET", pattern_ArkService_ListVtxos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
@@ -804,6 +756,28 @@ func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux,
}) })
mux.Handle("POST", pattern_ArkService_Onboard_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ark.v1.ArkService/Onboard", runtime.WithHTTPPathPattern("/v1/onboard"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_ArkService_Onboard_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_ArkService_Onboard_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil return nil
} }
@@ -820,11 +794,11 @@ var (
pattern_ArkService_Ping_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "ping", "payment_id"}, "")) pattern_ArkService_Ping_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "ping", "payment_id"}, ""))
pattern_ArkService_Faucet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "faucet", "address"}, ""))
pattern_ArkService_ListVtxos_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "vtxos", "address"}, "")) pattern_ArkService_ListVtxos_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "vtxos", "address"}, ""))
pattern_ArkService_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "info"}, "")) pattern_ArkService_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "info"}, ""))
pattern_ArkService_Onboard_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "onboard"}, ""))
) )
var ( var (
@@ -840,9 +814,9 @@ var (
forward_ArkService_Ping_0 = runtime.ForwardResponseMessage forward_ArkService_Ping_0 = runtime.ForwardResponseMessage
forward_ArkService_Faucet_0 = runtime.ForwardResponseMessage
forward_ArkService_ListVtxos_0 = runtime.ForwardResponseMessage forward_ArkService_ListVtxos_0 = runtime.ForwardResponseMessage
forward_ArkService_GetInfo_0 = runtime.ForwardResponseMessage forward_ArkService_GetInfo_0 = runtime.ForwardResponseMessage
forward_ArkService_Onboard_0 = runtime.ForwardResponseMessage
) )

View File

@@ -24,9 +24,9 @@ type ArkServiceClient interface {
GetRound(ctx context.Context, in *GetRoundRequest, opts ...grpc.CallOption) (*GetRoundResponse, error) GetRound(ctx context.Context, in *GetRoundRequest, opts ...grpc.CallOption) (*GetRoundResponse, error)
GetEventStream(ctx context.Context, in *GetEventStreamRequest, opts ...grpc.CallOption) (ArkService_GetEventStreamClient, error) GetEventStream(ctx context.Context, in *GetEventStreamRequest, opts ...grpc.CallOption) (ArkService_GetEventStreamClient, error)
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error)
Faucet(ctx context.Context, in *FaucetRequest, opts ...grpc.CallOption) (*FaucetResponse, error)
ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error) ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error)
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error) GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error)
Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error)
} }
type arkServiceClient struct { type arkServiceClient struct {
@@ -114,15 +114,6 @@ func (c *arkServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...gr
return out, nil return out, nil
} }
func (c *arkServiceClient) Faucet(ctx context.Context, in *FaucetRequest, opts ...grpc.CallOption) (*FaucetResponse, error) {
out := new(FaucetResponse)
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/Faucet", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *arkServiceClient) ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error) { func (c *arkServiceClient) ListVtxos(ctx context.Context, in *ListVtxosRequest, opts ...grpc.CallOption) (*ListVtxosResponse, error) {
out := new(ListVtxosResponse) out := new(ListVtxosResponse)
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/ListVtxos", in, out, opts...) err := c.cc.Invoke(ctx, "/ark.v1.ArkService/ListVtxos", in, out, opts...)
@@ -141,6 +132,15 @@ func (c *arkServiceClient) GetInfo(ctx context.Context, in *GetInfoRequest, opts
return out, nil return out, nil
} }
func (c *arkServiceClient) Onboard(ctx context.Context, in *OnboardRequest, opts ...grpc.CallOption) (*OnboardResponse, error) {
out := new(OnboardResponse)
err := c.cc.Invoke(ctx, "/ark.v1.ArkService/Onboard", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ArkServiceServer is the server API for ArkService service. // ArkServiceServer is the server API for ArkService service.
// All implementations should embed UnimplementedArkServiceServer // All implementations should embed UnimplementedArkServiceServer
// for forward compatibility // for forward compatibility
@@ -151,9 +151,9 @@ type ArkServiceServer interface {
GetRound(context.Context, *GetRoundRequest) (*GetRoundResponse, error) GetRound(context.Context, *GetRoundRequest) (*GetRoundResponse, error)
GetEventStream(*GetEventStreamRequest, ArkService_GetEventStreamServer) error GetEventStream(*GetEventStreamRequest, ArkService_GetEventStreamServer) error
Ping(context.Context, *PingRequest) (*PingResponse, error) Ping(context.Context, *PingRequest) (*PingResponse, error)
Faucet(context.Context, *FaucetRequest) (*FaucetResponse, error)
ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error) ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error)
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error)
} }
// UnimplementedArkServiceServer should be embedded to have forward compatible implementations. // UnimplementedArkServiceServer should be embedded to have forward compatible implementations.
@@ -178,15 +178,15 @@ func (UnimplementedArkServiceServer) GetEventStream(*GetEventStreamRequest, ArkS
func (UnimplementedArkServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { func (UnimplementedArkServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
} }
func (UnimplementedArkServiceServer) Faucet(context.Context, *FaucetRequest) (*FaucetResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Faucet not implemented")
}
func (UnimplementedArkServiceServer) ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error) { func (UnimplementedArkServiceServer) ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListVtxos not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListVtxos not implemented")
} }
func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) { func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented")
} }
func (UnimplementedArkServiceServer) Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Onboard not implemented")
}
// UnsafeArkServiceServer may be embedded to opt out of forward compatibility for this service. // UnsafeArkServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ArkServiceServer will // Use of this interface is not recommended, as added methods to ArkServiceServer will
@@ -310,24 +310,6 @@ func _ArkService_Ping_Handler(srv interface{}, ctx context.Context, dec func(int
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _ArkService_Faucet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FaucetRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ArkServiceServer).Faucet(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ark.v1.ArkService/Faucet",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ArkServiceServer).Faucet(ctx, req.(*FaucetRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ArkService_ListVtxos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _ArkService_ListVtxos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListVtxosRequest) in := new(ListVtxosRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -364,6 +346,24 @@ func _ArkService_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _ArkService_Onboard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OnboardRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ArkServiceServer).Onboard(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ark.v1.ArkService/Onboard",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ArkServiceServer).Onboard(ctx, req.(*OnboardRequest))
}
return interceptor(ctx, in, info, handler)
}
// ArkService_ServiceDesc is the grpc.ServiceDesc for ArkService service. // ArkService_ServiceDesc is the grpc.ServiceDesc for ArkService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -391,10 +391,6 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Ping", MethodName: "Ping",
Handler: _ArkService_Ping_Handler, Handler: _ArkService_Ping_Handler,
}, },
{
MethodName: "Faucet",
Handler: _ArkService_Faucet_Handler,
},
{ {
MethodName: "ListVtxos", MethodName: "ListVtxos",
Handler: _ArkService_ListVtxos_Handler, Handler: _ArkService_ListVtxos_Handler,
@@ -403,6 +399,10 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetInfo", MethodName: "GetInfo",
Handler: _ArkService_GetInfo_Handler, Handler: _ArkService_GetInfo_Handler,
}, },
{
MethodName: "Onboard",
Handler: _ArkService_Onboard_Handler,
},
}, },
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
{ {

View File

@@ -11,7 +11,6 @@ import (
oceanwallet "github.com/ark-network/ark/internal/infrastructure/ocean-wallet" oceanwallet "github.com/ark-network/ark/internal/infrastructure/ocean-wallet"
scheduler "github.com/ark-network/ark/internal/infrastructure/scheduler/gocron" scheduler "github.com/ark-network/ark/internal/infrastructure/scheduler/gocron"
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant" txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant"
txbuilderdummy "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
) )
@@ -24,7 +23,6 @@ var (
"gocron": {}, "gocron": {},
} }
supportedTxBuilders = supportedType{ supportedTxBuilders = supportedType{
"dummy": {},
"covenant": {}, "covenant": {},
} }
supportedScanners = supportedType{ supportedScanners = supportedType{
@@ -167,8 +165,6 @@ func (c *Config) txBuilderService() error {
net := c.mainChain() net := c.mainChain()
switch c.TxBuilderType { switch c.TxBuilderType {
case "dummy":
svc = txbuilderdummy.NewTxBuilder(c.wallet, net)
case "covenant": case "covenant":
svc = txbuilder.NewTxBuilder(c.wallet, net, c.RoundLifetime, c.ExitDelay) svc = txbuilder.NewTxBuilder(c.wallet, net, c.RoundLifetime, c.ExitDelay)
default: default:

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/internal/core/domain" "github.com/ark-network/ark/internal/core/domain"
"github.com/ark-network/ark/internal/core/ports" "github.com/ark-network/ark/internal/core/ports"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -20,10 +21,6 @@ import (
var ( var (
paymentsThreshold = int64(128) paymentsThreshold = int64(128)
dustAmount = uint64(450) dustAmount = uint64(450)
faucetVtxo = domain.VtxoKey{
Txid: "0000000000000000000000000000000000000000000000000000000000000000",
VOut: 0,
}
) )
type Service interface { type Service interface {
@@ -32,12 +29,12 @@ type Service interface {
SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error)
ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error
SignVtxos(ctx context.Context, forfeitTxs []string) error SignVtxos(ctx context.Context, forfeitTxs []string) error
FaucetVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) error
GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error) GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error)
GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent
UpdatePaymentStatus(ctx context.Context, id string) error UpdatePaymentStatus(ctx context.Context, id string) error
ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, error) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, error)
GetInfo(ctx context.Context) (string, int64, int64, error) GetInfo(ctx context.Context) (string, int64, int64, error)
Onboard(ctx context.Context, boardingTx string, congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey) error
} }
type service struct { type service struct {
@@ -88,8 +85,9 @@ func NewService(
} }
repoManager.RegisterEventsHandler( repoManager.RegisterEventsHandler(
func(round *domain.Round) { func(round *domain.Round) {
svc.updateProjectionStore(round) go svc.updateVtxoSet(round)
svc.propagateEvents(round) go svc.propagateEvents(round)
go svc.scheduleSweepVtxosForRound(round)
}, },
) )
@@ -165,43 +163,6 @@ func (s *service) UpdatePaymentStatus(_ context.Context, id string) error {
return s.paymentRequests.updatePingTimestamp(id) return s.paymentRequests.updatePingTimestamp(id)
} }
func (s *service) FaucetVtxos(ctx context.Context, userPubkey *secp256k1.PublicKey) error {
pubkey := hex.EncodeToString(userPubkey.SerializeCompressed())
payment, err := domain.NewPayment([]domain.Vtxo{
{
VtxoKey: faucetVtxo,
Receiver: domain.Receiver{
Pubkey: pubkey,
Amount: 10000,
},
},
})
if err != nil {
return err
}
if err := payment.AddReceivers([]domain.Receiver{
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
{Pubkey: pubkey, Amount: 1000},
}); err != nil {
return err
}
if err := s.paymentRequests.push(*payment); err != nil {
return err
}
return s.paymentRequests.updatePingTimestamp(payment.Id)
}
func (s *service) SignVtxos(ctx context.Context, forfeitTxs []string) error { func (s *service) SignVtxos(ctx context.Context, forfeitTxs []string) error {
return s.forfeitTxs.sign(forfeitTxs) return s.forfeitTxs.sign(forfeitTxs)
} }
@@ -223,6 +184,40 @@ func (s *service) GetInfo(ctx context.Context) (string, int64, int64, error) {
return hex.EncodeToString(s.pubkey.SerializeCompressed()), s.roundLifetime, s.exitDelay, nil return hex.EncodeToString(s.pubkey.SerializeCompressed()), s.roundLifetime, s.exitDelay, nil
} }
func (s *service) Onboard(
ctx context.Context, boardingTx string,
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
) error {
if err := tree.ValidateCongestionTree(
congestionTree, boardingTx, s.pubkey, s.roundLifetime,
); err != nil {
return err
}
ptx, err := psetv2.NewPsetFromBase64(boardingTx)
if err != nil {
return fmt.Errorf("failed to parse boarding tx: %s", err)
}
utx, _ := ptx.UnsignedTx()
txid := utx.TxHash().String()
isConfirmed, _, err := s.wallet.IsTransactionConfirmed(ctx, txid)
if err != nil {
return fmt.Errorf("failed to fetch confirmation info for boaridng tx: %s", err)
}
if !isConfirmed {
return fmt.Errorf("boarding tx not confirmed yet, please retry later")
}
pubkey := hex.EncodeToString(userPubkey.SerializeCompressed())
payments := getPaymentsFromOnboarding(congestionTree, pubkey)
round := domain.NewFinalizedRound(
dustAmount, pubkey, txid, boardingTx, congestionTree, payments,
)
return s.saveEvents(ctx, round.Id, round.Events())
}
func (s *service) start() { func (s *service) start() {
s.startRound() s.startRound()
} }
@@ -365,15 +360,6 @@ func (s *service) finalizeRound() {
return return
} }
now := time.Now().Unix()
expirationTimestamp := now + s.roundLifetime + 30 // add 30 secs to be sure that the tx is confirmed
if err := s.sweeper.schedule(expirationTimestamp, txid, round.CongestionTree); err != nil {
changes = round.Fail(fmt.Errorf("failed to schedule sweep tx: %s", err))
log.WithError(err).Warn("failed to schedule sweep tx")
return
}
changes, _ = round.EndFinalization(forfeitTxs, txid) changes, _ = round.EndFinalization(forfeitTxs, txid)
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid) log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
@@ -403,49 +389,50 @@ func (s *service) listenToRedemptions() {
} }
} }
func (s *service) updateProjectionStore(round *domain.Round) { func (s *service) updateVtxoSet(round *domain.Round) {
ctx := context.Background()
lastChange := round.Events()[len(round.Events())-1]
// Update the vtxo set only after a round is finalized. // Update the vtxo set only after a round is finalized.
if _, ok := lastChange.(domain.RoundFinalized); ok { if !round.IsEnded() {
repo := s.repoManager.Vtxos() return
spentVtxos := getSpentVtxos(round.Payments) }
if len(spentVtxos) > 0 {
for {
if err := repo.SpendVtxos(ctx, spentVtxos); err != nil {
log.WithError(err).Warn("failed to add new vtxos, retrying soon")
time.Sleep(100 * time.Millisecond)
continue
}
log.Debugf("spent %d vtxos", len(spentVtxos))
break
}
}
newVtxos := s.getNewVtxos(round) ctx := context.Background()
repo := s.repoManager.Vtxos()
spentVtxos := getSpentVtxos(round.Payments)
if len(spentVtxos) > 0 {
for { for {
if err := repo.AddVtxos(ctx, newVtxos); err != nil { if err := repo.SpendVtxos(ctx, spentVtxos); err != nil {
log.WithError(err).Warn("failed to add new vtxos, retrying soon") log.WithError(err).Warn("failed to add new vtxos, retrying soon")
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
continue continue
} }
log.Debugf("added %d new vtxos", len(newVtxos)) log.Debugf("spent %d vtxos", len(spentVtxos))
break break
} }
go func() {
for {
if err := s.startWatchingVtxos(newVtxos); err != nil {
log.WithError(err).Warn(
"failed to start watching vtxos, retrying in a moment...",
)
continue
}
log.Debugf("started watching %d vtxos", len(newVtxos))
return
}
}()
} }
newVtxos := s.getNewVtxos(round)
for {
if err := repo.AddVtxos(ctx, newVtxos); err != nil {
log.WithError(err).Warn("failed to add new vtxos, retrying soon")
time.Sleep(100 * time.Millisecond)
continue
}
log.Debugf("added %d new vtxos", len(newVtxos))
break
}
go func() {
for {
if err := s.startWatchingVtxos(newVtxos); err != nil {
log.WithError(err).Warn(
"failed to start watching vtxos, retrying in a moment...",
)
continue
}
log.Debugf("started watching %d vtxos", len(newVtxos))
return
}
}()
} }
func (s *service) propagateEvents(round *domain.Round) { func (s *service) propagateEvents(round *domain.Round) {
@@ -465,6 +452,23 @@ func (s *service) propagateEvents(round *domain.Round) {
} }
} }
func (s *service) scheduleSweepVtxosForRound(round *domain.Round) {
// Schedule the sweeping procedure only for completed round.
if !round.IsEnded() {
return
}
expirationTimestamp := time.Now().Add(
time.Duration(s.roundLifetime+30) * time.Second,
)
if err := s.sweeper.schedule(
expirationTimestamp.Unix(), round.Txid, round.CongestionTree,
); err != nil {
log.WithError(err).Warn("failed to schedule sweep tx")
}
}
func (s *service) getNewVtxos(round *domain.Round) []domain.Vtxo { func (s *service) getNewVtxos(round *domain.Round) []domain.Vtxo {
leaves := round.CongestionTree.Leaves() leaves := round.CongestionTree.Leaves()
vtxos := make([]domain.Vtxo, 0) vtxos := make([]domain.Vtxo, 0)
@@ -590,11 +594,25 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
vtxos := make([]domain.VtxoKey, 0) vtxos := make([]domain.VtxoKey, 0)
for _, p := range payments { for _, p := range payments {
for _, vtxo := range p.Inputs { for _, vtxo := range p.Inputs {
if vtxo.VtxoKey == faucetVtxo {
continue
}
vtxos = append(vtxos, vtxo.VtxoKey) vtxos = append(vtxos, vtxo.VtxoKey)
} }
} }
return vtxos return vtxos
} }
func getPaymentsFromOnboarding(
congestionTree tree.CongestionTree, userKey string,
) []domain.Payment {
leaves := congestionTree.Leaves()
receivers := make([]domain.Receiver, 0, len(leaves))
for _, node := range leaves {
ptx, _ := psetv2.NewPsetFromBase64(node.Tx)
receiver := domain.Receiver{
Pubkey: userKey,
Amount: ptx.Outputs[0].Value,
}
receivers = append(receivers, receiver)
}
payment := domain.NewPaymentUnsafe(nil, receivers)
return []domain.Payment{*payment}
}

View File

@@ -82,7 +82,7 @@ func (s *sweeper) schedule(
task := s.createTask(roundTxid, congestionTree) task := s.createTask(roundTxid, congestionTree)
fancyTime := time.Unix(expirationTimestamp, 0).Format("2006-01-02 15:04:05") fancyTime := time.Unix(expirationTimestamp, 0).Format("2006-01-02 15:04:05")
log.Debugf("scheduled sweep task at %s", fancyTime) log.Debugf("scheduled sweep for round %s at %s", roundTxid, fancyTime)
if err := s.scheduler.ScheduleTaskOnce(expirationTimestamp, task); err != nil { if err := s.scheduler.ScheduleTaskOnce(expirationTimestamp, task); err != nil {
return err return err
} }
@@ -290,7 +290,7 @@ func (s *sweeper) findSweepableOutputs(
newNodesToCheck := make([]tree.Node, 0) newNodesToCheck := make([]tree.Node, 0)
for _, node := range nodesToCheck { for _, node := range nodesToCheck {
isPublished, blocktime, err := s.wallet.IsTransactionPublished(ctx, node.Txid) isConfirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, node.Txid)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -298,10 +298,10 @@ func (s *sweeper) findSweepableOutputs(
var expirationTime int64 var expirationTime int64
var sweepInputs []ports.SweepInput var sweepInputs []ports.SweepInput
if !isPublished { if !isConfirmed {
if _, ok := blocktimeCache[node.ParentTxid]; !ok { if _, ok := blocktimeCache[node.ParentTxid]; !ok {
isPublished, blocktime, err := s.wallet.IsTransactionPublished(ctx, node.ParentTxid) isConfirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, node.ParentTxid)
if !isPublished || err != nil { if !isConfirmed || err != nil {
return nil, fmt.Errorf("tx %s not found", node.Txid) return nil, fmt.Errorf("tx %s not found", node.Txid)
} }

View File

@@ -1,8 +1,6 @@
package application package application
import ( import (
"bytes"
"encoding/hex"
"fmt" "fmt"
"sort" "sort"
"sync" "sync"
@@ -154,23 +152,13 @@ func (m *forfeitTxsMap) push(txs []string) {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
faucetTxID, _ := hex.DecodeString(faucetVtxo.Txid)
for _, tx := range txs { for _, tx := range txs {
ptx, _ := psetv2.NewPsetFromBase64(tx) ptx, _ := psetv2.NewPsetFromBase64(tx)
utx, _ := ptx.UnsignedTx() utx, _ := ptx.UnsignedTx()
txid := utx.TxHash().String()
signed := false signed := false
// find the faucet vtxos, and mark them as signed m.forfeitTxs[txid] = &signedTx{tx, signed}
for _, input := range ptx.Inputs {
if bytes.Equal(input.PreviousTxid, faucetTxID) {
signed = true
break
}
}
m.forfeitTxs[utx.TxHash().String()] = &signedTx{tx, signed}
} }
} }

View File

@@ -28,6 +28,14 @@ func NewPayment(inputs []Vtxo) (*Payment, error) {
return p, nil return p, nil
} }
func NewPaymentUnsafe(inputs []Vtxo, receivers []Receiver) *Payment {
return &Payment{
Id: uuid.New().String(),
Inputs: inputs,
Receivers: receivers,
}
}
func (p *Payment) AddReceivers(receivers []Receiver) (err error) { func (p *Payment) AddReceivers(receivers []Receiver) (err error) {
if p.Receivers == nil { if p.Receivers == nil {
p.Receivers = make([]Receiver, 0) p.Receivers = make([]Receiver, 0)

View File

@@ -59,6 +59,39 @@ func NewRound(dustAmount uint64) *Round {
} }
} }
func NewFinalizedRound(
dustAmount uint64, userKey, poolTxid, poolTx string,
congestionTree tree.CongestionTree, payments []Payment,
) *Round {
r := NewRound(dustAmount)
events := []RoundEvent{
RoundStarted{
Id: r.Id,
Timestamp: time.Now().Unix(),
},
PaymentsRegistered{
Id: r.Id,
Payments: payments,
},
RoundFinalizationStarted{
Id: r.Id,
CongestionTree: congestionTree,
PoolTx: poolTx,
},
RoundFinalized{
Id: r.Id,
Txid: poolTxid,
Timestamp: time.Now().Unix(),
},
}
for _, event := range events {
r.raise(event)
}
return r
}
func NewRoundFromEvents(events []RoundEvent) *Round { func NewRoundFromEvents(events []RoundEvent) *Round {
r := &Round{} r := &Round{}

View File

@@ -17,7 +17,7 @@ type WalletService interface {
SelectUtxos(ctx context.Context, asset string, amount uint64) ([]TxInput, uint64, error) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]TxInput, uint64, error)
BroadcastTransaction(ctx context.Context, txHex string) (string, error) BroadcastTransaction(ctx context.Context, txHex string) (string, error)
SignPsetWithKey(ctx context.Context, pset string, inputIndexes []int) (string, error) // inputIndexes == nil means sign all inputs SignPsetWithKey(ctx context.Context, pset string, inputIndexes []int) (string, error) // inputIndexes == nil means sign all inputs
IsTransactionPublished(ctx context.Context, txid string) (isPublished bool, blocktime int64, err error) IsTransactionConfirmed(ctx context.Context, txid string) (isConfirmed bool, blocktime int64, err error)
EstimateFees(ctx context.Context, pset string) (uint64, error) EstimateFees(ctx context.Context, pset string) (uint64, error)
Close() Close()
} }

View File

@@ -117,7 +117,7 @@ func (s *service) BroadcastTransaction(
return res.GetTxid(), nil return res.GetTxid(), nil
} }
func (s *service) IsTransactionPublished( func (s *service) IsTransactionConfirmed(
ctx context.Context, txid string, ctx context.Context, txid string,
) (bool, int64, error) { ) (bool, int64, error) {
_, blocktime, err := s.GetTransaction(ctx, txid) _, blocktime, err := s.GetTransaction(ctx, txid)

View File

@@ -113,8 +113,8 @@ func (b *txBuilder) BuildPoolTx(
// generated in the process and takes the shared utxo outpoint as argument. // generated in the process and takes the shared utxo outpoint as argument.
// This is safe as the memory allocated for `craftCongestionTree` is freed // This is safe as the memory allocated for `craftCongestionTree` is freed
// only after `BuildPoolTx` returns. // only after `BuildPoolTx` returns.
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := craftCongestionTree( treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
b.net.AssetID, aspPubkey, payments, minRelayFee, b.roundLifetime, b.exitDelay, b.net.AssetID, aspPubkey, getOffchainReceivers(payments), minRelayFee, b.roundLifetime, b.exitDelay,
) )
if err != nil { if err != nil {
return return

View File

@@ -97,7 +97,7 @@ func (m *mockedWallet) EstimateFees(ctx context.Context, pset string) (uint64, e
return res, args.Error(1) return res, args.Error(1)
} }
func (m *mockedWallet) IsTransactionPublished(ctx context.Context, txid string) (bool, int64, error) { func (m *mockedWallet) IsTransactionConfirmed(ctx context.Context, txid string) (bool, int64, error) {
args := m.Called(ctx, txid) args := m.Called(ctx, txid)
var res bool var res bool

View File

@@ -4,6 +4,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/internal/core/domain" "github.com/ark-network/ark/internal/core/domain"
"github.com/ark-network/ark/internal/core/ports" "github.com/ark-network/ark/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -14,7 +15,6 @@ import (
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
@@ -62,12 +62,15 @@ func getOnchainReceivers(
func getOffchainReceivers( func getOffchainReceivers(
payments []domain.Payment, payments []domain.Payment,
) []domain.Receiver { ) []tree.Receiver {
receivers := make([]domain.Receiver, 0) receivers := make([]tree.Receiver, 0)
for _, payment := range payments { for _, payment := range payments {
for _, receiver := range payment.Receivers { for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() { if !receiver.IsOnchain() {
receivers = append(receivers, receiver) receivers = append(receivers, tree.Receiver{
Pubkey: receiver.Pubkey,
Amount: receiver.Amount,
})
} }
} }
} }
@@ -133,35 +136,6 @@ func addInputs(
return nil return nil
} }
// wrapper of updater methods adding a taproot input to the pset with all the necessary data to spend it via any taproot script
func addTaprootInput(
updater *psetv2.Updater,
input psetv2.InputArgs,
internalTaprootKey *secp256k1.PublicKey,
taprootTree *taproot.IndexedElementsTapScriptTree,
) error {
if err := updater.AddInputs([]psetv2.InputArgs{input}); err != nil {
return err
}
if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil {
return err
}
for _, proof := range taprootTree.LeafMerkleProofs {
controlBlock := proof.ToControlBlock(internalTaprootKey)
if err := updater.AddInTapLeafScript(0, psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(proof.Script),
ControlBlock: controlBlock,
}); err != nil {
return err
}
}
return nil
}
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) { func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script() return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
} }

View File

@@ -1,285 +0,0 @@
package txbuilder
import (
"context"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/internal/core/domain"
"github.com/ark-network/ark/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
const (
connectorAmount = 450
sevenDays = 7 * 24 * 60 * 60
)
type txBuilder struct {
wallet ports.WalletService
net network.Network
}
func NewTxBuilder(
wallet ports.WalletService, net network.Network,
) ports.TxBuilder {
return &txBuilder{wallet, net}
}
// BuildSweepTx implements ports.TxBuilder.
func (*txBuilder) BuildSweepTx(wallet ports.WalletService, inputs []ports.SweepInput) (signedSweepTx string, err error) {
panic("unimplemented")
}
// BuildForfeitTxs implements ports.TxBuilder.
func (b *txBuilder) BuildForfeitTxs(
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
) (connectors []string, forfeitTxs []string, err error) {
poolTxID, err := getTxid(poolTx)
if err != nil {
return nil, nil, err
}
aspScript, err := p2wpkhScript(aspPubkey, b.net)
if err != nil {
return nil, nil, err
}
numberOfConnectors := countSpentVtxos(payments)
connectors, err = createConnectors(
poolTxID,
1,
psetv2.OutputArgs{
Asset: b.net.AssetID,
Amount: connectorAmount,
Script: aspScript,
},
aspScript,
numberOfConnectors,
)
if err != nil {
return nil, nil, err
}
connectorsAsInputs, err := connectorsToInputArgs(connectors)
if err != nil {
return nil, nil, err
}
forfeitTxs = make([]string, 0)
for _, payment := range payments {
for _, vtxo := range payment.Inputs {
for _, connector := range connectorsAsInputs {
forfeitTx, err := createForfeitTx(
connector,
psetv2.InputArgs{
Txid: vtxo.Txid,
TxIndex: vtxo.VOut,
},
vtxo.Amount,
aspScript,
b.net,
)
if err != nil {
return nil, nil, err
}
forfeitTxs = append(forfeitTxs, forfeitTx)
}
}
}
return connectors, forfeitTxs, nil
}
// BuildPoolTx implements ports.TxBuilder.
func (b *txBuilder) BuildPoolTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64,
) (poolTx string, congestionTree tree.CongestionTree, err error) {
aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net)
if err != nil {
return "", nil, err
}
offchainReceivers, onchainReceivers := receiversFromPayments(payments)
sharedOutputAmount := sumReceivers(offchainReceivers)
numberOfConnectors := countSpentVtxos(payments)
connectorOutputAmount := connectorAmount * numberOfConnectors
ctx := context.Background()
outputs := []psetv2.OutputArgs{
{
Asset: b.net.AssetID,
Amount: sharedOutputAmount,
Script: aspScriptBytes,
},
{
Asset: b.net.AssetID,
Amount: connectorOutputAmount,
Script: aspScriptBytes,
},
}
amountToSelect := sharedOutputAmount + connectorOutputAmount
for _, receiver := range onchainReceivers {
amountToSelect += receiver.Amount
receiverScript, err := address.ToOutputScript(receiver.OnchainAddress)
if err != nil {
return "", nil, err
}
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.net.AssetID,
Amount: receiver.Amount,
Script: receiverScript,
})
}
utxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, amountToSelect)
if err != nil {
return
}
if change > 0 {
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.net.AssetID,
Amount: change,
Script: aspScriptBytes,
})
}
ptx, err := psetv2.New(toInputArgs(utxos), outputs, nil)
if err != nil {
return
}
utx, err := ptx.UnsignedTx()
if err != nil {
return
}
congestionTree, err = buildCongestionTree(
newOutputScriptFactory(aspPubkey, b.net),
b.net,
utx.TxHash().String(),
offchainReceivers,
)
if err != nil {
return
}
poolTx, err = ptx.ToBase64()
if err != nil {
return
}
return poolTx, congestionTree, err
}
func (b *txBuilder) GetVtxoScript(userPubkey, _ *secp256k1.PublicKey) ([]byte, error) {
p2wpkh := payment.FromPublicKey(userPubkey, &b.net, nil)
addr, _ := p2wpkh.WitnessPubKeyHash()
return address.ToOutputScript(addr)
}
func (b *txBuilder) GetLeafSweepClosure(
node tree.Node, userPubKey *secp256k1.PublicKey,
) (*psetv2.TapLeafScript, int64, error) {
panic("unimplemented")
}
func connectorsToInputArgs(connectors []string) ([]psetv2.InputArgs, error) {
inputs := make([]psetv2.InputArgs, 0, len(connectors)+1)
for i, psetb64 := range connectors {
tx, err := psetv2.NewPsetFromBase64(psetb64)
if err != nil {
return nil, err
}
utx, err := tx.UnsignedTx()
if err != nil {
return nil, err
}
txid := utx.TxHash().String()
for j := range tx.Outputs {
inputs = append(inputs, psetv2.InputArgs{
Txid: txid,
TxIndex: uint32(j),
})
if i != len(connectors)-1 {
break
}
}
}
return inputs, nil
}
func getTxid(txStr string) (string, error) {
pset, err := psetv2.NewPsetFromBase64(txStr)
if err != nil {
tx, err := transaction.NewTxFromHex(txStr)
if err != nil {
return "", err
}
return tx.TxHash().String(), nil
}
utx, err := pset.UnsignedTx()
if err != nil {
return "", err
}
return utx.TxHash().String(), nil
}
func countSpentVtxos(payments []domain.Payment) uint64 {
var sum uint64
for _, payment := range payments {
sum += uint64(len(payment.Inputs))
}
return sum
}
func receiversFromPayments(
payments []domain.Payment,
) (offchainReceivers, onchainReceivers []domain.Receiver) {
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if receiver.IsOnchain() {
onchainReceivers = append(onchainReceivers, receiver)
} else {
offchainReceivers = append(offchainReceivers, receiver)
}
}
}
return
}
func sumReceivers(receivers []domain.Receiver) uint64 {
var sum uint64
for _, r := range receivers {
sum += r.Amount
}
return sum
}
func toInputArgs(
ins []ports.TxInput,
) []psetv2.InputArgs {
inputs := make([]psetv2.InputArgs, 0, len(ins))
for _, in := range ins {
inputs = append(inputs, psetv2.InputArgs{
Txid: in.GetTxid(),
TxIndex: in.GetIndex(),
})
}
return inputs
}

View File

@@ -1,403 +0,0 @@
package txbuilder_test
import (
"context"
"crypto/rand"
"encoding/hex"
"testing"
"github.com/ark-network/ark/internal/core/domain"
"github.com/ark-network/ark/internal/core/ports"
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
"github.com/btcsuite/btcd/chaincfg/chainhash"
secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/require"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/psetv2"
)
const (
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
fakePoolTx = "cHNldP8BAgQCAAAAAQQBAQEFAQMBBgEDAfsEAgAAAAABDiDk7dXxh4KQzgLO8i1ABtaLCe4aPL12GVhN1E9zM1ePLwEPBAAAAAABEAT/////AAEDCOgDAAAAAAAAAQQWABSNnpy01UJqd99eTg2M1IpdKId11gf8BHBzZXQCICWyUQcOKcoZBDzzPM1zJOLdqwPsxK4LXnfE/A5c9slaB/wEcHNldAgEAAAAAAABAwh4BQAAAAAAAAEEFgAUjZ6ctNVCanffXk4NjNSKXSiHddYH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAAAQMI9AEAAAAAAAABBAAH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAA"
)
type input struct {
txid string
vout uint32
}
func (i *input) GetTxid() string {
return i.txid
}
func (i *input) GetIndex() uint32 {
return i.vout
}
func (i *input) GetScript() string {
return "a914ea9f486e82efb3dd83a69fd96e3f0113757da03c87"
}
func (i *input) GetAsset() string {
return "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
}
func (i *input) GetValue() uint64 {
return 1000
}
type mockedWalletService struct{}
// BroadcastTransaction implements ports.WalletService.
func (*mockedWalletService) BroadcastTransaction(ctx context.Context, txHex string) (string, error) {
panic("unimplemented")
}
// Close implements ports.WalletService.
func (*mockedWalletService) Close() {
panic("unimplemented")
}
// DeriveAddresses implements ports.WalletService.
func (*mockedWalletService) DeriveAddresses(ctx context.Context, num int) ([]string, error) {
panic("unimplemented")
}
// GetPubkey implements ports.WalletService.
func (*mockedWalletService) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) {
panic("unimplemented")
}
// SignPset implements ports.WalletService.
func (*mockedWalletService) SignPset(ctx context.Context, pset string, extractRawTx bool) (string, error) {
panic("unimplemented")
}
// Status implements ports.WalletService.
func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, error) {
panic("unimplemented")
}
func (*mockedWalletService) WatchScripts(ctx context.Context, scripts []string) error {
panic("unimplemented")
}
func (*mockedWalletService) UnwatchScripts(ctx context.Context, scripts []string) error {
panic("unimplemented")
}
func (*mockedWalletService) GetNotificationChannel(ctx context.Context) chan []domain.VtxoKey {
panic("unimplemented")
}
func (*mockedWalletService) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) {
// random txid
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return nil, 0, err
}
fakeInput := input{
txid: hex.EncodeToString(bytes),
vout: 0,
}
return []ports.TxInput{&fakeInput}, 0, nil
}
func (*mockedWalletService) EstimateFees(ctx context.Context, pset string) (uint64, error) {
return 100, nil
}
func (*mockedWalletService) SignPsetWithKey(ctx context.Context, pset string, inputIndex []int) (string, error) {
panic("unimplemented")
}
func (*mockedWalletService) IsTransactionPublished(ctx context.Context, txid string) (bool, int64, error) {
panic("unimplemented")
}
func TestBuildCongestionTree(t *testing.T) {
builder := txbuilder.NewTxBuilder(&mockedWalletService{}, network.Liquid)
fixtures := []struct {
payments []domain.Payment
expectedNodesNum int // 2*len(receivers)-1
expectedLeavesNum int
}{
{
payments: []domain.Payment{
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
},
expectedNodesNum: 3,
expectedLeavesNum: 2,
},
{
payments: []domain.Payment{
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
},
expectedNodesNum: 11,
expectedLeavesNum: 6,
},
}
pubkeyBytes, _ := hex.DecodeString(testingKey)
pubkey, _ := secp256k1.ParsePubKey(pubkeyBytes)
for _, f := range fixtures {
poolTx, tree, err := builder.BuildPoolTx(pubkey, f.payments, 30)
require.NoError(t, err)
require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes())
require.Len(t, tree.Leaves(), f.expectedLeavesNum)
poolPset, err := psetv2.NewPsetFromBase64(poolTx)
require.NoError(t, err)
poolTxUnsigned, err := poolPset.UnsignedTx()
require.NoError(t, err)
poolTxID := poolTxUnsigned.TxHash().String()
// check the root
require.Len(t, tree[0], 1)
require.Equal(t, poolTxID, tree[0][0].ParentTxid)
// check the leaves
for _, leaf := range tree.Leaves() {
pset, err := psetv2.NewPsetFromBase64(leaf.Tx)
require.NoError(t, err)
require.Len(t, pset.Inputs, 1)
require.Len(t, pset.Outputs, 1)
inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
require.Equal(t, leaf.ParentTxid, inputTxID)
}
// check the nodes
for _, level := range tree[:len(tree)-2] {
for _, node := range level {
pset, err := psetv2.NewPsetFromBase64(node.Tx)
require.NoError(t, err)
require.Len(t, pset.Inputs, 1)
require.Len(t, pset.Outputs, 2)
inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
require.Equal(t, node.ParentTxid, inputTxID)
children := tree.Children(node.Txid)
require.Len(t, children, 2)
}
}
}
}
func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder(&mockedWalletService{}, network.Liquid)
poolPset, err := psetv2.NewPsetFromBase64(fakePoolTx)
require.NoError(t, err)
poolTxUnsigned, err := poolPset.UnsignedTx()
require.NoError(t, err)
poolTxID := poolTxUnsigned.TxHash().String()
fixtures := []struct {
payments []domain.Payment
expectedNumOfForfeitTxs int
expectedNumOfConnectors int
}{
{
payments: []domain.Payment{
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 1,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
},
expectedNumOfForfeitTxs: 4,
expectedNumOfConnectors: 1,
},
}
pubkeyBytes, _ := hex.DecodeString(testingKey)
pubkey, _ := secp256k1.ParsePubKey(pubkeyBytes)
for _, f := range fixtures {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
pubkey, fakePoolTx, f.payments,
)
require.NoError(t, err)
require.Len(t, connectors, f.expectedNumOfConnectors)
require.Len(t, forfeitTxs, f.expectedNumOfForfeitTxs)
// decode and check connectors
connectorsPsets := make([]*psetv2.Pset, 0, f.expectedNumOfConnectors)
for _, pset := range connectors {
p, err := psetv2.NewPsetFromBase64(pset)
require.NoError(t, err)
connectorsPsets = append(connectorsPsets, p)
}
for i, pset := range connectorsPsets {
require.Len(t, pset.Inputs, 1)
require.Len(t, pset.Outputs, 2)
expectedInputTxid := poolTxID
expectedInputVout := uint32(1)
if i > 0 {
tx, err := connectorsPsets[i-1].UnsignedTx()
require.NoError(t, err)
require.NotNil(t, tx)
expectedInputTxid = tx.TxHash().String()
}
inputTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
require.Equal(t, expectedInputTxid, inputTxid)
require.Equal(t, expectedInputVout, pset.Inputs[0].PreviousTxIndex)
}
// decode and check forfeit txs
forfeitTxsPsets := make([]*psetv2.Pset, 0, f.expectedNumOfForfeitTxs)
for _, pset := range forfeitTxs {
p, err := psetv2.NewPsetFromBase64(pset)
require.NoError(t, err)
forfeitTxsPsets = append(forfeitTxsPsets, p)
}
// each forfeit tx should have 2 inputs and 2 outputs
for _, pset := range forfeitTxsPsets {
require.Len(t, pset.Inputs, 2)
require.Len(t, pset.Outputs, 1)
}
}
}

View File

@@ -1,103 +0,0 @@
package txbuilder
import (
"github.com/vulpemventures/go-elements/psetv2"
)
func createConnectors(
poolTxID string,
connectorOutputIndex uint32,
connectorOutput psetv2.OutputArgs,
changeScript []byte,
numberOfConnectors uint64,
) (connectorsPsets []string, err error) {
previousInput := psetv2.InputArgs{
Txid: poolTxID,
TxIndex: connectorOutputIndex,
}
if numberOfConnectors == 1 {
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return nil, err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return nil, err
}
err = updater.AddInputs([]psetv2.InputArgs{previousInput})
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{connectorOutput})
if err != nil {
return nil, err
}
base64, err := pset.ToBase64()
if err != nil {
return nil, err
}
return []string{base64}, nil
}
// compute the initial amount of the connectors output in pool transaction
remainingAmount := connectorAmount * numberOfConnectors
connectorsPset := make([]string, 0, numberOfConnectors-1)
for i := uint64(0); i < numberOfConnectors-1; i++ {
// create a new pset
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return nil, err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return nil, err
}
err = updater.AddInputs([]psetv2.InputArgs{previousInput})
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{connectorOutput})
if err != nil {
return nil, err
}
changeAmount := remainingAmount - connectorOutput.Amount
if changeAmount > 0 {
changeOutput := psetv2.OutputArgs{
Asset: connectorOutput.Asset,
Amount: changeAmount,
Script: changeScript,
}
err = updater.AddOutputs([]psetv2.OutputArgs{changeOutput})
if err != nil {
return nil, err
}
tx, _ := pset.UnsignedTx()
txid := tx.TxHash().String()
// make the change the next previousInput
previousInput = psetv2.InputArgs{
Txid: txid,
TxIndex: 1,
}
}
base64, err := pset.ToBase64()
if err != nil {
return nil, err
}
connectorsPset = append(connectorsPset, base64)
}
return connectorsPset, nil
}

View File

@@ -1,42 +0,0 @@
package txbuilder
import (
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/psetv2"
)
func createForfeitTx(
connectorInput psetv2.InputArgs,
vtxoInput psetv2.InputArgs,
vtxoAmount uint64,
aspScript []byte,
net network.Network,
) (forfeitTx string, err error) {
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return "", err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return "", err
}
err = updater.AddInputs([]psetv2.InputArgs{connectorInput, vtxoInput})
if err != nil {
return "", err
}
err = updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: net.AssetID,
Amount: vtxoAmount,
Script: aspScript,
},
})
if err != nil {
return "", err
}
return pset.ToBase64()
}

View File

@@ -1,309 +0,0 @@
package txbuilder
import (
"encoding/hex"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/internal/core/domain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
)
const (
sharedOutputIndex = 0
)
type outputScriptFactory func(leaves []domain.Receiver) ([]byte, error)
func p2wpkhScript(publicKey *secp256k1.PublicKey, net network.Network) ([]byte, error) {
payment := payment.FromPublicKey(publicKey, &net, nil)
addr, err := payment.WitnessPubKeyHash()
if err != nil {
return nil, err
}
return address.ToOutputScript(addr)
}
// newOtputScriptFactory returns an output script factory func that lock funds using the ASP public key only on all branches psbt. The leaves are instead locked by the leaf public key.
func newOutputScriptFactory(aspPublicKey *secp256k1.PublicKey, net network.Network) outputScriptFactory {
return func(leaves []domain.Receiver) ([]byte, error) {
aspScript, err := p2wpkhScript(aspPublicKey, net)
if err != nil {
return nil, err
}
switch len(leaves) {
case 0:
return nil, nil
case 1: // it's a leaf
buf, err := hex.DecodeString(leaves[0].Pubkey)
if err != nil {
return nil, err
}
key, err := secp256k1.ParsePubKey(buf)
if err != nil {
return nil, err
}
return p2wpkhScript(key, net)
default: // it's a branch, lock funds with ASP public key
return aspScript, nil
}
}
}
// congestionTree builder iteratively creates a binary tree of Pset from a set of receivers
// it also expect createOutputScript func managing the output script creation and the network to use (mainly for L-BTC asset id)
func buildCongestionTree(
createOutputScript outputScriptFactory,
net network.Network,
poolTxID string,
receivers []domain.Receiver,
) (congestionTree tree.CongestionTree, err error) {
var nodes []*node
for _, r := range receivers {
nodes = append(nodes, newLeaf(createOutputScript, net, r))
}
for len(nodes) > 1 {
nodes, err = createTreeLevel(nodes)
if err != nil {
return nil, err
}
}
psets, err := nodes[0].psets(psetv2.InputArgs{
Txid: poolTxID,
TxIndex: sharedOutputIndex,
}, 0)
if err != nil {
return nil, err
}
maxLevel := 0
for _, psetWithLevel := range psets {
if psetWithLevel.level > maxLevel {
maxLevel = psetWithLevel.level
}
}
congestionTree = make(tree.CongestionTree, maxLevel+1)
for _, psetWithLevel := range psets {
utx, err := psetWithLevel.pset.UnsignedTx()
if err != nil {
return nil, err
}
txid := utx.TxHash().String()
psetB64, err := psetWithLevel.pset.ToBase64()
if err != nil {
return nil, err
}
parentTxid := chainhash.Hash(psetWithLevel.pset.Inputs[0].PreviousTxid).String()
congestionTree[psetWithLevel.level] = append(congestionTree[psetWithLevel.level], tree.Node{
Txid: txid,
Tx: psetB64,
ParentTxid: parentTxid,
Leaf: psetWithLevel.leaf,
})
}
return congestionTree, nil
}
func createTreeLevel(nodes []*node) ([]*node, error) {
if len(nodes)%2 != 0 {
last := nodes[len(nodes)-1]
pairs, err := createTreeLevel(nodes[:len(nodes)-1])
if err != nil {
return nil, err
}
return append(pairs, last), nil
}
pairs := make([]*node, 0, len(nodes)/2)
for i := 0; i < len(nodes); i += 2 {
pairs = append(pairs, newBranch(nodes[i], nodes[i+1]))
}
return pairs, nil
}
// internal struct to build a binary tree of Pset
type node struct {
receivers []domain.Receiver
left *node
right *node
createOutputScript outputScriptFactory
network network.Network
}
// create a node from a single receiver
func newLeaf(
createOutputScript outputScriptFactory,
network network.Network,
receiver domain.Receiver,
) *node {
return &node{
receivers: []domain.Receiver{receiver},
createOutputScript: createOutputScript,
network: network,
left: nil,
right: nil,
}
}
// aggregate two nodes into a branch node
func newBranch(
left *node,
right *node,
) *node {
return &node{
receivers: append(left.receivers, right.receivers...),
createOutputScript: left.createOutputScript,
network: left.network,
left: left,
right: right,
}
}
// is it the final node of the tree
func (n *node) isLeaf() bool {
return n.left == nil && n.right == nil
}
// compute the output amount of a node
func (n *node) amount() uint64 {
var amount uint64
for _, r := range n.receivers {
amount += r.Amount
}
return amount
}
// compute the output script of a node
func (n *node) script() ([]byte, error) {
return n.createOutputScript(n.receivers)
}
// use script & amount to create OutputArgs
func (n *node) output() (*psetv2.OutputArgs, error) {
script, err := n.script()
if err != nil {
return nil, err
}
return &psetv2.OutputArgs{
Asset: n.network.AssetID,
Amount: n.amount(),
Script: script,
}, nil
}
// create the node Pset from the previous node Pset represented by input arg
// if node is a branch, it adds two outputs to the Pset, one for the left branch and one for the right branch
// if node is a leaf, it only adds one output to the Pset (the node output)
func (n *node) pset(input psetv2.InputArgs) (*psetv2.Pset, error) {
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return nil, err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return nil, err
}
err = updater.AddInputs([]psetv2.InputArgs{input})
if err != nil {
return nil, err
}
if n.isLeaf() {
output, err := n.output()
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{*output})
if err != nil {
return nil, err
}
return pset, nil
}
outputLeft, err := n.left.output()
if err != nil {
return nil, err
}
outputRight, err := n.right.output()
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{*outputLeft, *outputRight})
if err != nil {
return nil, err
}
return pset, nil
}
type psetWithLevel struct {
pset *psetv2.Pset
level int
leaf bool
}
// create the node pset and all the psets of its children recursively, updating the input arg at each step
// the function stops when it reaches a leaf node
func (n *node) psets(input psetv2.InputArgs, level int) ([]psetWithLevel, error) {
pset, err := n.pset(input)
if err != nil {
return nil, err
}
nodeResult := []psetWithLevel{
{pset, level, n.isLeaf()},
}
if n.isLeaf() {
return nodeResult, nil
}
unsignedTx, err := pset.UnsignedTx()
if err != nil {
return nil, err
}
txID := unsignedTx.TxHash().String()
psetsLeft, err := n.left.psets(psetv2.InputArgs{
Txid: txID,
TxIndex: 0,
}, level+1)
if err != nil {
return nil, err
}
psetsRight, err := n.right.psets(psetv2.InputArgs{
Txid: txID,
TxIndex: 1,
}, level+1)
if err != nil {
return nil, err
}
return append(nodeResult, append(psetsLeft, psetsRight...)...), nil
}

View File

@@ -40,6 +40,37 @@ func NewHandler(service application.Service) arkv1.ArkServiceServer {
return h return h
} }
func (h *handler) Onboard(ctx context.Context, req *arkv1.OnboardRequest) (*arkv1.OnboardResponse, error) {
if req.GetUserPubkey() == "" {
return nil, status.Error(codes.InvalidArgument, "missing user pubkey")
}
pubKey, err := hex.DecodeString(req.GetUserPubkey())
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid user pubkey")
}
decodedPubKey, err := secp256k1.ParsePubKey(pubKey)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid user pubkey")
}
if req.GetBoardingTx() == "" {
return nil, status.Error(codes.InvalidArgument, "missing boarding tx id")
}
tree, err := toCongestionTree(req.GetCongestionTree())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if err := h.svc.Onboard(ctx, req.GetBoardingTx(), tree, decodedPubKey); err != nil {
return nil, err
}
return &arkv1.OnboardResponse{}, nil
}
func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.PingResponse, error) { func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.PingResponse, error) {
if req.GetPaymentId() == "" { if req.GetPaymentId() == "" {
return nil, status.Error(codes.InvalidArgument, "missing payment id") return nil, status.Error(codes.InvalidArgument, "missing payment id")
@@ -96,19 +127,6 @@ func (h *handler) FinalizePayment(ctx context.Context, req *arkv1.FinalizePaymen
return &arkv1.FinalizePaymentResponse{}, nil return &arkv1.FinalizePaymentResponse{}, nil
} }
func (h *handler) Faucet(ctx context.Context, req *arkv1.FaucetRequest) (*arkv1.FaucetResponse, error) {
_, pubkey, _, err := parseAddress(req.GetAddress())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if err := h.svc.FaucetVtxos(ctx, pubkey); err != nil {
return nil, err
}
return &arkv1.FaucetResponse{}, nil
}
func (h *handler) GetRound(ctx context.Context, req *arkv1.GetRoundRequest) (*arkv1.GetRoundResponse, error) { func (h *handler) GetRound(ctx context.Context, req *arkv1.GetRoundRequest) (*arkv1.GetRoundResponse, error) {
if len(req.GetTxid()) <= 0 { if len(req.GetTxid()) <= 0 {
return nil, status.Error(codes.InvalidArgument, "missing pool txid") return nil, status.Error(codes.InvalidArgument, "missing pool txid")
@@ -305,3 +323,32 @@ func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
Levels: levels, Levels: levels,
} }
} }
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
for _, level := range treeFromProto.Levels {
nodes := make([]tree.Node, 0, len(level.Nodes))
for _, node := range level.Nodes {
nodes = append(nodes, tree.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: false,
})
}
levels = append(levels, nodes)
}
for j, treeLvl := range levels {
for i, node := range treeLvl {
if len(levels.Children(node.Txid)) == 0 {
levels[j][i].Leaf = true
}
}
}
return levels, nil
}