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{
Name: "balance",
Usage: "Print balance of the Ark wallet",
Usage: "Shows the onchain and offchain balance of the Ark wallet",
Action: balanceAction,
Flags: []cli.Flag{&expiryDetailsFlag},
}

View File

@@ -15,6 +15,7 @@ import (
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
@@ -596,7 +597,7 @@ func handleRoundStream(
if len(output.Script) == 0 {
continue
}
if bytes.Equal(output.Script[2:], outputTapKey.SerializeCompressed()) {
if bytes.Equal(output.Script[2:], schnorr.SerializePubKey(outputTapKey)) {
if output.Value != receiver.Amount {
continue
}
@@ -733,6 +734,29 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
return levels, nil
}
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
for _, level := range congestionTree {
levelProto := &arkv1.TreeLevel{
Nodes: make([]*arkv1.Node, 0, len(level)),
}
for _, node := range level {
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
})
}
levels = append(levels, levelProto)
}
return &arkv1.Tree{
Levels: levels,
}
}
func decodeReceiverAddress(addr string) (
isOnChainAddress bool,
onchainScript []byte,

View File

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

View File

@@ -8,7 +8,7 @@ import (
var dumpCommand = cli.Command{
Name: "dump-privkey",
Usage: "Dump private key of the Ark wallet",
Usage: "Dumps private key of the Ark wallet",
Action: dumpAction,
}

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{
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,
Flags: []cli.Flag{&passwordFlag, &privateKeyFlag, &networkFlag, &urlFlag},
}

View File

@@ -57,17 +57,17 @@ func main() {
app.Version = version
app.Name = "Ark CLI"
app.Usage = "command line interface for Ark wallet"
app.Usage = "ark wallet command line interface"
app.Commands = append(
app.Commands,
&balanceCommand,
&configCommand,
&dumpCommand,
&faucetCommand,
&initCommand,
&receiveCommand,
&redeemCommand,
&sendCommand,
&onboardCommand,
)
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{
Name: "receive",
Usage: "Print the Ark address associated with your wallet and the connected Ark",
Usage: "Shows both onchain and offchain addresses",
Action: receiveAction,
}

View File

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

View File

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

View File

@@ -1,22 +1,49 @@
package txbuilder
package tree
import (
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/internal/core/domain"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/psetv2"
"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 {
sweepKey *secp256k1.PublicKey
receivers []domain.Receiver
receivers []Receiver
left *node
right *node
asset string
@@ -131,7 +158,7 @@ func (n *node) getWitnessData() (
return n._inputTaprootKey, n._inputTaprootTree, nil
}
sweepClosure := &tree.CSVSigClosure{
sweepClosure := &CSVSigClosure{
Pubkey: n.sweepKey,
Seconds: uint(n.roundLifetime),
}
@@ -147,7 +174,7 @@ func (n *node) getWitnessData() (
return nil, nil, err
}
unrollClosure := &tree.UnrollClosure{
unrollClosure := &UnrollClosure{
LeftKey: taprootKey,
LeftAmount: n.getAmount(),
}
@@ -163,7 +190,7 @@ func (n *node) getWitnessData() (
root := branchTaprootTree.RootNode.TapHash()
inputTapkey := taproot.ComputeTaprootOutputKey(
tree.UnspendableKey(),
UnspendableKey(),
root[:],
)
@@ -186,7 +213,7 @@ func (n *node) getWitnessData() (
leftAmount := n.left.getAmount() + n.feeSats
rightAmount := n.right.getAmount() + n.feeSats
unrollClosure := &tree.UnrollClosure{
unrollClosure := &UnrollClosure{
LeftKey: leftKey,
LeftAmount: leftAmount,
RightKey: rightKey,
@@ -204,7 +231,7 @@ func (n *node) getWitnessData() (
root := branchTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(
tree.UnspendableKey(),
UnspendableKey(),
root[:],
)
@@ -231,7 +258,7 @@ func (n *node) getVtxoWitnessData() (
return nil, nil, err
}
redeemClosure := &tree.CSVSigClosure{
redeemClosure := &CSVSigClosure{
Pubkey: pubkey,
Seconds: uint(n.exitDelay),
}
@@ -241,7 +268,7 @@ func (n *node) getVtxoWitnessData() (
return nil, nil, err
}
forfeitClosure := &tree.ForfeitClosure{
forfeitClosure := &ForfeitClosure{
Pubkey: pubkey,
AspPubkey: n.sweepKey,
}
@@ -257,7 +284,7 @@ func (n *node) getVtxoWitnessData() (
root := leafTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(
tree.UnspendableKey(),
UnspendableKey(),
root[:],
)
@@ -266,25 +293,24 @@ func (n *node) getVtxoWitnessData() (
func (n *node) getTreeNode(
input psetv2.InputArgs, tapTree *taproot.IndexedElementsTapScriptTree,
) (tree.Node, error) {
) (Node, error) {
pset, err := n.getTx(input, tapTree)
if err != nil {
return tree.Node{}, err
return Node{}, err
}
txid, err := getPsetId(pset)
if err != nil {
return tree.Node{}, err
return Node{}, err
}
tx, err := pset.ToBase64()
if err != nil {
return tree.Node{}, err
return Node{}, err
}
parentTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
return tree.Node{
return Node{
Txid: txid,
Tx: tx,
ParentTxid: parentTxid,
@@ -306,7 +332,7 @@ func (n *node) getTx(
}
if err := addTaprootInput(
updater, input, tree.UnspendableKey(), inputTapTree,
updater, input, UnspendableKey(), inputTapTree,
); err != nil {
return nil, err
}
@@ -328,9 +354,9 @@ func (n *node) getTx(
return pset, nil
}
func (n *node) createFinalCongestionTree() treeFactory {
return func(poolTxInput psetv2.InputArgs) (tree.CongestionTree, error) {
congestionTree := make(tree.CongestionTree, 0)
func (n *node) createFinalCongestionTree() TreeFactory {
return func(poolTxInput psetv2.InputArgs) (CongestionTree, error) {
congestionTree := make(CongestionTree, 0)
_, taprootTree, err := n.getWitnessData()
if err != nil {
@@ -346,7 +372,7 @@ func (n *node) createFinalCongestionTree() treeFactory {
nextInputsArgs := make([]psetv2.InputArgs, 0)
nextTaprootTrees := make([]*taproot.IndexedElementsTapScriptTree, 0)
treeLevel := make([]tree.Node, 0)
treeLevel := make([]Node, 0)
for i, node := range nodes {
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(
receivers []domain.Receiver,
receivers []Receiver,
aspPublicKey *secp256k1.PublicKey,
asset string,
feeSatsPerNode uint64,
@@ -431,7 +427,7 @@ func createPartialCongestionTree(
for _, r := range receivers {
leafNode := &node{
sweepKey: aspPublicKey,
receivers: []domain.Receiver{r},
receivers: []Receiver{r},
asset: asset,
feeSats: feeSatsPerNode,
roundLifetime: roundLifetime,
@@ -478,3 +474,45 @@ func createUpperLevel(nodes []*node) ([]*node, error) {
}
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) {
case *CSVSigClosure:
isASP := c.Pubkey.IsEqual(expectedPublicKeyASP)
isASP := bytes.Equal(schnorr.SerializePubKey(c.Pubkey), schnorr.SerializePubKey(expectedPublicKeyASP))
isSweepDelay := int64(c.Seconds) == expectedSequenceSeconds
if isASP && !isSweepDelay {

View File

@@ -73,22 +73,34 @@ This will add a `state.json` file to the following directory:
```bash
$ export ARK_WALLET_DATADIR=path/to/custom
$ ark init --password <password> --ark-url localhost:6000
```
Add funds to the ark wallet:
```
$ ark faucet
# ark now has 10000 sats on its offchain balance
$ ark receive
{
"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:
```
$ export ARK_WALLET_DATADIR=./datadir
$ alias ark2=$(pwd)/build/ark-cli-<os>-<arch>
$ 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.
@@ -98,7 +110,7 @@ $ ark2 faucet
You can now make ark payments between the 2 ark wallets:
```
$ ark receive
$ ark2 receive
{
"offchain_address": <address starting with "tark1q...">,
"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:
@@ -117,15 +129,15 @@ Both balances should reflect the payment:
```
$ ark balance
{
"offchain_balance": 12100,
"onchain_balance": 0
"offchain_balance": 18900,
"onchain_balance": 78872
}
```
```
$ ark2 balance
{
"offchain_balance": 7900,
"offchain_balance": 2100,
"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": {
"get": {
"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": {
"post": {
"operationId": "ArkService_ClaimPayment",
@@ -335,9 +337,6 @@
"v1ClaimPaymentResponse": {
"type": "object"
},
"v1FaucetResponse": {
"type": "object"
},
"v1FinalizePaymentRequest": {
"type": "object",
"properties": {
@@ -429,6 +428,23 @@
}
}
},
"v1OnboardRequest": {
"type": "object",
"properties": {
"boardingTx": {
"type": "string"
},
"congestionTree": {
"$ref": "#/definitions/v1Tree"
},
"userPubkey": {
"type": "string"
}
}
},
"v1OnboardResponse": {
"type": "object"
},
"v1Output": {
"type": "object",
"properties": {

View File

@@ -38,11 +38,6 @@ service ArkService {
get: "/v1/ping/{payment_id}"
};
};
rpc Faucet(FaucetRequest) returns (FaucetResponse) {
option (google.api.http) = {
post: "/v1/faucet/{address}"
};
}
rpc ListVtxos(ListVtxosRequest) returns (ListVtxosResponse) {
option (google.api.http) = {
get: "/v1/vtxos/{address}"
@@ -53,6 +48,21 @@ service ArkService {
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 {
@@ -151,12 +161,6 @@ message PingRequest {
message PingResponse {}
message FaucetRequest {
string address = 1;
}
message FaucetResponse {}
message ListVtxosRequest {
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) {
var protoReq ListVtxosRequest
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".
// UnaryRPC :call ArkServiceServer directly.
// 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) {
ctx, cancel := context.WithCancel(req.Context())
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
}
@@ -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) {
ctx, cancel := context.WithCancel(req.Context())
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
}
@@ -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_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_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 (
@@ -840,9 +814,9 @@ var (
forward_ArkService_Ping_0 = runtime.ForwardResponseMessage
forward_ArkService_Faucet_0 = runtime.ForwardResponseMessage
forward_ArkService_ListVtxos_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)
GetEventStream(ctx context.Context, in *GetEventStreamRequest, opts ...grpc.CallOption) (ArkService_GetEventStreamClient, 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)
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 {
@@ -114,15 +114,6 @@ func (c *arkServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...gr
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) {
out := new(ListVtxosResponse)
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
}
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.
// All implementations should embed UnimplementedArkServiceServer
// for forward compatibility
@@ -151,9 +151,9 @@ type ArkServiceServer interface {
GetRound(context.Context, *GetRoundRequest) (*GetRoundResponse, error)
GetEventStream(*GetEventStreamRequest, ArkService_GetEventStreamServer) error
Ping(context.Context, *PingRequest) (*PingResponse, error)
Faucet(context.Context, *FaucetRequest) (*FaucetResponse, error)
ListVtxos(context.Context, *ListVtxosRequest) (*ListVtxosResponse, error)
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
Onboard(context.Context, *OnboardRequest) (*OnboardResponse, error)
}
// 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) {
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) {
return nil, status.Errorf(codes.Unimplemented, "method ListVtxos not implemented")
}
func (UnimplementedArkServiceServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
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.
// 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)
}
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) {
in := new(ListVtxosRequest)
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)
}
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.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -391,10 +391,6 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Ping",
Handler: _ArkService_Ping_Handler,
},
{
MethodName: "Faucet",
Handler: _ArkService_Faucet_Handler,
},
{
MethodName: "ListVtxos",
Handler: _ArkService_ListVtxos_Handler,
@@ -403,6 +399,10 @@ var ArkService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetInfo",
Handler: _ArkService_GetInfo_Handler,
},
{
MethodName: "Onboard",
Handler: _ArkService_Onboard_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

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

View File

@@ -8,6 +8,7 @@ import (
"time"
"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/ports"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -20,10 +21,6 @@ import (
var (
paymentsThreshold = int64(128)
dustAmount = uint64(450)
faucetVtxo = domain.VtxoKey{
Txid: "0000000000000000000000000000000000000000000000000000000000000000",
VOut: 0,
}
)
type Service interface {
@@ -32,12 +29,12 @@ type Service interface {
SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error)
ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) 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)
GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent
UpdatePaymentStatus(ctx context.Context, id string) error
ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, 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 {
@@ -88,8 +85,9 @@ func NewService(
}
repoManager.RegisterEventsHandler(
func(round *domain.Round) {
svc.updateProjectionStore(round)
svc.propagateEvents(round)
go svc.updateVtxoSet(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)
}
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 {
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
}
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() {
s.startRound()
}
@@ -365,15 +360,6 @@ func (s *service) finalizeRound() {
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)
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
@@ -403,11 +389,13 @@ func (s *service) listenToRedemptions() {
}
}
func (s *service) updateProjectionStore(round *domain.Round) {
ctx := context.Background()
lastChange := round.Events()[len(round.Events())-1]
func (s *service) updateVtxoSet(round *domain.Round) {
// Update the vtxo set only after a round is finalized.
if _, ok := lastChange.(domain.RoundFinalized); ok {
if !round.IsEnded() {
return
}
ctx := context.Background()
repo := s.repoManager.Vtxos()
spentVtxos := getSpentVtxos(round.Payments)
if len(spentVtxos) > 0 {
@@ -446,7 +434,6 @@ func (s *service) updateProjectionStore(round *domain.Round) {
}
}()
}
}
func (s *service) propagateEvents(round *domain.Round) {
lastEvent := round.Events()[len(round.Events())-1]
@@ -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 {
leaves := round.CongestionTree.Leaves()
vtxos := make([]domain.Vtxo, 0)
@@ -590,11 +594,25 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
vtxos := make([]domain.VtxoKey, 0)
for _, p := range payments {
for _, vtxo := range p.Inputs {
if vtxo.VtxoKey == faucetVtxo {
continue
}
vtxos = append(vtxos, vtxo.VtxoKey)
}
}
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)
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 {
return err
}
@@ -290,7 +290,7 @@ func (s *sweeper) findSweepableOutputs(
newNodesToCheck := make([]tree.Node, 0)
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 {
return nil, err
}
@@ -298,10 +298,10 @@ func (s *sweeper) findSweepableOutputs(
var expirationTime int64
var sweepInputs []ports.SweepInput
if !isPublished {
if !isConfirmed {
if _, ok := blocktimeCache[node.ParentTxid]; !ok {
isPublished, blocktime, err := s.wallet.IsTransactionPublished(ctx, node.ParentTxid)
if !isPublished || err != nil {
isConfirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, node.ParentTxid)
if !isConfirmed || err != nil {
return nil, fmt.Errorf("tx %s not found", node.Txid)
}

View File

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

View File

@@ -28,6 +28,14 @@ func NewPayment(inputs []Vtxo) (*Payment, error) {
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) {
if p.Receivers == nil {
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 {
r := &Round{}

View File

@@ -17,7 +17,7 @@ type WalletService interface {
SelectUtxos(ctx context.Context, asset string, amount uint64) ([]TxInput, uint64, 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
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)
Close()
}

View File

@@ -117,7 +117,7 @@ func (s *service) BroadcastTransaction(
return res.GetTxid(), nil
}
func (s *service) IsTransactionPublished(
func (s *service) IsTransactionConfirmed(
ctx context.Context, txid string,
) (bool, int64, error) {
_, 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.
// This is safe as the memory allocated for `craftCongestionTree` is freed
// only after `BuildPoolTx` returns.
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := craftCongestionTree(
b.net.AssetID, aspPubkey, payments, minRelayFee, b.roundLifetime, b.exitDelay,
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
b.net.AssetID, aspPubkey, getOffchainReceivers(payments), minRelayFee, b.roundLifetime, b.exitDelay,
)
if err != nil {
return

View File

@@ -97,7 +97,7 @@ func (m *mockedWallet) EstimateFees(ctx context.Context, pset string) (uint64, e
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)
var res bool

View File

@@ -4,6 +4,7 @@ import (
"encoding/hex"
"fmt"
"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/btcsuite/btcd/btcec/v2/schnorr"
@@ -14,7 +15,6 @@ import (
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
)
@@ -62,12 +62,15 @@ func getOnchainReceivers(
func getOffchainReceivers(
payments []domain.Payment,
) []domain.Receiver {
receivers := make([]domain.Receiver, 0)
) []tree.Receiver {
receivers := make([]tree.Receiver, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
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
}
// 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) {
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
}
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) {
if req.GetPaymentId() == "" {
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
}
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) {
if len(req.GetTxid()) <= 0 {
return nil, status.Error(codes.InvalidArgument, "missing pool txid")
@@ -305,3 +323,32 @@ func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
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
}