* Cleanup common

* Cleanup client

* Cleanup server

* Renamings

* Tidy up proto

* Update ocean protos

* Fixes

* Fixes
This commit is contained in:
Pietralberto Mazza
2024-02-28 18:05:03 +01:00
committed by GitHub
parent 1650ea5935
commit 6d0d03e316
31 changed files with 2475 additions and 2217 deletions

View File

@@ -26,16 +26,20 @@ var balanceCommand = cli.Command{
func balanceAction(ctx *cli.Context) error { func balanceAction(ctx *cli.Context) error {
withExpiryDetails := ctx.Bool("expiry-details") withExpiryDetails := ctx.Bool("expiry-details")
client, cancel, err := getClientFromState(ctx) client, cancel, err := getClientFromState()
if err != nil { if err != nil {
return err return err
} }
defer cancel() defer cancel()
offchainAddr, onchainAddr, err := getAddress() offchainAddr, onchainAddr, redemptionAddr, err := getAddress()
if err != nil { if err != nil {
return err return err
} }
_, network := getNetwork()
// No need to check for error here becuase this function is called also by getAddress().
// nolint:all
unilateralExitDelay, _ := getUnilateralExitDelay()
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
wg.Add(3) wg.Add(3)
@@ -45,7 +49,7 @@ func balanceAction(ctx *cli.Context) error {
defer wg.Done() defer wg.Done()
explorer := NewExplorer() explorer := NewExplorer()
balance, amountByExpiration, err := getOffchainBalance( balance, amountByExpiration, err := getOffchainBalance(
ctx, explorer, client, offchainAddr, withExpiryDetails, ctx.Context, explorer, client, offchainAddr, withExpiryDetails,
) )
if err != nil { if err != nil {
chRes <- balanceRes{0, 0, nil, nil, err} chRes <- balanceRes{0, 0, nil, nil, err}
@@ -57,7 +61,8 @@ func balanceAction(ctx *cli.Context) error {
go func() { go func() {
defer wg.Done() defer wg.Done()
balance, err := getOnchainBalance(onchainAddr) explorer := NewExplorer()
balance, err := explorer.GetBalance(onchainAddr, network.AssetID)
if err != nil { if err != nil {
chRes <- balanceRes{0, 0, nil, nil, err} chRes <- balanceRes{0, 0, nil, nil, err}
return return
@@ -67,13 +72,17 @@ func balanceAction(ctx *cli.Context) error {
go func() { go func() {
defer wg.Done() defer wg.Done()
availableBalance, futureBalance, err := getOnchainVtxosBalance() explorer := NewExplorer()
spendableBalance, lockedBalance, err := explorer.GetRedeemedVtxosBalance(
redemptionAddr, unilateralExitDelay,
)
if err != nil { if err != nil {
chRes <- balanceRes{0, 0, nil, nil, err} chRes <- balanceRes{0, 0, nil, nil, err}
return return
} }
chRes <- balanceRes{0, availableBalance, futureBalance, nil, err} chRes <- balanceRes{0, spendableBalance, lockedBalance, nil, err}
}() }()
wg.Wait() wg.Wait()
@@ -90,11 +99,11 @@ func balanceAction(ctx *cli.Context) error {
if res.offchainBalance > 0 { if res.offchainBalance > 0 {
offchainBalance = res.offchainBalance offchainBalance = res.offchainBalance
} }
if res.onchainBalance > 0 { if res.onchainSpendableBalance > 0 {
onchainBalance += res.onchainBalance onchainBalance += res.onchainSpendableBalance
} }
if res.amountByExpiration != nil { if res.offchainBalanceByExpiration != nil {
for timestamp, amount := range res.amountByExpiration { for timestamp, amount := range res.offchainBalanceByExpiration {
if nextExpiration == 0 || timestamp < nextExpiration { if nextExpiration == 0 || timestamp < nextExpiration {
nextExpiration = timestamp nextExpiration = timestamp
} }
@@ -109,8 +118,8 @@ func balanceAction(ctx *cli.Context) error {
) )
} }
} }
if res.futureBalance != nil { if res.onchainLockedBalance != nil {
for timestamp, amount := range res.futureBalance { for timestamp, amount := range res.onchainLockedBalance {
fancyTime := time.Unix(timestamp, 0).Format("2006-01-02 15:04:05") fancyTime := time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
lockedOnchainBalance = append( lockedOnchainBalance = append(
lockedOnchainBalance, lockedOnchainBalance,
@@ -178,8 +187,8 @@ func balanceAction(ctx *cli.Context) error {
type balanceRes struct { type balanceRes struct {
offchainBalance uint64 offchainBalance uint64
onchainBalance uint64 onchainSpendableBalance uint64
futureBalance map[int64]uint64 // availableAt -> onchain balance onchainLockedBalance map[int64]uint64
amountByExpiration map[int64]uint64 // expireAt -> offchain balance offchainBalanceByExpiration map[int64]uint64
err error err error
} }

View File

@@ -1,12 +1,12 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
"time" "time"
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/urfave/cli/v2"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
@@ -21,13 +21,10 @@ type vtxo struct {
} }
func getVtxos( func getVtxos(
ctx *cli.Context, ctx context.Context, explorer Explorer, client arkv1.ArkServiceClient,
explorer Explorer, addr string, withExpiration bool,
client arkv1.ArkServiceClient,
addr string,
withExpiration bool,
) ([]vtxo, error) { ) ([]vtxo, error) {
response, err := client.ListVtxos(ctx.Context, &arkv1.ListVtxosRequest{ response, err := client.ListVtxos(ctx, &arkv1.ListVtxosRequest{
Address: addr, Address: addr,
}) })
if err != nil { if err != nil {
@@ -54,7 +51,7 @@ func getVtxos(
} }
for vtxoTxid, branch := range redeemBranches { for vtxoTxid, branch := range redeemBranches {
expiration, err := branch.ExpireAt() expiration, err := branch.expireAt()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -70,19 +67,19 @@ func getVtxos(
return vtxos, nil return vtxos, nil
} }
func getClientFromState(ctx *cli.Context) (arkv1.ArkServiceClient, func(), error) { func getClientFromState() (arkv1.ArkServiceClient, func(), error) {
state, err := getState() state, err := getState()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
addr, ok := state["ark_url"].(string) addr := state[ASP_URL]
if !ok { if len(addr) <= 0 {
return nil, nil, fmt.Errorf("missing ark_url") return nil, nil, fmt.Errorf("missing asp url")
} }
return getClient(ctx, addr) return getClient(addr)
} }
func getClient(ctx *cli.Context, addr string) (arkv1.ArkServiceClient, func(), error) { func getClient(addr string) (arkv1.ArkServiceClient, func(), error) {
creds := insecure.NewCredentials() creds := insecure.NewCredentials()
port := 80 port := 80
if strings.HasPrefix(addr, "https://") { if strings.HasPrefix(addr, "https://") {

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@@ -9,6 +10,7 @@ import (
"io" "io"
"net/http" "net/http"
"sort" "sort"
"strconv"
"syscall" "syscall"
"time" "time"
@@ -18,7 +20,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr" "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/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil" "github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
@@ -44,9 +45,9 @@ func verifyPassword(password []byte) error {
return err return err
} }
passwordHashString, ok := state["password_hash"].(string) passwordHashString := state[PASSWORD_HASH]
if !ok { if len(passwordHashString) <= 0 {
return fmt.Errorf("password hash not found") return fmt.Errorf("missing password hash")
} }
passwordHash, err := hex.DecodeString(passwordHashString) passwordHash, err := hex.DecodeString(passwordHashString)
@@ -84,9 +85,9 @@ func privateKeyFromPassword() (*secp256k1.PrivateKey, error) {
return nil, err return nil, err
} }
encryptedPrivateKeyString, ok := state["encrypted_private_key"].(string) encryptedPrivateKeyString := state[ENCRYPTED_PRVKEY]
if !ok { if len(encryptedPrivateKeyString) <= 0 {
return nil, fmt.Errorf("encrypted private key not found") return nil, fmt.Errorf("missing encrypted private key")
} }
encryptedPrivateKey, err := hex.DecodeString(encryptedPrivateKeyString) encryptedPrivateKey, err := hex.DecodeString(encryptedPrivateKeyString)
@@ -100,8 +101,8 @@ func privateKeyFromPassword() (*secp256k1.PrivateKey, error) {
} }
fmt.Println("wallet unlocked") fmt.Println("wallet unlocked")
cypher := NewAES128Cypher() cypher := newAES128Cypher()
privateKeyBytes, err := cypher.Decrypt(encryptedPrivateKey, password) privateKeyBytes, err := cypher.decrypt(encryptedPrivateKey, password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -116,9 +117,9 @@ func getWalletPublicKey() (*secp256k1.PublicKey, error) {
return nil, err return nil, err
} }
publicKeyString, ok := state["public_key"].(string) publicKeyString := state[PUBKEY]
if !ok { if len(publicKeyString) <= 0 {
return nil, fmt.Errorf("public key not found") return nil, fmt.Errorf("missing public key")
} }
publicKeyBytes, err := hex.DecodeString(publicKeyString) publicKeyBytes, err := hex.DecodeString(publicKeyString)
@@ -129,15 +130,15 @@ func getWalletPublicKey() (*secp256k1.PublicKey, error) {
return secp256k1.ParsePubKey(publicKeyBytes) return secp256k1.ParsePubKey(publicKeyBytes)
} }
func getServiceProviderPublicKey() (*secp256k1.PublicKey, error) { func getAspPublicKey() (*secp256k1.PublicKey, error) {
state, err := getState() state, err := getState()
if err != nil { if err != nil {
return nil, err return nil, err
} }
arkPubKey, ok := state["ark_pubkey"].(string) arkPubKey := state[ASP_PUBKEY]
if !ok { if len(arkPubKey) <= 0 {
return nil, fmt.Errorf("ark public key not found") return nil, fmt.Errorf("missing asp public key")
} }
pubKeyBytes, err := hex.DecodeString(arkPubKey) pubKeyBytes, err := hex.DecodeString(arkPubKey)
@@ -148,32 +149,41 @@ func getServiceProviderPublicKey() (*secp256k1.PublicKey, error) {
return secp256k1.ParsePubKey(pubKeyBytes) return secp256k1.ParsePubKey(pubKeyBytes)
} }
func getLifetime() (int64, error) { func getRoundLifetime() (int64, error) {
state, err := getState() state, err := getState()
if err != nil { if err != nil {
return 0, err return -1, err
} }
lifetime, ok := state["ark_lifetime"].(float64) lifetime := state[ROUND_LIFETIME]
if !ok { if len(lifetime) <= 0 {
return 0, fmt.Errorf("lifetime not found") return -1, fmt.Errorf("missing round lifetime")
} }
return int64(lifetime), nil roundLifetime, err := strconv.Atoi(lifetime)
if err != nil {
return -1, err
}
return int64(roundLifetime), nil
} }
func getExitDelay() (int64, error) { func getUnilateralExitDelay() (int64, error) {
state, err := getState() state, err := getState()
if err != nil { if err != nil {
return 0, err return -1, err
} }
exitDelay, ok := state["exit_delay"].(float64) delay := state[UNILATERAL_EXIT_DELAY]
if !ok { if len(delay) <= 0 {
return 0, fmt.Errorf("exit delay not found") return -1, fmt.Errorf("missing unilateral exit delay")
} }
return int64(exitDelay), nil redeemDelay, err := strconv.Atoi(delay)
if err != nil {
return -1, err
}
return int64(redeemDelay), nil
} }
func coinSelect(vtxos []vtxo, amount uint64) ([]vtxo, uint64, error) { func coinSelect(vtxos []vtxo, amount uint64) ([]vtxo, uint64, error) {
@@ -201,7 +211,7 @@ func coinSelect(vtxos []vtxo, amount uint64) ([]vtxo, uint64, error) {
} }
if selectedAmount < amount { if selectedAmount < amount {
return nil, 0, fmt.Errorf("insufficient balance: %d to cover %d", selectedAmount, amount) return nil, 0, fmt.Errorf("not enough funds to cover amount%d", amount)
} }
change := selectedAmount - amount change := selectedAmount - amount
@@ -217,7 +227,8 @@ func coinSelect(vtxos []vtxo, amount uint64) ([]vtxo, uint64, error) {
} }
func getOffchainBalance( func getOffchainBalance(
ctx *cli.Context, explorer Explorer, client arkv1.ArkServiceClient, addr string, withExpiration bool, ctx context.Context, explorer Explorer, client arkv1.ArkServiceClient,
addr string, withExpiration bool,
) (uint64, map[int64]uint64, error) { ) (uint64, map[int64]uint64, error) {
amountByExpiration := make(map[int64]uint64, 0) amountByExpiration := make(map[int64]uint64, 0)
@@ -243,120 +254,6 @@ func getOffchainBalance(
return balance, amountByExpiration, nil return balance, amountByExpiration, nil
} }
type utxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount uint64 `json:"value"`
Asset string `json:"asset"`
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
func getOnchainUtxos(addr string) ([]utxo, error) {
_, net := getNetwork()
baseUrl := explorerUrl[net.Name]
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", baseUrl, addr))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(string(body))
}
payload := []utxo{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
return payload, nil
}
func getOnchainBalance(addr string) (uint64, error) {
payload, err := getOnchainUtxos(addr)
if err != nil {
return 0, err
}
_, net := getNetwork()
balance := uint64(0)
for _, p := range payload {
if p.Asset != net.AssetID {
continue
}
balance += p.Amount
}
return balance, nil
}
func getOnchainVtxosBalance() (availableBalance uint64, futureBalance map[int64]uint64, err error) {
userPubKey, err := getWalletPublicKey()
if err != nil {
return
}
aspPublicKey, err := getServiceProviderPublicKey()
if err != nil {
return
}
exitDelay, err := getExitDelay()
if err != nil {
return
}
vtxoTapKey, _, err := computeVtxoTaprootScript(userPubKey, aspPublicKey, uint(exitDelay))
if err != nil {
return
}
_, net := getNetwork()
payment, err := payment.FromTweakedKey(vtxoTapKey, net, nil)
if err != nil {
return
}
addr, err := payment.TaprootAddress()
if err != nil {
return
}
utxos, err := getOnchainUtxos(addr)
if err != nil {
return
}
availableBalance = uint64(0)
futureBalance = make(map[int64]uint64, 0)
now := time.Now()
for _, utxo := range utxos {
blocktime := now
if utxo.Status.Confirmed {
blocktime = time.Unix(utxo.Status.Blocktime, 0)
}
availableAt := blocktime.Add(time.Duration(exitDelay) * time.Second)
if availableAt.After(now) {
if _, ok := futureBalance[availableAt.Unix()]; !ok {
futureBalance[availableAt.Unix()] = 0
}
futureBalance[availableAt.Unix()] += utxo.Amount
} else {
availableBalance += utxo.Amount
}
}
return
}
func getTxBlocktime(txid string) (confirmed bool, blocktime int64, err error) { func getTxBlocktime(txid string) (confirmed bool, blocktime int64, err error) {
_, net := getNetwork() _, net := getNetwork()
baseUrl := explorerUrl[net.Name] baseUrl := explorerUrl[net.Name]
@@ -392,35 +289,13 @@ func getTxBlocktime(txid string) (confirmed bool, blocktime int64, err error) {
} }
func broadcast(txHex string) (string, error) {
_, net := getNetwork()
body := bytes.NewBuffer([]byte(txHex))
baseUrl := explorerUrl[net.Name]
resp, err := http.Post(fmt.Sprintf("%s/tx", baseUrl), "text/plain", body)
if err != nil {
return "", err
}
defer resp.Body.Close()
bodyResponse, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(string(bodyResponse))
}
return string(bodyResponse), nil
}
func getNetwork() (*common.Network, *network.Network) { func getNetwork() (*common.Network, *network.Network) {
state, err := getState() state, err := getState()
if err != nil { if err != nil {
return &common.TestNet, &network.Testnet return &common.TestNet, &network.Testnet
} }
net, ok := state["network"] net, ok := state[NETWORK]
if !ok { if !ok {
return &common.MainNet, &network.Liquid return &common.MainNet, &network.Liquid
} }
@@ -430,30 +305,54 @@ func getNetwork() (*common.Network, *network.Network) {
return &common.MainNet, &network.Liquid return &common.MainNet, &network.Liquid
} }
func getAddress() (offchainAddr, onchainAddr string, err error) { func getAddress() (offchainAddr, onchainAddr, redemptionAddr string, err error) {
publicKey, err := getWalletPublicKey() userPubkey, err := getWalletPublicKey()
if err != nil { if err != nil {
return return
} }
aspPublicKey, err := getServiceProviderPublicKey() aspPubkey, err := getAspPublicKey()
if err != nil {
return
}
unilateralExitDelay, err := getUnilateralExitDelay()
if err != nil { if err != nil {
return return
} }
arkNet, liquidNet := getNetwork() arkNet, liquidNet := getNetwork()
arkAddr, err := common.EncodeAddress(arkNet.Addr, publicKey, aspPublicKey) arkAddr, err := common.EncodeAddress(arkNet.Addr, userPubkey, aspPubkey)
if err != nil { if err != nil {
return return
} }
p2wpkh := payment.FromPublicKey(publicKey, liquidNet, nil) p2wpkh := payment.FromPublicKey(userPubkey, liquidNet, nil)
liquidAddr, err := p2wpkh.WitnessPubKeyHash() liquidAddr, err := p2wpkh.WitnessPubKeyHash()
if err != nil { if err != nil {
return return
} }
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil {
return
}
_, net := getNetwork()
payment, err := payment.FromTweakedKey(vtxoTapKey, net, nil)
if err != nil {
return
}
redemptionAddr, err = payment.TaprootAddress()
if err != nil {
return
}
offchainAddr = arkAddr offchainAddr = arkAddr
onchainAddr = liquidAddr onchainAddr = liquidAddr
@@ -471,14 +370,10 @@ func printJSON(resp interface{}) error {
} }
func handleRoundStream( func handleRoundStream(
ctx *cli.Context, ctx context.Context, client arkv1.ArkServiceClient, paymentID string,
client arkv1.ArkServiceClient, vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
paymentID string,
vtxosToSign []vtxo,
secKey *secp256k1.PrivateKey,
receivers []*arkv1.Output,
) (poolTxID string, err error) { ) (poolTxID string, err error) {
stream, err := client.GetEventStream(ctx.Context, &arkv1.GetEventStreamRequest{}) stream, err := client.GetEventStream(ctx, &arkv1.GetEventStreamRequest{})
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -502,54 +397,53 @@ func handleRoundStream(
return "", err return "", err
} }
if event.GetRoundFailed() != nil { if e := event.GetRoundFailed(); e != nil {
pingStop() pingStop()
return "", fmt.Errorf("round failed: %s", event.GetRoundFailed().GetReason()) return "", fmt.Errorf("round failed: %s", e.GetReason())
} }
if event.GetRoundFinalization() != nil { if e := event.GetRoundFinalization(); e != nil {
// stop pinging as soon as we receive some forfeit txs // stop pinging as soon as we receive some forfeit txs
pingStop() pingStop()
fmt.Println("round finalization started") fmt.Println("round finalization started")
poolPartialTx := event.GetRoundFinalization().GetPoolPartialTx() poolTxStr := e.GetPoolPartialTx()
poolTransaction, err := psetv2.NewPsetFromBase64(poolPartialTx) poolTx, err := psetv2.NewPsetFromBase64(poolTxStr)
if err != nil { if err != nil {
return "", err return "", err
} }
congestionTree, err := toCongestionTree(event.GetRoundFinalization().GetCongestionTree()) congestionTree, err := toCongestionTree(e.GetCongestionTree())
if err != nil { if err != nil {
return "", err return "", err
} }
aspPublicKey, err := getServiceProviderPublicKey() aspPubkey, err := getAspPublicKey()
if err != nil { if err != nil {
return "", err return "", err
} }
seconds, err := getLifetime() roundLifetime, err := getRoundLifetime()
if err != nil { if err != nil {
return "", err return "", err
} }
// validate the congestion tree // validate the congestion tree
if err := tree.ValidateCongestionTree( if err := tree.ValidateCongestionTree(
congestionTree, congestionTree, poolTxStr, aspPubkey, int64(roundLifetime),
poolPartialTx,
aspPublicKey,
int64(seconds),
); err != nil { ); err != nil {
return "", err return "", err
} }
exitDelay, err := getExitDelay() exitDelay, err := getUnilateralExitDelay()
if err != nil { if err != nil {
return "", err return "", err
} }
for _, receiver := range receivers { for _, receiver := range receivers {
isOnChain, onchainScript, userPubKey, err := decodeReceiverAddress(receiver.Address) isOnChain, onchainScript, userPubkey, err := decodeReceiverAddress(
receiver.Address,
)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -558,10 +452,13 @@ func handleRoundStream(
// collaborative exit case // collaborative exit case
// search for the output in the pool tx // search for the output in the pool tx
found := false found := false
for _, output := range poolTransaction.Outputs { for _, output := range poolTx.Outputs {
if bytes.Equal(output.Script, onchainScript) { if bytes.Equal(output.Script, onchainScript) {
if output.Value != receiver.Amount { if output.Value != receiver.Amount {
return "", fmt.Errorf("invalid collaborative exit output amount: got %d, want %d", output.Value, receiver.Amount) return "", fmt.Errorf(
"invalid collaborative exit output amount: got %d, want %d",
output.Value, receiver.Amount,
)
} }
found = true found = true
@@ -570,7 +467,9 @@ func handleRoundStream(
} }
if !found { if !found {
return "", fmt.Errorf("collaborative exit output not found: %s", receiver.Address) return "", fmt.Errorf(
"collaborative exit output not found: %s", receiver.Address,
)
} }
continue continue
@@ -581,7 +480,9 @@ func handleRoundStream(
found := false found := false
// compute the receiver output taproot key // compute the receiver output taproot key
outputTapKey, _, err := computeVtxoTaprootScript(userPubKey, aspPublicKey, uint(exitDelay)) outputTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(exitDelay),
)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -597,7 +498,9 @@ func handleRoundStream(
if len(output.Script) == 0 { if len(output.Script) == 0 {
continue continue
} }
if bytes.Equal(output.Script[2:], schnorr.SerializePubKey(outputTapKey)) { if bytes.Equal(
output.Script[2:], schnorr.SerializePubKey(outputTapKey),
) {
if output.Value != receiver.Amount { if output.Value != receiver.Amount {
continue continue
} }
@@ -613,13 +516,15 @@ func handleRoundStream(
} }
if !found { if !found {
return "", fmt.Errorf("off-chain send output not found: %s", receiver.Address) return "", fmt.Errorf(
"off-chain send output not found: %s", receiver.Address,
)
} }
} }
fmt.Println("congestion tree validated") fmt.Println("congestion tree validated")
forfeits := event.GetRoundFinalization().GetForfeitTxs() forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0) signedForfeits := make([]string, 0)
fmt.Print("signing forfeit txs... ") fmt.Print("signing forfeit txs... ")
@@ -665,7 +570,7 @@ func handleRoundStream(
fmt.Printf("%d signed\n", len(signedForfeits)) fmt.Printf("%d signed\n", len(signedForfeits))
fmt.Print("finalizing payment... ") fmt.Print("finalizing payment... ")
_, err = client.FinalizePayment(ctx.Context, &arkv1.FinalizePaymentRequest{ _, err = client.FinalizePayment(ctx, &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeits, SignedForfeitTxs: signedForfeits,
}) })
if err != nil { if err != nil {
@@ -687,8 +592,10 @@ func handleRoundStream(
// send 1 ping message every 5 seconds to signal to the ark service that we are still alive // send 1 ping message every 5 seconds to signal to the ark service that we are still alive
// returns a function that can be used to stop the pinging // returns a function that can be used to stop the pinging
func ping(ctx *cli.Context, client arkv1.ArkServiceClient, req *arkv1.PingRequest) func() { func ping(
_, err := client.Ping(ctx.Context, req) ctx context.Context, client arkv1.ArkServiceClient, req *arkv1.PingRequest,
) func() {
_, err := client.Ping(ctx, req)
if err != nil { if err != nil {
return nil return nil
} }
@@ -698,7 +605,7 @@ func ping(ctx *cli.Context, client arkv1.ArkServiceClient, req *arkv1.PingReques
go func(t *time.Ticker) { go func(t *time.Ticker) {
for range t.C { for range t.C {
// nolint // nolint
client.Ping(ctx.Context, req) client.Ping(ctx, req)
} }
}(ticker) }(ticker)
@@ -758,18 +665,15 @@ func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
} }
func decodeReceiverAddress(addr string) ( func decodeReceiverAddress(addr string) (
isOnChainAddress bool, bool, []byte, *secp256k1.PublicKey, error,
onchainScript []byte,
userPubKey *secp256k1.PublicKey,
err error,
) { ) {
outputScript, err := address.ToOutputScript(addr) outputScript, err := address.ToOutputScript(addr)
if err != nil { if err != nil {
_, userPubKey, _, err = common.DecodeAddress(addr) _, userPubkey, _, err := common.DecodeAddress(addr)
if err != nil { if err != nil {
return return false, nil, nil, err
} }
return false, nil, userPubKey, nil return false, nil, userPubkey, nil
} }
return true, outputScript, nil, nil return true, outputScript, nil, nil
@@ -777,18 +681,20 @@ func decodeReceiverAddress(addr string) (
func findSweepClosure( func findSweepClosure(
congestionTree tree.CongestionTree, congestionTree tree.CongestionTree,
) (sweepClosure *taproot.TapElementsLeaf, seconds uint, err error) { ) (*taproot.TapElementsLeaf, uint, error) {
root, err := congestionTree.Root() root, err := congestionTree.Root()
if err != nil { if err != nil {
return return nil, 0, err
} }
// find the sweep closure // find the sweep closure
tx, err := psetv2.NewPsetFromBase64(root.Tx) tx, err := psetv2.NewPsetFromBase64(root.Tx)
if err != nil { if err != nil {
return return nil, 0, err
} }
var seconds uint
var sweepClosure *taproot.TapElementsLeaf
for _, tapLeaf := range tx.Inputs[0].TapLeafScript { for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
closure := &tree.CSVSigClosure{} closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(tapLeaf.Script) valid, err := closure.Decode(tapLeaf.Script)
@@ -806,21 +712,19 @@ func findSweepClosure(
return nil, 0, fmt.Errorf("sweep closure not found") return nil, 0, fmt.Errorf("sweep closure not found")
} }
return return sweepClosure, seconds, nil
} }
func getRedeemBranches( func getRedeemBranches(
ctx *cli.Context, ctx context.Context, explorer Explorer, client arkv1.ArkServiceClient,
explorer Explorer,
client arkv1.ArkServiceClient,
vtxos []vtxo, vtxos []vtxo,
) (map[string]RedeemBranch, error) { ) (map[string]*redeemBranch, error) {
congestionTrees := make(map[string]tree.CongestionTree, 0) // poolTxid -> congestionTree congestionTrees := make(map[string]tree.CongestionTree, 0)
redeemBranches := make(map[string]RedeemBranch, 0) // vtxo.txid -> redeemBranch redeemBranches := make(map[string]*redeemBranch, 0)
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.poolTxid]; !ok { if _, ok := congestionTrees[vtxo.poolTxid]; !ok {
round, err := client.GetRound(ctx.Context, &arkv1.GetRoundRequest{ round, err := client.GetRound(ctx, &arkv1.GetRoundRequest{
Txid: vtxo.poolTxid, Txid: vtxo.poolTxid,
}) })
if err != nil { if err != nil {
@@ -836,7 +740,9 @@ func getRedeemBranches(
congestionTrees[vtxo.poolTxid] = congestionTree congestionTrees[vtxo.poolTxid] = congestionTree
} }
redeemBranch, err := newRedeemBranch(ctx, explorer, congestionTrees[vtxo.poolTxid], vtxo) redeemBranch, err := newRedeemBranch(
ctx, explorer, congestionTrees[vtxo.poolTxid], vtxo,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -848,18 +754,16 @@ func getRedeemBranches(
} }
func computeVtxoTaprootScript( func computeVtxoTaprootScript(
userPubKey *secp256k1.PublicKey, userPubkey, aspPubkey *secp256k1.PublicKey, exitDelay uint,
aspPublicKey *secp256k1.PublicKey,
exitDelay uint,
) (*secp256k1.PublicKey, *taproot.TapscriptElementsProof, error) { ) (*secp256k1.PublicKey, *taproot.TapscriptElementsProof, error) {
redeemClosure := &tree.CSVSigClosure{ redeemClosure := &tree.CSVSigClosure{
Pubkey: userPubKey, Pubkey: userPubkey,
Seconds: exitDelay, Seconds: exitDelay,
} }
forfeitClosure := &tree.ForfeitClosure{ forfeitClosure := &tree.ForfeitClosure{
Pubkey: userPubKey, Pubkey: userPubkey,
AspPubkey: aspPublicKey, AspPubkey: aspPubkey,
} }
redeemLeaf, err := redeemClosure.Leaf() redeemLeaf, err := redeemClosure.Leaf()
@@ -872,7 +776,9 @@ func computeVtxoTaprootScript(
return nil, nil, err return nil, nil, err
} }
vtxoTaprootTree := taproot.AssembleTaprootScriptTree(*redeemLeaf, *forfeitLeaf) vtxoTaprootTree := taproot.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := vtxoTaprootTree.RootNode.TapHash() root := vtxoTaprootTree.RootNode.TapHash()
unspendableKey := tree.UnspendableKey() unspendableKey := tree.UnspendableKey()
@@ -886,9 +792,7 @@ func computeVtxoTaprootScript(
} }
func addVtxoInput( func addVtxoInput(
updater *psetv2.Updater, updater *psetv2.Updater, inputArgs psetv2.InputArgs, exitDelay uint,
inputArgs psetv2.InputArgs,
exitDelay uint,
tapLeafProof *taproot.TapscriptElementsProof, tapLeafProof *taproot.TapscriptElementsProof,
) error { ) error {
sequence, err := common.BIP68EncodeAsNumber(exitDelay) sequence, err := common.BIP68EncodeAsNumber(exitDelay)
@@ -912,18 +816,20 @@ func addVtxoInput(
) )
} }
func coinSelectOnchain(targetAmount uint64, exclude []utxo) (utxos []utxo, delayedUtxos []utxo, change uint64, err error) { func coinSelectOnchain(
_, onchainAddr, err := getAddress() explorer Explorer, targetAmount uint64, exclude []utxo,
) ([]utxo, []utxo, uint64, error) {
_, onchainAddr, _, err := getAddress()
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
fromExplorer, err := getOnchainUtxos(onchainAddr) fromExplorer, err := explorer.GetUtxos(onchainAddr)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
utxos = make([]utxo, 0) utxos := make([]utxo, 0)
selectedAmount := uint64(0) selectedAmount := uint64(0)
for _, utxo := range fromExplorer { for _, utxo := range fromExplorer {
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
@@ -944,22 +850,24 @@ func coinSelectOnchain(targetAmount uint64, exclude []utxo) (utxos []utxo, delay
return utxos, nil, selectedAmount - targetAmount, nil return utxos, nil, selectedAmount - targetAmount, nil
} }
userPubKey, err := getWalletPublicKey() userPubkey, err := getWalletPublicKey()
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
aspPublicKey, err := getServiceProviderPublicKey() aspPubkey, err := getAspPublicKey()
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
exitDelay, err := getExitDelay() unilateralExitDelay, err := getUnilateralExitDelay()
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
vtxoTapKey, _, err := computeVtxoTaprootScript(userPubKey, aspPublicKey, uint(exitDelay)) vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
@@ -976,18 +884,20 @@ func coinSelectOnchain(targetAmount uint64, exclude []utxo) (utxos []utxo, delay
return nil, nil, 0, err return nil, nil, 0, err
} }
fromExplorer, err = getOnchainUtxos(addr) fromExplorer, err = explorer.GetUtxos(addr)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
delayedUtxos = make([]utxo, 0) delayedUtxos := make([]utxo, 0)
for _, utxo := range fromExplorer { for _, utxo := range fromExplorer {
if selectedAmount >= targetAmount { if selectedAmount >= targetAmount {
break break
} }
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(time.Duration(exitDelay) * time.Second) availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(unilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) { if availableAt.After(time.Now()) {
continue continue
} }
@@ -1003,19 +913,18 @@ func coinSelectOnchain(targetAmount uint64, exclude []utxo) (utxos []utxo, delay
} }
if selectedAmount < targetAmount { if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf("insufficient balance: %d to cover %d", selectedAmount, targetAmount) return nil, nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
} }
return utxos, delayedUtxos, selectedAmount - targetAmount, nil return utxos, delayedUtxos, selectedAmount - targetAmount, nil
} }
func addInputs( func addInputs(
updater *psetv2.Updater, updater *psetv2.Updater, utxos, delayedUtxos []utxo, net *network.Network,
selected []utxo, // the utxos to add owned by the P2WPKH script
delayedSelected []utxo, // the utxos to add owned by the VTXO script
net *network.Network,
) error { ) error {
_, onchainAddr, err := getAddress() _, onchainAddr, _, err := getAddress()
if err != nil { if err != nil {
return err return err
} }
@@ -1025,23 +934,22 @@ func addInputs(
return err return err
} }
for _, coin := range selected { for _, utxo := range utxos {
fmt.Println("adding input", coin.Txid, coin.Vout)
if err := updater.AddInputs([]psetv2.InputArgs{ if err := updater.AddInputs([]psetv2.InputArgs{
{ {
Txid: coin.Txid, Txid: utxo.Txid,
TxIndex: coin.Vout, TxIndex: utxo.Vout,
}, },
}); err != nil { }); err != nil {
return err return err
} }
assetID, err := elementsutil.AssetHashToBytes(coin.Asset) assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil { if err != nil {
return err return err
} }
value, err := elementsutil.ValueToBytes(coin.Amount) value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil { if err != nil {
return err return err
} }
@@ -1053,28 +961,32 @@ func addInputs(
Nonce: []byte{0x00}, Nonce: []byte{0x00},
} }
if err := updater.AddInWitnessUtxo(len(updater.Pset.Inputs)-1, &witnessUtxo); err != nil { if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, &witnessUtxo,
); err != nil {
return err return err
} }
} }
if len(delayedSelected) > 0 { if len(delayedUtxos) > 0 {
userPubKey, err := getWalletPublicKey() userPubkey, err := getWalletPublicKey()
if err != nil { if err != nil {
return err return err
} }
aspPublicKey, err := getServiceProviderPublicKey() aspPubkey, err := getAspPublicKey()
if err != nil { if err != nil {
return err return err
} }
exitDelay, err := getExitDelay() unilateralExitDelay, err := getUnilateralExitDelay()
if err != nil { if err != nil {
return err return err
} }
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(userPubKey, aspPublicKey, uint(exitDelay)) vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil { if err != nil {
return err return err
} }
@@ -1094,25 +1006,25 @@ func addInputs(
return err return err
} }
for _, coin := range delayedSelected { for _, utxo := range delayedUtxos {
if err := addVtxoInput( if err := addVtxoInput(
updater, updater,
psetv2.InputArgs{ psetv2.InputArgs{
Txid: coin.Txid, Txid: utxo.Txid,
TxIndex: coin.Vout, TxIndex: utxo.Vout,
}, },
uint(exitDelay), uint(unilateralExitDelay),
leafProof, leafProof,
); err != nil { ); err != nil {
return err return err
} }
assetID, err := elementsutil.AssetHashToBytes(coin.Asset) assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil { if err != nil {
return err return err
} }
value, err := elementsutil.ValueToBytes(coin.Amount) value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil { if err != nil {
return err return err
} }
@@ -1124,7 +1036,9 @@ func addInputs(
Nonce: []byte{0x00}, Nonce: []byte{0x00},
} }
if err := updater.AddInWitnessUtxo(len(updater.Pset.Inputs)-1, &witnessUtxo); err != nil { if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, &witnessUtxo,
); err != nil {
return err return err
} }
} }

View File

@@ -10,13 +10,13 @@ import (
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
) )
type Cypher struct{} type cypher struct{}
func NewAES128Cypher() *Cypher { func newAES128Cypher() *cypher {
return &Cypher{} return &cypher{}
} }
func (c *Cypher) Encrypt(privateKey, password []byte) ([]byte, error) { func (c *cypher) encrypt(privateKey, password []byte) ([]byte, error) {
// Due to https://github.com/golang/go/issues/7168. // Due to https://github.com/golang/go/issues/7168.
// This call makes sure that memory is freed in case the GC doesn't do that // This call makes sure that memory is freed in case the GC doesn't do that
// right after the encryption/decryption. // right after the encryption/decryption.
@@ -53,7 +53,7 @@ func (c *Cypher) Encrypt(privateKey, password []byte) ([]byte, error) {
return ciphertext, nil return ciphertext, nil
} }
func (c *Cypher) Decrypt(encrypted, password []byte) ([]byte, error) { func (c *cypher) decrypt(encrypted, password []byte) ([]byte, error) {
defer debug.FreeOSMemory() defer debug.FreeOSMemory()
if len(encrypted) == 0 { if len(encrypted) == 0 {

View File

@@ -19,6 +19,6 @@ func dumpAction(ctx *cli.Context) error {
} }
return printJSON(map[string]interface{}{ return printJSON(map[string]interface{}{
"privateKey": hex.EncodeToString(privateKey.Serialize()), "private_key": hex.EncodeToString(privateKey.Serialize()),
}) })
} }

View File

@@ -1,17 +1,37 @@
package main package main
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
type utxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount uint64 `json:"value"`
Asset string `json:"asset"`
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
type Explorer interface { type Explorer interface {
GetTxHex(txid string) (string, error) GetTxHex(txid string) (string, error)
Broadcast(txHex string) (string, error) Broadcast(txHex string) (string, error)
GetUtxos(addr string) ([]utxo, error)
GetBalance(addr, asset string) (uint64, error)
GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
) (uint64, map[int64]uint64, error)
} }
type explorer struct { type explorer struct {
@@ -44,17 +64,28 @@ func (e *explorer) GetTxHex(txid string) (string, error) {
return txHex, nil return txHex, nil
} }
func (e *explorer) Broadcast(txHex string) (string, error) { func (e *explorer) Broadcast(txStr string) (string, error) {
tx, err := transaction.NewTxFromHex(txHex) tx, err := transaction.NewTxFromHex(txStr)
if err != nil {
pset, err := psetv2.NewPsetFromBase64(txStr)
if err != nil { if err != nil {
return "", err return "", err
} }
extracted, err := psetv2.Extract(pset)
if err != nil {
return "", err
}
txStr, _ = extracted.ToHex()
}
txid := tx.TxHash().String() txid := tx.TxHash().String()
e.cache[txid] = txHex e.cache[txid] = txStr
txid, err = broadcast(txHex) txid, err = e.broadcast(txStr)
if err != nil { if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "transaction already in block chain") { if strings.Contains(
strings.ToLower(err.Error()), "transaction already in block chain",
) {
return txid, nil return txid, nil
} }
@@ -64,6 +95,76 @@ func (e *explorer) Broadcast(txHex string) (string, error) {
return txid, nil return txid, nil
} }
func (e *explorer) GetUtxos(addr string) ([]utxo, error) {
resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.baseUrl, addr))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(string(body))
}
payload := []utxo{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
return payload, nil
}
func (e *explorer) GetBalance(addr, asset string) (uint64, error) {
payload, err := e.GetUtxos(addr)
if err != nil {
return 0, err
}
balance := uint64(0)
for _, p := range payload {
if p.Asset != asset {
continue
}
balance += p.Amount
}
return balance, nil
}
func (e *explorer) GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
utxos, err := e.GetUtxos(addr)
if err != nil {
return
}
lockedBalance = make(map[int64]uint64, 0)
now := time.Now()
for _, utxo := range utxos {
blocktime := now
if utxo.Status.Confirmed {
blocktime = time.Unix(utxo.Status.Blocktime, 0)
}
delay := time.Duration(unilateralExitDelay) * time.Second
availableAt := blocktime.Add(delay)
if availableAt.After(now) {
if _, ok := lockedBalance[availableAt.Unix()]; !ok {
lockedBalance[availableAt.Unix()] = 0
}
lockedBalance[availableAt.Unix()] += utxo.Amount
} else {
spendableBalance += utxo.Amount
}
}
return
}
func (e *explorer) getTxHex(txid string) (string, error) { func (e *explorer) getTxHex(txid string) (string, error) {
resp, err := http.Get(fmt.Sprintf("%s/tx/%s/hex", e.baseUrl, txid)) resp, err := http.Get(fmt.Sprintf("%s/tx/%s/hex", e.baseUrl, txid))
if err != nil { if err != nil {
@@ -83,3 +184,23 @@ func (e *explorer) getTxHex(txid string) (string, error) {
e.cache[txid] = hex e.cache[txid] = hex
return hex, nil return hex, nil
} }
func (e *explorer) broadcast(txHex string) (string, error) {
body := bytes.NewBuffer([]byte(txHex))
resp, err := http.Post(fmt.Sprintf("%s/tx", e.baseUrl), "text/plain", body)
if err != nil {
return "", err
}
defer resp.Body.Close()
bodyResponse, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(string(bodyResponse))
}
return string(bodyResponse), nil
}

View File

@@ -1,8 +1,10 @@
package main package main
import ( import (
"context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"strconv"
"strings" "strings"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
@@ -57,7 +59,7 @@ func initAction(ctx *cli.Context) error {
return fmt.Errorf("invalid network") return fmt.Errorf("invalid network")
} }
if err := connectToAsp(ctx, net, url); err != nil { if err := connectToAsp(ctx.Context, net, url); err != nil {
return err return err
} }
return initWallet(ctx, key, password) return initWallet(ctx, key, password)
@@ -71,24 +73,24 @@ func generateRandomPrivateKey() (*secp256k1.PrivateKey, error) {
return privKey, nil return privKey, nil
} }
func connectToAsp(ctx *cli.Context, net, url string) error { func connectToAsp(ctx context.Context, net, url string) error {
client, close, err := getClient(ctx, url) client, close, err := getClient(url)
if err != nil { if err != nil {
return err return err
} }
defer close() defer close()
resp, err := client.GetInfo(ctx.Context, &arkv1.GetInfoRequest{}) resp, err := client.GetInfo(ctx, &arkv1.GetInfoRequest{})
if err != nil { if err != nil {
return err return err
} }
return setState(map[string]interface{}{ return setState(map[string]string{
"ark_url": url, ASP_URL: url,
"network": net, NETWORK: net,
"ark_pubkey": resp.Pubkey, ASP_PUBKEY: resp.Pubkey,
"ark_lifetime": resp.Lifetime, ROUND_LIFETIME: strconv.Itoa(int(resp.GetRoundLifetime())),
"exit_delay": resp.ExitDelay, UNILATERAL_EXIT_DELAY: strconv.Itoa(int(resp.GetUnilateralExitDelay())),
}) })
} }
@@ -109,17 +111,20 @@ func initWallet(ctx *cli.Context, key, password string) error {
privateKey = secp256k1.PrivKeyFromBytes(privKeyBytes) privateKey = secp256k1.PrivKeyFromBytes(privKeyBytes)
} }
encryptedPrivateKey, err := NewAES128Cypher().Encrypt(privateKey.Serialize(), []byte(password)) cypher := newAES128Cypher()
buf := privateKey.Serialize()
encryptedPrivateKey, err := cypher.encrypt(buf, []byte(password))
if err != nil { if err != nil {
return err return err
} }
passwordHash := hashPassword([]byte(password)) passwordHash := hashPassword([]byte(password))
state := map[string]interface{}{ pubkey := privateKey.PubKey().SerializeCompressed()
"encrypted_private_key": hex.EncodeToString(encryptedPrivateKey), state := map[string]string{
"password_hash": hex.EncodeToString(passwordHash), ENCRYPTED_PRVKEY: hex.EncodeToString(encryptedPrivateKey),
"public_key": hex.EncodeToString(privateKey.PubKey().SerializeCompressed()), PASSWORD_HASH: hex.EncodeToString(passwordHash),
PUBKEY: hex.EncodeToString(pubkey),
} }
if err := setState(state); err != nil { if err := setState(state); err != nil {

View File

@@ -17,6 +17,15 @@ const (
DATADIR_ENVVAR = "ARK_WALLET_DATADIR" DATADIR_ENVVAR = "ARK_WALLET_DATADIR"
STATE_FILE = "state.json" STATE_FILE = "state.json"
defaultNetwork = "testnet" defaultNetwork = "testnet"
ASP_URL = "asp_url"
ASP_PUBKEY = "asp_public_key"
ROUND_LIFETIME = "round_lifetime"
UNILATERAL_EXIT_DELAY = "unilateral_exit_delay "
ENCRYPTED_PRVKEY = "encrypted_private_key "
PASSWORD_HASH = "password_hash "
PUBKEY = "public_key "
NETWORK = "network "
) )
var ( var (
@@ -29,14 +38,15 @@ var (
network.Testnet.Name: "https://blockstream.info/liquidtestnet/api", network.Testnet.Name: "https://blockstream.info/liquidtestnet/api",
} }
initialState = map[string]interface{}{ initialState = map[string]string{
"ark_url": "", ASP_URL: "",
"ark_pubkey": "", ASP_PUBKEY: "",
"ark_lifetime": 0, ROUND_LIFETIME: "",
"encrypted_private_key": "", UNILATERAL_EXIT_DELAY: "",
"password_hash": "", ENCRYPTED_PRVKEY: "",
"public_key": "", PASSWORD_HASH: "",
"network": defaultNetwork, PUBKEY: "",
NETWORK: defaultNetwork,
} }
) )
@@ -110,7 +120,7 @@ func cleanAndExpandPath(path string) string {
return filepath.Clean(os.ExpandEnv(path)) return filepath.Clean(os.ExpandEnv(path))
} }
func getState() (map[string]interface{}, error) { func getState() (map[string]string, error) {
file, err := os.ReadFile(statePath) file, err := os.ReadFile(statePath)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
@@ -122,7 +132,7 @@ func getState() (map[string]interface{}, error) {
return initialState, nil return initialState, nil
} }
data := map[string]interface{}{} data := map[string]string{}
if err := json.Unmarshal(file, &data); err != nil { if err := json.Unmarshal(file, &data); err != nil {
return nil, err return nil, err
} }
@@ -138,7 +148,7 @@ func setInitialState() error {
return os.WriteFile(statePath, jsonString, 0755) return os.WriteFile(statePath, jsonString, 0755)
} }
func setState(data map[string]interface{}) error { func setState(data map[string]string) error {
currentData, err := getState() currentData, err := getState()
if err != nil { if err != nil {
return err return err
@@ -158,8 +168,8 @@ func setState(data map[string]interface{}) error {
return nil return nil
} }
func merge(maps ...map[string]interface{}) map[string]interface{} { func merge(maps ...map[string]string) map[string]string {
merge := make(map[string]interface{}, 0) merge := make(map[string]string, 0)
for _, m := range maps { for _, m := range maps {
for k, v := range m { for k, v := range m {
merge[k] = v merge[k] = v

View File

@@ -40,17 +40,17 @@ func onboardAction(ctx *cli.Context) error {
_, net := getNetwork() _, net := getNetwork()
aspPubkey, err := getServiceProviderPublicKey() aspPubkey, err := getAspPublicKey()
if err != nil { if err != nil {
return err return err
} }
lifetime, err := getLifetime() roundLifetime, err := getRoundLifetime()
if err != nil { if err != nil {
return err return err
} }
exitDelay, err := getExitDelay() unilateralExitDelay, err := getUnilateralExitDelay()
if err != nil { if err != nil {
return err return err
} }
@@ -66,7 +66,8 @@ func onboardAction(ctx *cli.Context) error {
} }
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree( treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := tree.CraftCongestionTree(
net.AssetID, aspPubkey, []tree.Receiver{congestionTreeLeaf}, minRelayFee, lifetime, exitDelay, net.AssetID, aspPubkey, []tree.Receiver{congestionTreeLeaf},
minRelayFee, roundLifetime, unilateralExitDelay,
) )
if err != nil { if err != nil {
return err return err
@@ -92,13 +93,14 @@ func onboardAction(ctx *cli.Context) error {
return err return err
} }
txid, err := broadcastPset(pset) explorer := NewExplorer()
txid, err := explorer.Broadcast(pset)
if err != nil { if err != nil {
return err return err
} }
fmt.Println("onboard txid:", txid) fmt.Println("onboard_txid:", txid)
fmt.Println("waiting for confirmation... (this may take a while, do not cancel the process)") fmt.Println("waiting for confirmation... (this may take up to a minute, do not cancel the process)")
// wait for the transaction to be confirmed // wait for the transaction to be confirmed
if err := waitForTxConfirmation(ctx, txid); err != nil { if err := waitForTxConfirmation(ctx, txid); err != nil {
@@ -116,11 +118,11 @@ func onboardAction(ctx *cli.Context) error {
return err return err
} }
client, close, err := getClientFromState(ctx) client, cancel, err := getClientFromState()
if err != nil { if err != nil {
return err return err
} }
defer close() defer cancel()
_, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{ _, err = client.Onboard(ctx.Context, &arkv1.OnboardRequest{
BoardingTx: pset, BoardingTx: pset,

View File

@@ -11,7 +11,7 @@ var receiveCommand = cli.Command{
} }
func receiveAction(ctx *cli.Context) error { func receiveAction(ctx *cli.Context) error {
offchainAddr, onchainAddr, err := getAddress() offchainAddr, onchainAddr, _, err := getAddress()
if err != nil { if err != nil {
return err return err
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bufio" "bufio"
"context"
"fmt" "fmt"
"log" "log"
"os" "os"
@@ -56,18 +57,26 @@ func redeemAction(ctx *cli.Context) error {
return fmt.Errorf("missing amount flag (--amount)") return fmt.Errorf("missing amount flag (--amount)")
} }
client, clean, err := getClientFromState()
if err != nil {
return err
}
defer clean()
if force { if force {
if amount > 0 { if amount > 0 {
fmt.Printf("WARNING: unilateral exit (--force) ignores --amount flag, it will redeem all your VTXOs\n") fmt.Printf("WARNING: unilateral exit (--force) ignores --amount flag, it will redeem all your VTXOs\n")
} }
return unilateralRedeem(ctx) return unilateralRedeem(client, ctx.Context)
} }
return collaborativeRedeem(ctx, addr, amount) return collaborativeRedeem(client, ctx.Context, addr, amount)
} }
func collaborativeRedeem(ctx *cli.Context, addr string, amount uint64) error { func collaborativeRedeem(
client arkv1.ArkServiceClient, ctx context.Context, addr string, amount uint64,
) error {
if _, err := address.ToOutputScript(addr); err != nil { if _, err := address.ToOutputScript(addr); err != nil {
return fmt.Errorf("invalid onchain address") return fmt.Errorf("invalid onchain address")
} }
@@ -86,7 +95,7 @@ func collaborativeRedeem(ctx *cli.Context, addr string, amount uint64) error {
addr = info.Address addr = info.Address
} }
offchainAddr, _, err := getAddress() offchainAddr, _, _, err := getAddress()
if err != nil { if err != nil {
return err return err
} }
@@ -98,12 +107,6 @@ func collaborativeRedeem(ctx *cli.Context, addr string, amount uint64) error {
}, },
} }
client, close, err := getClientFromState(ctx)
if err != nil {
return err
}
defer close()
explorer := NewExplorer() explorer := NewExplorer()
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, true) vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, true)
@@ -137,14 +140,14 @@ func collaborativeRedeem(ctx *cli.Context, addr string, amount uint64) error {
return err return err
} }
registerResponse, err := client.RegisterPayment(ctx.Context, &arkv1.RegisterPaymentRequest{ registerResponse, err := client.RegisterPayment(ctx, &arkv1.RegisterPaymentRequest{
Inputs: inputs, Inputs: inputs,
}) })
if err != nil { if err != nil {
return err return err
} }
_, err = client.ClaimPayment(ctx.Context, &arkv1.ClaimPaymentRequest{ _, err = client.ClaimPayment(ctx, &arkv1.ClaimPaymentRequest{
Id: registerResponse.GetId(), Id: registerResponse.GetId(),
Outputs: receivers, Outputs: receivers,
}) })
@@ -173,14 +176,8 @@ func collaborativeRedeem(ctx *cli.Context, addr string, amount uint64) error {
return nil return nil
} }
func unilateralRedeem(ctx *cli.Context) error { func unilateralRedeem(client arkv1.ArkServiceClient, ctx context.Context) error {
client, close, err := getClientFromState(ctx) offchainAddr, _, _, err := getAddress()
if err != nil {
return err
}
defer close()
offchainAddr, _, err := getAddress()
if err != nil { if err != nil {
return err return err
} }
@@ -212,7 +209,7 @@ func unilateralRedeem(ctx *cli.Context) error {
} }
for _, branch := range redeemBranches { for _, branch := range redeemBranches {
branchTxs, err := branch.RedeemPath() branchTxs, err := branch.redeemPath()
if err != nil { if err != nil {
return err return err
} }

View File

@@ -18,7 +18,7 @@ type receiver struct {
Amount uint64 `json:"amount"` Amount uint64 `json:"amount"`
} }
func (r *receiver) IsOnchain() bool { func (r *receiver) isOnchain() bool {
_, err := address.ToOutputScript(r.To) _, err := address.ToOutputScript(r.To)
return err == nil return err == nil
} }
@@ -75,20 +75,22 @@ func sendAction(ctx *cli.Context) error {
offchainReceivers := make([]receiver, 0) offchainReceivers := make([]receiver, 0)
for _, receiver := range receiversJSON { for _, receiver := range receiversJSON {
if receiver.IsOnchain() { if receiver.isOnchain() {
onchainReceivers = append(onchainReceivers, receiver) onchainReceivers = append(onchainReceivers, receiver)
} else { } else {
offchainReceivers = append(offchainReceivers, receiver) offchainReceivers = append(offchainReceivers, receiver)
} }
} }
explorer := NewExplorer()
if len(onchainReceivers) > 0 { if len(onchainReceivers) > 0 {
pset, err := sendOnchain(ctx, onchainReceivers) pset, err := sendOnchain(ctx, onchainReceivers)
if err != nil { if err != nil {
return err return err
} }
txid, err := broadcastPset(pset) txid, err := explorer.Broadcast(pset)
if err != nil { if err != nil {
return err return err
} }
@@ -108,7 +110,7 @@ func sendAction(ctx *cli.Context) error {
} }
func sendOffchain(ctx *cli.Context, receivers []receiver) error { func sendOffchain(ctx *cli.Context, receivers []receiver) error {
offchainAddr, _, err := getAddress() offchainAddr, _, _, err := getAddress()
if err != nil { if err != nil {
return err return err
} }
@@ -127,7 +129,9 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
return fmt.Errorf("invalid receiver address: %s", err) return fmt.Errorf("invalid receiver address: %s", err)
} }
if !bytes.Equal(aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed()) { if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To) return fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To)
} }
@@ -141,7 +145,7 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
}) })
sumOfReceivers += receiver.Amount sumOfReceivers += receiver.Amount
} }
client, close, err := getClientFromState(ctx) client, close, err := getClientFromState()
if err != nil { if err != nil {
return err return err
} }
@@ -149,7 +153,7 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
explorer := NewExplorer() explorer := NewExplorer()
vtxos, err := getVtxos(ctx, explorer, client, offchainAddr, true) vtxos, err := getVtxos(ctx.Context, explorer, client, offchainAddr, true)
if err != nil { if err != nil {
return err return err
} }
@@ -181,9 +185,9 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
return err return err
} }
registerResponse, err := client.RegisterPayment(ctx.Context, &arkv1.RegisterPaymentRequest{ registerResponse, err := client.RegisterPayment(
Inputs: inputs, ctx.Context, &arkv1.RegisterPaymentRequest{Inputs: inputs},
}) )
if err != nil { if err != nil {
return err return err
} }
@@ -197,12 +201,8 @@ func sendOffchain(ctx *cli.Context, receivers []receiver) error {
} }
poolTxID, err := handleRoundStream( poolTxID, err := handleRoundStream(
ctx, ctx.Context, client, registerResponse.GetId(),
client, selectedCoins, secKey, receiversOutput,
registerResponse.GetId(),
selectedCoins,
secKey,
receiversOutput,
) )
if err != nil { if err != nil {
return err return err
@@ -248,17 +248,21 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
} }
} }
selected, delayedSelected, change, err := coinSelectOnchain(targetAmount, nil) explorer := NewExplorer()
utxos, delayedUtxos, change, err := coinSelectOnchain(
explorer, 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, utxos, delayedUtxos, 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
} }
@@ -297,8 +301,7 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
} }
// reselect the difference // reselect the difference
selected, delayedSelected, newChange, err := coinSelectOnchain( selected, delayedSelected, newChange, err := coinSelectOnchain(
feeAmount-change, explorer, feeAmount-change, append(utxos, delayedUtxos...),
append(selected, delayedSelected...),
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -309,7 +312,7 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
} }
if newChange > 0 { if newChange > 0 {
_, changeAddr, err := getAddress() _, changeAddr, _, err := getAddress()
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -345,8 +348,6 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
return "", err return "", err
} }
explorer := NewExplorer()
if err := signPset(updater.Pset, explorer, prvKey); err != nil { if err := signPset(updater.Pset, explorer, prvKey); err != nil {
return "", err return "", err
} }
@@ -357,22 +358,3 @@ func sendOnchain(ctx *cli.Context, receivers []receiver) (string, error) {
return updater.Pset.ToBase64() 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
}
hex, err := extracted.ToHex()
if err != nil {
return "", err
}
return NewExplorer().Broadcast(hex)
}

View File

@@ -17,9 +17,7 @@ import (
) )
func signPset( func signPset(
pset *psetv2.Pset, pset *psetv2.Pset, explorer Explorer, prvKey *secp256k1.PrivateKey,
explorer Explorer,
prvKey *secp256k1.PrivateKey,
) error { ) error {
updater, err := psetv2.NewUpdater(pset) updater, err := psetv2.NewUpdater(pset)
if err != nil { if err != nil {
@@ -66,7 +64,7 @@ func signPset(
return err return err
} }
_, onchainAddr, err := getAddress() _, onchainAddr, _, err := getAddress()
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,24 +1,17 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"time" "time"
"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/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v2"
"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 RedeemBranch interface {
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
RedeemPath() ([]string, error)
// ExpireAt returns the expiration time of the branch
ExpireAt() (*time.Time, error)
}
type redeemBranch struct { type redeemBranch struct {
vtxo *vtxo vtxo *vtxo
branch []*psetv2.Pset branch []*psetv2.Pset
@@ -29,11 +22,9 @@ type redeemBranch struct {
} }
func newRedeemBranch( func newRedeemBranch(
ctx *cli.Context, ctx context.Context, explorer Explorer,
explorer Explorer, congestionTree tree.CongestionTree, vtxo vtxo,
congestionTree tree.CongestionTree, ) (*redeemBranch, error) {
vtxo vtxo,
) (RedeemBranch, error) {
sweepClosure, seconds, err := findSweepClosure(congestionTree) sweepClosure, seconds, err := findSweepClosure(congestionTree)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -75,7 +66,7 @@ func newRedeemBranch(
} }
// RedeemPath returns the list of transactions to broadcast in order to access the vtxo output // RedeemPath returns the list of transactions to broadcast in order to access the vtxo output
func (r *redeemBranch) RedeemPath() ([]string, error) { func (r *redeemBranch) redeemPath() ([]string, error) {
transactions := make([]string, 0, len(r.branch)) transactions := make([]string, 0, len(r.branch))
offchainPath, err := r.offchainPath() offchainPath, err := r.offchainPath()
@@ -125,7 +116,7 @@ func (r *redeemBranch) RedeemPath() ([]string, error) {
return transactions, nil return transactions, nil
} }
func (r *redeemBranch) ExpireAt() (*time.Time, error) { func (r *redeemBranch) expireAt() (*time.Time, error) {
lastKnownBlocktime := int64(0) lastKnownBlocktime := int64(0)
confirmed, blocktime, _ := getTxBlocktime(r.vtxo.poolTxid) confirmed, blocktime, _ := getTxBlocktime(r.vtxo.poolTxid)

View File

@@ -7,7 +7,9 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
func EncodeAddress(hrp string, userKey, aspKey *secp256k1.PublicKey) (addr string, err error) { func EncodeAddress(
hrp string, userKey, aspKey *secp256k1.PublicKey,
) (addr string, err error) {
if userKey == nil { if userKey == nil {
err = fmt.Errorf("missing public key") err = fmt.Errorf("missing public key")
return return
@@ -20,7 +22,9 @@ func EncodeAddress(hrp string, userKey, aspKey *secp256k1.PublicKey) (addr strin
err = fmt.Errorf("invalid prefix") err = fmt.Errorf("invalid prefix")
return return
} }
combinedKey := append(aspKey.SerializeCompressed(), userKey.SerializeCompressed()...) combinedKey := append(
aspKey.SerializeCompressed(), userKey.SerializeCompressed()...,
)
grp, err := bech32.ConvertBits(combinedKey, 8, 5, true) grp, err := bech32.ConvertBits(combinedKey, 8, 5, true)
if err != nil { if err != nil {
return return
@@ -29,7 +33,9 @@ func EncodeAddress(hrp string, userKey, aspKey *secp256k1.PublicKey) (addr strin
return return
} }
func DecodeAddress(addr string) (hrp string, userKey *secp256k1.PublicKey, aspKey *secp256k1.PublicKey, err error) { func DecodeAddress(
addr string,
) (hrp string, userKey *secp256k1.PublicKey, aspKey *secp256k1.PublicKey, err error) {
prefix, buf, err := bech32.DecodeNoLimit(addr) prefix, buf, err := bech32.DecodeNoLimit(addr)
if err != nil { if err != nil {
return return

View File

@@ -10,9 +10,7 @@ import (
// TaprootPreimage computes the hash for witness v1 input of a pset // TaprootPreimage computes the hash for witness v1 input of a pset
// it implicitly assumes that the pset has witnessUtxo fields populated // it implicitly assumes that the pset has witnessUtxo fields populated
func TaprootPreimage( func TaprootPreimage(
genesisBlockHash *chainhash.Hash, genesisBlockHash *chainhash.Hash, pset *psetv2.Pset, inputIndex int,
pset *psetv2.Pset,
inputIndex int,
leafHash *chainhash.Hash, leafHash *chainhash.Hash,
) ([]byte, error) { ) ([]byte, error) {
prevoutScripts := make([][]byte, 0) prevoutScripts := make([][]byte, 0)
@@ -35,14 +33,8 @@ func TaprootPreimage(
} }
preimage := utx.HashForWitnessV1( preimage := utx.HashForWitnessV1(
inputIndex, inputIndex, prevoutScripts, prevoutAssets, prevoutValues,
prevoutScripts, pset.Inputs[inputIndex].SigHashType, genesisBlockHash, leafHash, nil,
prevoutAssets,
prevoutValues,
pset.Inputs[inputIndex].SigHashType,
genesisBlockHash,
leafHash,
nil,
) )
return preimage[:], nil return preimage[:], nil
} }

View File

@@ -13,14 +13,14 @@ import (
) )
func CraftCongestionTree( func CraftCongestionTree(
asset string, aspPublicKey *secp256k1.PublicKey, asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
receivers []Receiver, feeSatsPerNode uint64, roundLifetime int64, exitDelay int64, feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64,
) ( ) (
buildCongestionTree TreeFactory, buildCongestionTree TreeFactory,
sharedOutputScript []byte, sharedOutputAmount uint64, err error, sharedOutputScript []byte, sharedOutputAmount uint64, err error,
) { ) {
root, err := createPartialCongestionTree( root, err := createPartialCongestionTree(
receivers, aspPublicKey, asset, feeSatsPerNode, roundLifetime, exitDelay, asset, aspPubkey, receivers, feeSatsPerNode, roundLifetime, unilateralExitDelay,
) )
if err != nil { if err != nil {
return return
@@ -49,7 +49,7 @@ type node struct {
asset string asset string
feeSats uint64 feeSats uint64
roundLifetime int64 roundLifetime int64
exitDelay int64 unilateralExitDelay int64
_inputTaprootKey *secp256k1.PublicKey _inputTaprootKey *secp256k1.PublicKey
_inputTaprootTree *taproot.IndexedElementsTapScriptTree _inputTaprootTree *taproot.IndexedElementsTapScriptTree
@@ -260,7 +260,7 @@ func (n *node) getVtxoWitnessData() (
redeemClosure := &CSVSigClosure{ redeemClosure := &CSVSigClosure{
Pubkey: pubkey, Pubkey: pubkey,
Seconds: uint(n.exitDelay), Seconds: uint(n.unilateralExitDelay),
} }
redeemLeaf, err := redeemClosure.Leaf() redeemLeaf, err := redeemClosure.Leaf()
@@ -412,12 +412,8 @@ func (n *node) createFinalCongestionTree() TreeFactory {
} }
func createPartialCongestionTree( func createPartialCongestionTree(
receivers []Receiver, asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
aspPublicKey *secp256k1.PublicKey, feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64,
asset string,
feeSatsPerNode uint64,
roundLifetime int64,
exitDelay int64,
) (root *node, err error) { ) (root *node, err error) {
if len(receivers) == 0 { if len(receivers) == 0 {
return nil, fmt.Errorf("no receivers provided") return nil, fmt.Errorf("no receivers provided")
@@ -426,12 +422,12 @@ func createPartialCongestionTree(
nodes := make([]*node, 0, len(receivers)) nodes := make([]*node, 0, len(receivers))
for _, r := range receivers { for _, r := range receivers {
leafNode := &node{ leafNode := &node{
sweepKey: aspPublicKey, sweepKey: aspPubkey,
receivers: []Receiver{r}, receivers: []Receiver{r},
asset: asset, asset: asset,
feeSats: feeSatsPerNode, feeSats: feeSatsPerNode,
roundLifetime: roundLifetime, roundLifetime: roundLifetime,
exitDelay: exitDelay, unilateralExitDelay: unilateralExitDelay,
} }
nodes = append(nodes, leafNode) nodes = append(nodes, leafNode)
} }
@@ -476,7 +472,9 @@ func createUpperLevel(nodes []*node) ([]*node, error) {
} }
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()
} }
func getPsetId(pset *psetv2.Pset) (string, error) { func getPsetId(pset *psetv2.Pset) (string, error) {
@@ -488,10 +486,8 @@ func getPsetId(pset *psetv2.Pset) (string, error) {
return utx.TxHash().String(), nil 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( func addTaprootInput(
updater *psetv2.Updater, updater *psetv2.Updater, input psetv2.InputArgs,
input psetv2.InputArgs,
internalTaprootKey *secp256k1.PublicKey, internalTaprootKey *secp256k1.PublicKey,
taprootTree *taproot.IndexedElementsTapScriptTree, taprootTree *taproot.IndexedElementsTapScriptTree,
) error { ) error {
@@ -499,7 +495,9 @@ func addTaprootInput(
return err return err
} }
if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil { if err := updater.AddInTapInternalKey(
0, schnorr.SerializePubKey(internalTaprootKey),
); err != nil {
return err return err
} }

View File

@@ -64,7 +64,9 @@ func (f *ForfeitClosure) Leaf() (*taproot.TapElementsLeaf, error) {
aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey) aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey)
userKeyBytes := schnorr.SerializePubKey(f.Pubkey) userKeyBytes := schnorr.SerializePubKey(f.Pubkey)
script, err := txscript.NewScriptBuilder().AddData(aspKeyBytes).AddOp(txscript.OP_CHECKSIGVERIFY).AddData(userKeyBytes).AddOp(txscript.OP_CHECKSIG).Script() script, err := txscript.NewScriptBuilder().AddData(aspKeyBytes).
AddOp(txscript.OP_CHECKSIGVERIFY).AddData(userKeyBytes).
AddOp(txscript.OP_CHECKSIG).Script()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -108,7 +110,7 @@ func (f *ForfeitClosure) Decode(script []byte) (bool, error) {
} }
func (d *CSVSigClosure) Leaf() (*taproot.TapElementsLeaf, error) { func (d *CSVSigClosure) Leaf() (*taproot.TapElementsLeaf, error) {
script, err := csvChecksigScript(d.Pubkey, d.Seconds) script, err := encodeCsvWithChecksigScript(d.Pubkey, d.Seconds)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -118,7 +120,9 @@ func (d *CSVSigClosure) Leaf() (*taproot.TapElementsLeaf, error) {
} }
func (d *CSVSigClosure) Decode(script []byte) (bool, error) { func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
csvIndex := bytes.Index(script, []byte{txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_DROP}) csvIndex := bytes.Index(
script, []byte{txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_DROP},
)
if csvIndex == -1 || csvIndex == 0 { if csvIndex == -1 || csvIndex == 0 {
return false, nil return false, nil
} }
@@ -140,7 +144,7 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
return false, nil return false, nil
} }
rebuilt, err := csvChecksigScript(pubkey, seconds) rebuilt, err := encodeCsvWithChecksigScript(pubkey, seconds)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -160,14 +164,19 @@ func (c *UnrollClosure) Leaf() (*taproot.TapElementsLeaf, error) {
return nil, fmt.Errorf("left key and amount are required") return nil, fmt.Errorf("left key and amount are required")
} }
nextScriptLeft := withOutput(txscript.OP_0, schnorr.SerializePubKey(c.LeftKey), c.LeftAmount, c.RightKey != nil) nextScriptLeft := encodeIntrospectionScript(
txscript.OP_0,
schnorr.SerializePubKey(c.LeftKey), c.LeftAmount, c.RightKey != nil,
)
branchScript := append([]byte{}, nextScriptLeft...) branchScript := append([]byte{}, nextScriptLeft...)
if c.RightKey != nil { if c.RightKey != nil {
if c.RightAmount == 0 { if c.RightAmount == 0 {
return nil, fmt.Errorf("right amount is required") return nil, fmt.Errorf("right amount is required")
} }
nextScriptRight := withOutput(txscript.OP_1, schnorr.SerializePubKey(c.RightKey), c.RightAmount, false) nextScriptRight := encodeIntrospectionScript(
txscript.OP_1, schnorr.SerializePubKey(c.RightKey), c.RightAmount, false,
)
branchScript = append(branchScript, nextScriptRight...) branchScript = append(branchScript, nextScriptRight...)
} }
leaf := taproot.NewBaseTapElementsLeaf(branchScript) leaf := taproot.NewBaseTapElementsLeaf(branchScript)
@@ -181,7 +190,9 @@ func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
isLeftOnly := len(script) == 52 isLeftOnly := len(script) == 52
validLeft, leftKey, leftAmount, err := decodeWithOutputScript(script[:52], txscript.OP_0, !isLeftOnly) validLeft, leftKey, leftAmount, err := decodeIntrospectionScript(
script[:52], txscript.OP_0, !isLeftOnly,
)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -197,7 +208,9 @@ func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
return true, nil return true, nil
} }
validRight, rightKey, rightAmount, err := decodeWithOutputScript(script[52:], txscript.OP_1, false) validRight, rightKey, rightAmount, err := decodeIntrospectionScript(
script[52:], txscript.OP_1, false,
)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -221,7 +234,9 @@ func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
return true, nil return true, nil
} }
func decodeWithOutputScript(script []byte, expectedIndex byte, isVerify bool) (valid bool, pubkey *secp256k1.PublicKey, amount uint64, err error) { func decodeIntrospectionScript(
script []byte, expectedIndex byte, isVerify bool,
) (bool, *secp256k1.PublicKey, uint64, error) {
if len(script) != 52 { if len(script) != 52 {
return false, nil, 0, nil return false, nil, 0, nil
} }
@@ -231,7 +246,7 @@ func decodeWithOutputScript(script []byte, expectedIndex byte, isVerify bool) (v
} }
// 32 bytes for the witness program // 32 bytes for the witness program
pubkey, err = schnorr.ParsePubKey(script[5 : 5+32]) pubkey, err := schnorr.ParsePubKey(script[5 : 5+32])
if err != nil { if err != nil {
return false, nil, 0, err return false, nil, 0, err
} }
@@ -243,9 +258,11 @@ func decodeWithOutputScript(script []byte, expectedIndex byte, isVerify bool) (v
// 8 bytes for the amount // 8 bytes for the amount
amountBytes := script[len(script)-9 : len(script)-1] amountBytes := script[len(script)-9 : len(script)-1]
amount = binary.LittleEndian.Uint64(amountBytes) amount := binary.LittleEndian.Uint64(amountBytes)
rebuilt := withOutput(expectedIndex, schnorr.SerializePubKey(pubkey), amount, isVerify) rebuilt := encodeIntrospectionScript(
expectedIndex, schnorr.SerializePubKey(pubkey), amount, isVerify,
)
if !bytes.Equal(rebuilt, script) { if !bytes.Equal(rebuilt, script) {
return false, nil, 0, nil return false, nil, 0, nil
} }
@@ -253,7 +270,7 @@ func decodeWithOutputScript(script []byte, expectedIndex byte, isVerify bool) (v
return true, pubkey, amount, nil return true, pubkey, amount, nil
} }
func decodeChecksigScript(script []byte) (valid bool, pubkey *secp256k1.PublicKey, err error) { func decodeChecksigScript(script []byte) (bool, *secp256k1.PublicKey, error) {
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32}) data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
if data32Index == -1 { if data32Index == -1 {
return false, nil, nil return false, nil, nil
@@ -264,7 +281,7 @@ func decodeChecksigScript(script []byte) (valid bool, pubkey *secp256k1.PublicKe
return false, nil, nil return false, nil, nil
} }
pubkey, err = schnorr.ParsePubKey(key) pubkey, err := schnorr.ParsePubKey(key)
if err != nil { if err != nil {
return false, nil, err return false, nil, err
} }
@@ -273,7 +290,7 @@ func decodeChecksigScript(script []byte) (valid bool, pubkey *secp256k1.PublicKe
} }
// checkSequenceVerifyScript without checksig // checkSequenceVerifyScript without checksig
func checkSequenceVerifyScript(seconds uint) ([]byte, error) { func encodeCsvScript(seconds uint) ([]byte, error) {
sequence, err := common.BIP68Encode(seconds) sequence, err := common.BIP68Encode(seconds)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -286,13 +303,15 @@ func checkSequenceVerifyScript(seconds uint) ([]byte, error) {
} }
// checkSequenceVerifyScript + checksig // checkSequenceVerifyScript + checksig
func csvChecksigScript(pubkey *secp256k1.PublicKey, seconds uint) ([]byte, error) { func encodeCsvWithChecksigScript(
script, err := checksigScript(pubkey) pubkey *secp256k1.PublicKey, seconds uint,
) ([]byte, error) {
script, err := encodeChecksigScript(pubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
csvScript, err := checkSequenceVerifyScript(seconds) csvScript, err := encodeCsvScript(seconds)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -300,15 +319,19 @@ func csvChecksigScript(pubkey *secp256k1.PublicKey, seconds uint) ([]byte, error
return append(csvScript, script...), nil return append(csvScript, script...), nil
} }
func checksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) { func encodeChecksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) {
key := schnorr.SerializePubKey(pubkey) key := schnorr.SerializePubKey(pubkey)
return txscript.NewScriptBuilder().AddData(key).AddOp(txscript.OP_CHECKSIG).Script() return txscript.NewScriptBuilder().AddData(key).
AddOp(txscript.OP_CHECKSIG).Script()
} }
// withOutput returns an introspection script that checks the script and the amount of the output at the given index // getIntrospectionScript returns an introspection script that checks the
// verify will add an OP_EQUALVERIFY at the end of the script, otherwise it will add an OP_EQUAL // script and the amount of the output at the given index verify will add an
// OP_EQUALVERIFY at the end of the script, otherwise it will add an OP_EQUAL
// length = 52 bytes // length = 52 bytes
func withOutput(index byte, taprootWitnessProgram []byte, amount uint64, verify bool) []byte { func encodeIntrospectionScript(
index byte, taprootWitnessProgram []byte, amount uint64, verify bool,
) []byte {
amountBuffer := make([]byte, 8) amountBuffer := make([]byte, 8)
binary.LittleEndian.PutUint64(amountBuffer, amount) binary.LittleEndian.PutUint64(amountBuffer, amount)

View File

@@ -63,7 +63,7 @@ func UnspendableKey() *secp256k1.PublicKey {
// ValidateCongestionTree checks if the given congestion tree is valid // ValidateCongestionTree checks if the given congestion tree is valid
// poolTxID & poolTxIndex & poolTxAmount are used to validate the root input outpoint // poolTxID & poolTxIndex & poolTxAmount are used to validate the root input outpoint
// aspPublicKey & roundLifetimeSeconds are used to validate the sweep tapscript leaves // aspPublicKey & roundLifetime are used to validate the sweep tapscript leaves
// besides that, the function validates: // besides that, the function validates:
// - the number of nodes // - the number of nodes
// - the number of leaves // - the number of leaves
@@ -71,10 +71,8 @@ func UnspendableKey() *secp256k1.PublicKey {
// - every control block and taproot output scripts // - every control block and taproot output scripts
// - input and output amounts // - input and output amounts
func ValidateCongestionTree( func ValidateCongestionTree(
tree CongestionTree, tree CongestionTree, poolTx string, aspPublicKey *secp256k1.PublicKey,
poolTx string, roundLifetime int64,
aspPublicKey *secp256k1.PublicKey,
roundLifetimeSeconds int64,
) error { ) error {
poolTransaction, err := psetv2.NewPsetFromBase64(poolTx) poolTransaction, err := psetv2.NewPsetFromBase64(poolTx)
if err != nil { if err != nil {
@@ -115,7 +113,8 @@ func ValidateCongestionTree(
} }
rootInput := rootPset.Inputs[0] rootInput := rootPset.Inputs[0]
if chainhash.Hash(rootInput.PreviousTxid).String() != poolTxID || rootInput.PreviousTxIndex != sharedOutputIndex { if chainhash.Hash(rootInput.PreviousTxid).String() != poolTxID ||
rootInput.PreviousTxIndex != sharedOutputIndex {
return ErrWrongPoolTxID return ErrWrongPoolTxID
} }
@@ -135,7 +134,9 @@ func ValidateCongestionTree(
// iterates over all the nodes of the tree // iterates over all the nodes of the tree
for _, level := range tree { for _, level := range tree {
for _, node := range level { for _, node := range level {
if err := validateNodeTransaction(node, tree, UnspendableKey(), aspPublicKey, roundLifetimeSeconds); err != nil { if err := validateNodeTransaction(
node, tree, UnspendableKey(), aspPublicKey, roundLifetime,
); err != nil {
return err return err
} }
} }
@@ -145,11 +146,9 @@ func ValidateCongestionTree(
} }
func validateNodeTransaction( func validateNodeTransaction(
node Node, node Node, tree CongestionTree,
tree CongestionTree, expectedInternalKey, expectedPublicKeyASP *secp256k1.PublicKey,
expectedInternalKey, expectedSequence int64,
expectedPublicKeyASP *secp256k1.PublicKey,
expectedSequenceSeconds int64,
) error { ) error {
if node.Tx == "" { if node.Tx == "" {
return ErrNodeTransactionEmpty return ErrNodeTransactionEmpty
@@ -186,7 +185,8 @@ func validateNodeTransaction(
return ErrNumberOfTapscripts return ErrNumberOfTapscripts
} }
if chainhash.Hash(decodedPset.Inputs[0].PreviousTxid).String() != node.ParentTxid { prevTxid := chainhash.Hash(decodedPset.Inputs[0].PreviousTxid).String()
if prevTxid != node.ParentTxid {
return ErrParentTxidInput return ErrParentTxidInput
} }
@@ -225,19 +225,24 @@ func validateNodeTransaction(
rootHash := tapLeaf.ControlBlock.RootHash(tapLeaf.Script) rootHash := tapLeaf.ControlBlock.RootHash(tapLeaf.Script)
outputScript := taproot.ComputeTaprootOutputKey(key, rootHash) outputScript := taproot.ComputeTaprootOutputKey(key, rootHash)
if !bytes.Equal(schnorr.SerializePubKey(outputScript), previousScriptKey) { if !bytes.Equal(
schnorr.SerializePubKey(outputScript), previousScriptKey,
) {
return ErrInvalidTaprootScript return ErrInvalidTaprootScript
} }
close, err := DecodeClosure(tapLeaf.Script) closure, err := DecodeClosure(tapLeaf.Script)
if err != nil { if err != nil {
continue continue
} }
switch c := close.(type) { switch c := closure.(type) {
case *CSVSigClosure: case *CSVSigClosure:
isASP := bytes.Equal(schnorr.SerializePubKey(c.Pubkey), schnorr.SerializePubKey(expectedPublicKeyASP)) isASP := bytes.Equal(
isSweepDelay := int64(c.Seconds) == expectedSequenceSeconds schnorr.SerializePubKey(c.Pubkey),
schnorr.SerializePubKey(expectedPublicKeyASP),
)
isSweepDelay := int64(c.Seconds) == expectedSequence
if isASP && !isSweepDelay { if isASP && !isSweepDelay {
return ErrInvalidSweepSequence return ErrInvalidSweepSequence
@@ -268,7 +273,9 @@ func validateNodeTransaction(
leftWitnessProgram := childTx.Outputs[0].Script[2:] leftWitnessProgram := childTx.Outputs[0].Script[2:]
leftOutputAmount := childTx.Outputs[0].Value leftOutputAmount := childTx.Outputs[0].Value
if !bytes.Equal(leftWitnessProgram, schnorr.SerializePubKey(c.LeftKey)) { if !bytes.Equal(
leftWitnessProgram, schnorr.SerializePubKey(c.LeftKey),
) {
return ErrInvalidLeftOutput return ErrInvalidLeftOutput
} }
@@ -280,7 +287,9 @@ func validateNodeTransaction(
rightWitnessProgram := childTx.Outputs[1].Script[2:] rightWitnessProgram := childTx.Outputs[1].Script[2:]
rightOutputAmount := childTx.Outputs[1].Value rightOutputAmount := childTx.Outputs[1].Value
if !bytes.Equal(rightWitnessProgram, schnorr.SerializePubKey(c.RightKey)) { if !bytes.Equal(
rightWitnessProgram, schnorr.SerializePubKey(c.RightKey),
) {
return ErrInvalidRightOutput return ErrInvalidRightOutput
} }

View File

@@ -372,11 +372,11 @@
"pubkey": { "pubkey": {
"type": "string" "type": "string"
}, },
"lifetime": { "roundLifetime": {
"type": "string", "type": "string",
"format": "int64" "format": "int64"
}, },
"exitDelay": { "unilateralExitDelay": {
"type": "string", "type": "string",
"format": "int64" "format": "int64"
} }

View File

@@ -169,6 +169,15 @@
} }
} }
}, },
"v1LockUtxosResponse": {
"type": "object",
"properties": {
"expirationDate": {
"type": "string",
"format": "int64"
}
}
},
"v1MintResponse": { "v1MintResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -56,15 +56,6 @@ service ArkService {
} }
} }
message OnboardRequest {
string boarding_tx = 1;
Tree congestion_tree = 2;
string user_pubkey = 3;
}
message OnboardResponse {
}
message RegisterPaymentRequest { message RegisterPaymentRequest {
repeated Input inputs = 1; repeated Input inputs = 1;
} }
@@ -94,6 +85,63 @@ message GetRoundResponse {
Round round = 1; Round round = 1;
} }
message GetEventStreamRequest {}
message GetEventStreamResponse {
oneof event {
RoundFinalizationEvent round_finalization = 1;
RoundFinalizedEvent round_finalized = 2;
RoundFailed round_failed = 3;
}
}
message PingRequest {
string payment_id = 1;
}
message PingResponse {}
message ListVtxosRequest {
string address = 1;
}
message ListVtxosResponse {
repeated Vtxo vtxos = 1;
}
message GetInfoRequest {}
message GetInfoResponse {
string pubkey = 1;
int64 round_lifetime = 2;
int64 unilateral_exit_delay = 3;
}
message OnboardRequest {
string boarding_tx = 1;
Tree congestion_tree = 2;
string user_pubkey = 3;
}
message OnboardResponse {
}
// EVENT TYPES
message RoundFinalizationEvent {
string id = 1;
string pool_partial_tx = 2;
repeated string forfeit_txs = 3;
Tree congestion_tree = 4;
}
message RoundFinalizedEvent {
string id = 1;
string pool_txid = 2;
}
message RoundFailed {
string id = 1;
string reason = 2;
}
// TYPES
message Round { message Round {
string id = 1; string id = 1;
int64 start = 2; int64 start = 2;
@@ -114,13 +162,6 @@ message Output {
uint64 amount = 2; uint64 amount = 2;
} }
message RoundFinalizationEvent {
string id = 1;
string pool_partial_tx = 2;
repeated string forfeit_txs = 3;
Tree congestion_tree = 4;
}
message Tree { message Tree {
repeated TreeLevel levels = 1; repeated TreeLevel levels = 1;
} }
@@ -135,51 +176,9 @@ message Node {
string parent_txid = 3; string parent_txid = 3;
} }
message RoundFinalizedEvent {
string id = 1;
string pool_txid = 2;
}
message RoundFailed {
string id = 1;
string reason = 2;
}
message GetEventStreamRequest {}
message GetEventStreamResponse {
oneof event {
RoundFinalizationEvent round_finalization = 1;
RoundFinalizedEvent round_finalized = 2;
RoundFailed round_failed = 3;
}
}
message PingRequest {
string payment_id = 1;
}
message PingResponse {}
message ListVtxosRequest {
string address = 1;
}
message ListVtxosResponse {
repeated Vtxo vtxos = 1;
}
message Vtxo { message Vtxo {
Input outpoint = 1; Input outpoint = 1;
Output receiver = 2; Output receiver = 2;
bool spent = 3; bool spent = 3;
string pool_txid = 4; string pool_txid = 4;
} }
message GetInfoRequest {}
message GetInfoResponse {
string pubkey = 1;
int64 lifetime = 2;
int64 exit_delay = 3;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,8 @@ type TransactionServiceClient interface {
// Selected utxos are locked for predefined amount of time to prevent // Selected utxos are locked for predefined amount of time to prevent
// double-spending them. // double-spending them.
SelectUtxos(ctx context.Context, in *SelectUtxosRequest, opts ...grpc.CallOption) (*SelectUtxosResponse, error) SelectUtxos(ctx context.Context, in *SelectUtxosRequest, opts ...grpc.CallOption) (*SelectUtxosResponse, error)
// LockUtxos allows to manually select utxos to spend by a subsequent tx.
LockUtxos(ctx context.Context, in *LockUtxosRequest, opts ...grpc.CallOption) (*LockUtxosResponse, error)
// EstimateFees returns the fee amount to pay for a tx containing the given // EstimateFees returns the fee amount to pay for a tx containing the given
// inputs and outputs. // inputs and outputs.
EstimateFees(ctx context.Context, in *EstimateFeesRequest, opts ...grpc.CallOption) (*EstimateFeesResponse, error) EstimateFees(ctx context.Context, in *EstimateFeesRequest, opts ...grpc.CallOption) (*EstimateFeesResponse, error)
@@ -87,6 +89,15 @@ func (c *transactionServiceClient) SelectUtxos(ctx context.Context, in *SelectUt
return out, nil return out, nil
} }
func (c *transactionServiceClient) LockUtxos(ctx context.Context, in *LockUtxosRequest, opts ...grpc.CallOption) (*LockUtxosResponse, error) {
out := new(LockUtxosResponse)
err := c.cc.Invoke(ctx, "/ocean.v1.TransactionService/LockUtxos", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *transactionServiceClient) EstimateFees(ctx context.Context, in *EstimateFeesRequest, opts ...grpc.CallOption) (*EstimateFeesResponse, error) { func (c *transactionServiceClient) EstimateFees(ctx context.Context, in *EstimateFeesRequest, opts ...grpc.CallOption) (*EstimateFeesResponse, error) {
out := new(EstimateFeesResponse) out := new(EstimateFeesResponse)
err := c.cc.Invoke(ctx, "/ocean.v1.TransactionService/EstimateFees", in, out, opts...) err := c.cc.Invoke(ctx, "/ocean.v1.TransactionService/EstimateFees", in, out, opts...)
@@ -224,6 +235,8 @@ type TransactionServiceServer interface {
// Selected utxos are locked for predefined amount of time to prevent // Selected utxos are locked for predefined amount of time to prevent
// double-spending them. // double-spending them.
SelectUtxos(context.Context, *SelectUtxosRequest) (*SelectUtxosResponse, error) SelectUtxos(context.Context, *SelectUtxosRequest) (*SelectUtxosResponse, error)
// LockUtxos allows to manually select utxos to spend by a subsequent tx.
LockUtxos(context.Context, *LockUtxosRequest) (*LockUtxosResponse, error)
// EstimateFees returns the fee amount to pay for a tx containing the given // EstimateFees returns the fee amount to pay for a tx containing the given
// inputs and outputs. // inputs and outputs.
EstimateFees(context.Context, *EstimateFeesRequest) (*EstimateFeesResponse, error) EstimateFees(context.Context, *EstimateFeesRequest) (*EstimateFeesResponse, error)
@@ -270,6 +283,9 @@ func (UnimplementedTransactionServiceServer) GetTransaction(context.Context, *Ge
func (UnimplementedTransactionServiceServer) SelectUtxos(context.Context, *SelectUtxosRequest) (*SelectUtxosResponse, error) { func (UnimplementedTransactionServiceServer) SelectUtxos(context.Context, *SelectUtxosRequest) (*SelectUtxosResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SelectUtxos not implemented") return nil, status.Errorf(codes.Unimplemented, "method SelectUtxos not implemented")
} }
func (UnimplementedTransactionServiceServer) LockUtxos(context.Context, *LockUtxosRequest) (*LockUtxosResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method LockUtxos not implemented")
}
func (UnimplementedTransactionServiceServer) EstimateFees(context.Context, *EstimateFeesRequest) (*EstimateFeesResponse, error) { func (UnimplementedTransactionServiceServer) EstimateFees(context.Context, *EstimateFeesRequest) (*EstimateFeesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method EstimateFees not implemented") return nil, status.Errorf(codes.Unimplemented, "method EstimateFees not implemented")
} }
@@ -360,6 +376,24 @@ func _TransactionService_SelectUtxos_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _TransactionService_LockUtxos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LockUtxosRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TransactionServiceServer).LockUtxos(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ocean.v1.TransactionService/LockUtxos",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TransactionServiceServer).LockUtxos(ctx, req.(*LockUtxosRequest))
}
return interceptor(ctx, in, info, handler)
}
func _TransactionService_EstimateFees_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _TransactionService_EstimateFees_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EstimateFeesRequest) in := new(EstimateFeesRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -627,6 +661,10 @@ var TransactionService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SelectUtxos", MethodName: "SelectUtxos",
Handler: _TransactionService_SelectUtxos_Handler, Handler: _TransactionService_SelectUtxos_Handler,
}, },
{
MethodName: "LockUtxos",
Handler: _TransactionService_LockUtxos_Handler,
},
{ {
MethodName: "EstimateFees", MethodName: "EstimateFees",
Handler: _TransactionService_EstimateFees_Handler, Handler: _TransactionService_EstimateFees_Handler,

View File

@@ -31,18 +31,6 @@ func main() {
NoTLS: cfg.NoTLS, NoTLS: cfg.NoTLS,
} }
if cfg.RoundLifetime%512 != 0 {
setLifetime := cfg.RoundLifetime
cfg.RoundLifetime = cfg.RoundLifetime - (cfg.RoundLifetime % 512)
log.Infof("round lifetime must be a multiple of 512, %d -> %d", setLifetime, cfg.RoundLifetime)
}
if cfg.ExitDelay%512 != 0 {
setExitDelay := cfg.ExitDelay
cfg.ExitDelay = cfg.ExitDelay - (cfg.ExitDelay % 512)
log.Infof("exit delay must be a multiple of 512, %d -> %d", setExitDelay, cfg.ExitDelay)
}
appConfig := &appconfig.Config{ appConfig := &appconfig.Config{
DbType: cfg.DbType, DbType: cfg.DbType,
DbDir: cfg.DbDir, DbDir: cfg.DbDir,
@@ -54,7 +42,7 @@ func main() {
WalletAddr: cfg.WalletAddr, WalletAddr: cfg.WalletAddr,
MinRelayFee: cfg.MinRelayFee, MinRelayFee: cfg.MinRelayFee,
RoundLifetime: cfg.RoundLifetime, RoundLifetime: cfg.RoundLifetime,
ExitDelay: cfg.ExitDelay, UnilateralExitDelay: cfg.UnilateralExitDelay,
} }
svc, err := grpcservice.NewService(svcConfig, appConfig) svc, err := grpcservice.NewService(svcConfig, appConfig)
if err != nil { if err != nil {

View File

@@ -15,6 +15,8 @@ import (
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
) )
const minAllowedSequence = 512
var ( var (
supportedDbs = supportedType{ supportedDbs = supportedType{
"badger": {}, "badger": {},
@@ -41,7 +43,7 @@ type Config struct {
WalletAddr string WalletAddr string
MinRelayFee uint64 MinRelayFee uint64
RoundLifetime int64 RoundLifetime int64
ExitDelay int64 UnilateralExitDelay int64
repo ports.RepoManager repo ports.RepoManager
svc application.Service svc application.Service
@@ -95,25 +97,32 @@ func (c *Config) Validate() error {
return err return err
} }
// round life time must be a multiple of 512 // round life time must be a multiple of 512
if c.RoundLifetime < 512 || c.RoundLifetime%512 != 0 { if c.RoundLifetime < minAllowedSequence {
return fmt.Errorf("invalid round lifetime, must be greater or equal than 512 and a multiple of 512") return fmt.Errorf(
} "invalid round lifetime, must be a at least %d", minAllowedSequence,
seq, err := common.BIP68Encode(uint(c.RoundLifetime)) )
if err != nil {
return fmt.Errorf("invalid round lifetime, %s", err)
} }
seconds, err := common.BIP68Decode(seq) if c.UnilateralExitDelay < minAllowedSequence {
if err != nil { return fmt.Errorf(
return fmt.Errorf("invalid round lifetime, %s", err) "invalid unilateral exit delay, must at least %d", minAllowedSequence,
)
} }
if seconds != uint(c.RoundLifetime) { if c.RoundLifetime%minAllowedSequence != 0 {
return fmt.Errorf("invalid round lifetime, must be a multiple of 512") c.RoundLifetime -= c.RoundLifetime % minAllowedSequence
log.Infof(
"round lifetime must be a multiple of %d, rounded to %d",
minAllowedSequence, c.RoundLifetime,
)
} }
if c.ExitDelay < 512 || c.ExitDelay%512 != 0 { if c.UnilateralExitDelay%minAllowedSequence != 0 {
return fmt.Errorf("invalid exit delay, must be greater or equal than 512 and a multiple of 512") c.UnilateralExitDelay -= c.UnilateralExitDelay % minAllowedSequence
log.Infof(
"unilateral exit delay must be a multiple of %d, rounded to %d",
minAllowedSequence, c.UnilateralExitDelay,
)
} }
return nil return nil
@@ -166,7 +175,9 @@ func (c *Config) txBuilderService() error {
switch c.TxBuilderType { switch c.TxBuilderType {
case "covenant": case "covenant":
svc = txbuilder.NewTxBuilder(c.wallet, net, c.RoundLifetime, c.ExitDelay) svc = txbuilder.NewTxBuilder(
c.wallet, net, c.RoundLifetime, c.UnilateralExitDelay,
)
default: default:
err = fmt.Errorf("unknown tx builder type") err = fmt.Errorf("unknown tx builder type")
} }
@@ -215,7 +226,8 @@ func (c *Config) schedulerService() error {
func (c *Config) appService() error { func (c *Config) appService() error {
net := c.mainChain() net := c.mainChain()
svc, err := application.NewService( svc, err := application.NewService(
c.Network, net, c.RoundInterval, c.RoundLifetime, c.ExitDelay, c.MinRelayFee, c.Network, net,
c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay, c.MinRelayFee,
c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
) )
if err != nil { if err != nil {

View File

@@ -24,7 +24,7 @@ type Config struct {
LogLevel int LogLevel int
MinRelayFee uint64 MinRelayFee uint64
RoundLifetime int64 RoundLifetime int64
ExitDelay int64 UnilateralExitDelay int64
} }
var ( var (
@@ -41,7 +41,7 @@ var (
Network = "NETWORK" Network = "NETWORK"
MinRelayFee = "MIN_RELAY_FEE" MinRelayFee = "MIN_RELAY_FEE"
RoundLifetime = "ROUND_LIFETIME" RoundLifetime = "ROUND_LIFETIME"
ExitDelay = "EXIT_DELAY" UnilateralExitDelay = "UNILATERAL_EXIT_DELAY"
defaultDatadir = common.AppDataDir("arkd", false) defaultDatadir = common.AppDataDir("arkd", false)
defaultRoundInterval = 10 defaultRoundInterval = 10
@@ -55,7 +55,7 @@ var (
defaultLogLevel = 5 defaultLogLevel = 5
defaultMinRelayFee = 30 defaultMinRelayFee = 30
defaultRoundLifetime = 512 defaultRoundLifetime = 512
defaultExitDelay = 512 defaultUnilateralExitDelay = 512
) )
func LoadConfig() (*Config, error) { func LoadConfig() (*Config, error) {
@@ -74,7 +74,7 @@ func LoadConfig() (*Config, error) {
viper.SetDefault(Network, defaultNetwork) viper.SetDefault(Network, defaultNetwork)
viper.SetDefault(RoundLifetime, defaultRoundLifetime) viper.SetDefault(RoundLifetime, defaultRoundLifetime)
viper.SetDefault(MinRelayFee, defaultMinRelayFee) viper.SetDefault(MinRelayFee, defaultMinRelayFee)
viper.SetDefault(ExitDelay, defaultExitDelay) viper.SetDefault(UnilateralExitDelay, defaultUnilateralExitDelay)
net, err := getNetwork() net, err := getNetwork()
if err != nil { if err != nil {
@@ -99,7 +99,7 @@ func LoadConfig() (*Config, error) {
Network: net, Network: net,
MinRelayFee: viper.GetUint64(MinRelayFee), MinRelayFee: viper.GetUint64(MinRelayFee),
RoundLifetime: viper.GetInt64(RoundLifetime), RoundLifetime: viper.GetInt64(RoundLifetime),
ExitDelay: viper.GetInt64(ExitDelay), UnilateralExitDelay: viper.GetInt64(UnilateralExitDelay),
}, nil }, nil
} }

View File

@@ -43,8 +43,8 @@ type service struct {
pubkey *secp256k1.PublicKey pubkey *secp256k1.PublicKey
roundLifetime int64 roundLifetime int64
roundInterval int64 roundInterval int64
unilateralExitDelay int64
minRelayFee uint64 minRelayFee uint64
exitDelay int64
wallet ports.WalletService wallet ports.WalletService
repoManager ports.RepoManager repoManager ports.RepoManager
@@ -60,7 +60,7 @@ type service struct {
func NewService( func NewService(
network common.Network, onchainNetwork network.Network, network common.Network, onchainNetwork network.Network,
roundInterval, roundLifetime int64, exitDelay int64, minRelayFee uint64, roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64,
walletSvc ports.WalletService, repoManager ports.RepoManager, walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner, builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService, scheduler ports.SchedulerService,
@@ -79,7 +79,7 @@ func NewService(
svc := &service{ svc := &service{
network, onchainNetwork, pubkey, network, onchainNetwork, pubkey,
roundLifetime, roundInterval, minRelayFee, exitDelay, roundLifetime, roundInterval, unilateralExitDelay, minRelayFee,
walletSvc, repoManager, builder, scanner, sweeper, walletSvc, repoManager, builder, scanner, sweeper,
paymentRequests, forfeitTxs, eventsCh, paymentRequests, forfeitTxs, eventsCh,
} }
@@ -181,7 +181,8 @@ func (s *service) GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.
} }
func (s *service) GetInfo(ctx context.Context) (string, int64, int64, error) { func (s *service) GetInfo(ctx context.Context) (string, int64, int64, error) {
return hex.EncodeToString(s.pubkey.SerializeCompressed()), s.roundLifetime, s.exitDelay, nil pubkey := hex.EncodeToString(s.pubkey.SerializeCompressed())
return pubkey, s.roundLifetime, s.unilateralExitDelay, nil
} }
func (s *service) Onboard( func (s *service) Onboard(

View File

@@ -23,7 +23,7 @@ const (
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6" testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
minRelayFee = uint64(30) minRelayFee = uint64(30)
roundLifetime = int64(1209344) roundLifetime = int64(1209344)
exitDelay = int64(512) unilateralExitDelay = int64(512)
) )
var ( var (
@@ -45,7 +45,9 @@ func TestMain(m *testing.M) {
} }
func TestBuildPoolTx(t *testing.T) { func TestBuildPoolTx(t *testing.T) {
builder := txbuilder.NewTxBuilder(wallet, network.Liquid, roundLifetime, exitDelay) builder := txbuilder.NewTxBuilder(
wallet, network.Liquid, roundLifetime, unilateralExitDelay,
)
fixtures, err := parsePoolTxFixtures() fixtures, err := parsePoolTxFixtures()
require.NoError(t, err) require.NoError(t, err)
@@ -54,14 +56,18 @@ func TestBuildPoolTx(t *testing.T) {
if len(fixtures.Valid) > 0 { if len(fixtures.Valid) > 0 {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid { for _, f := range fixtures.Valid {
poolTx, congestionTree, err := builder.BuildPoolTx(pubkey, f.Payments, minRelayFee) poolTx, congestionTree, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee,
)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, poolTx) require.NotEmpty(t, poolTx)
require.NotEmpty(t, congestionTree) require.NotEmpty(t, congestionTree)
require.Equal(t, f.ExpectedNumOfNodes, congestionTree.NumberOfNodes()) require.Equal(t, f.ExpectedNumOfNodes, congestionTree.NumberOfNodes())
require.Len(t, congestionTree.Leaves(), f.ExpectedNumOfLeaves) require.Len(t, congestionTree.Leaves(), f.ExpectedNumOfLeaves)
err = tree.ValidateCongestionTree(congestionTree, poolTx, pubkey, roundLifetime) err = tree.ValidateCongestionTree(
congestionTree, poolTx, pubkey, roundLifetime,
)
require.NoError(t, err) require.NoError(t, err)
} }
}) })
@@ -70,7 +76,9 @@ func TestBuildPoolTx(t *testing.T) {
if len(fixtures.Invalid) > 0 { if len(fixtures.Invalid) > 0 {
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid { for _, f := range fixtures.Invalid {
poolTx, congestionTree, err := builder.BuildPoolTx(pubkey, f.Payments, minRelayFee) poolTx, congestionTree, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee,
)
require.EqualError(t, err, f.ExpectedErr) require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, poolTx) require.Empty(t, poolTx)
require.Empty(t, congestionTree) require.Empty(t, congestionTree)
@@ -80,7 +88,9 @@ func TestBuildPoolTx(t *testing.T) {
} }
func TestBuildForfeitTxs(t *testing.T) { func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder(wallet, network.Liquid, 1209344, exitDelay) builder := txbuilder.NewTxBuilder(
wallet, network.Liquid, 1209344, unilateralExitDelay,
)
fixtures, err := parseForfeitTxsFixtures() fixtures, err := parseForfeitTxsFixtures()
require.NoError(t, err) require.NoError(t, err)

View File

@@ -204,8 +204,8 @@ func (h *handler) GetInfo(ctx context.Context, req *arkv1.GetInfoRequest) (*arkv
return &arkv1.GetInfoResponse{ return &arkv1.GetInfoResponse{
Pubkey: pubkey, Pubkey: pubkey,
Lifetime: lifetime, RoundLifetime: lifetime,
ExitDelay: delay, UnilateralExitDelay: delay,
}, nil }, nil
} }