Files
ark/client/common.go
Pietralberto Mazza 6d0d03e316 Cleanup (#121)
* Cleanup common

* Cleanup client

* Cleanup server

* Renamings

* Tidy up proto

* Update ocean protos

* Fixes

* Fixes
2024-02-28 18:05:03 +01:00

1049 lines
22 KiB
Go

package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"syscall"
"time"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
"golang.org/x/term"
)
const (
DUST = 450
)
func hashPassword(password []byte) []byte {
hash := sha256.Sum256(password)
return hash[:]
}
func verifyPassword(password []byte) error {
state, err := getState()
if err != nil {
return err
}
passwordHashString := state[PASSWORD_HASH]
if len(passwordHashString) <= 0 {
return fmt.Errorf("missing password hash")
}
passwordHash, err := hex.DecodeString(passwordHashString)
if err != nil {
return err
}
currentPassHash := hashPassword(password)
if !bytes.Equal(passwordHash, currentPassHash) {
return fmt.Errorf("invalid password")
}
return nil
}
func readPassword() ([]byte, error) {
fmt.Print("unlock your wallet with password: ")
passwordInput, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // new line
if err != nil {
return nil, err
}
if err := verifyPassword(passwordInput); err != nil {
return nil, err
}
return passwordInput, nil
}
func privateKeyFromPassword() (*secp256k1.PrivateKey, error) {
state, err := getState()
if err != nil {
return nil, err
}
encryptedPrivateKeyString := state[ENCRYPTED_PRVKEY]
if len(encryptedPrivateKeyString) <= 0 {
return nil, fmt.Errorf("missing encrypted private key")
}
encryptedPrivateKey, err := hex.DecodeString(encryptedPrivateKeyString)
if err != nil {
return nil, fmt.Errorf("invalid encrypted private key: %s", err)
}
password, err := readPassword()
if err != nil {
return nil, err
}
fmt.Println("wallet unlocked")
cypher := newAES128Cypher()
privateKeyBytes, err := cypher.decrypt(encryptedPrivateKey, password)
if err != nil {
return nil, err
}
privateKey := secp256k1.PrivKeyFromBytes(privateKeyBytes)
return privateKey, nil
}
func getWalletPublicKey() (*secp256k1.PublicKey, error) {
state, err := getState()
if err != nil {
return nil, err
}
publicKeyString := state[PUBKEY]
if len(publicKeyString) <= 0 {
return nil, fmt.Errorf("missing public key")
}
publicKeyBytes, err := hex.DecodeString(publicKeyString)
if err != nil {
return nil, err
}
return secp256k1.ParsePubKey(publicKeyBytes)
}
func getAspPublicKey() (*secp256k1.PublicKey, error) {
state, err := getState()
if err != nil {
return nil, err
}
arkPubKey := state[ASP_PUBKEY]
if len(arkPubKey) <= 0 {
return nil, fmt.Errorf("missing asp public key")
}
pubKeyBytes, err := hex.DecodeString(arkPubKey)
if err != nil {
return nil, err
}
return secp256k1.ParsePubKey(pubKeyBytes)
}
func getRoundLifetime() (int64, error) {
state, err := getState()
if err != nil {
return -1, err
}
lifetime := state[ROUND_LIFETIME]
if len(lifetime) <= 0 {
return -1, fmt.Errorf("missing round lifetime")
}
roundLifetime, err := strconv.Atoi(lifetime)
if err != nil {
return -1, err
}
return int64(roundLifetime), nil
}
func getUnilateralExitDelay() (int64, error) {
state, err := getState()
if err != nil {
return -1, err
}
delay := state[UNILATERAL_EXIT_DELAY]
if len(delay) <= 0 {
return -1, fmt.Errorf("missing unilateral exit delay")
}
redeemDelay, err := strconv.Atoi(delay)
if err != nil {
return -1, err
}
return int64(redeemDelay), nil
}
func coinSelect(vtxos []vtxo, amount uint64) ([]vtxo, uint64, error) {
selected := make([]vtxo, 0)
notSelected := make([]vtxo, 0)
selectedAmount := uint64(0)
// sort vtxos by expiration (older first)
sort.SliceStable(vtxos, func(i, j int) bool {
if vtxos[i].expireAt == nil || vtxos[j].expireAt == nil {
return false
}
return vtxos[i].expireAt.Before(*vtxos[j].expireAt)
})
for _, vtxo := range vtxos {
if selectedAmount >= amount {
notSelected = append(notSelected, vtxo)
break
}
selected = append(selected, vtxo)
selectedAmount += vtxo.amount
}
if selectedAmount < amount {
return nil, 0, fmt.Errorf("not enough funds to cover amount%d", amount)
}
change := selectedAmount - amount
if change < DUST {
if len(notSelected) > 0 {
selected = append(selected, notSelected[0])
change += notSelected[0].amount
}
}
return selected, change, nil
}
func getOffchainBalance(
ctx context.Context, explorer Explorer, client arkv1.ArkServiceClient,
addr string, withExpiration bool,
) (uint64, map[int64]uint64, error) {
amountByExpiration := make(map[int64]uint64, 0)
vtxos, err := getVtxos(ctx, explorer, client, addr, withExpiration)
if err != nil {
return 0, nil, err
}
var balance uint64
for _, vtxo := range vtxos {
balance += vtxo.amount
if withExpiration {
expiration := vtxo.expireAt.Unix()
if _, ok := amountByExpiration[expiration]; !ok {
amountByExpiration[expiration] = 0
}
amountByExpiration[expiration] += vtxo.amount
}
}
return balance, amountByExpiration, nil
}
func getTxBlocktime(txid string) (confirmed bool, blocktime int64, err error) {
_, net := getNetwork()
baseUrl := explorerUrl[net.Name]
resp, err := http.Get(fmt.Sprintf("%s/tx/%s", baseUrl, txid))
if err != nil {
return false, 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, 0, err
}
if resp.StatusCode != http.StatusOK {
return false, 0, fmt.Errorf(string(body))
}
var tx struct {
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
if err := json.Unmarshal(body, &tx); err != nil {
return false, 0, err
}
if !tx.Status.Confirmed {
return false, -1, nil
}
return true, tx.Status.Blocktime, nil
}
func getNetwork() (*common.Network, *network.Network) {
state, err := getState()
if err != nil {
return &common.TestNet, &network.Testnet
}
net, ok := state[NETWORK]
if !ok {
return &common.MainNet, &network.Liquid
}
if net == "testnet" {
return &common.TestNet, &network.Testnet
}
return &common.MainNet, &network.Liquid
}
func getAddress() (offchainAddr, onchainAddr, redemptionAddr string, err error) {
userPubkey, err := getWalletPublicKey()
if err != nil {
return
}
aspPubkey, err := getAspPublicKey()
if err != nil {
return
}
unilateralExitDelay, err := getUnilateralExitDelay()
if err != nil {
return
}
arkNet, liquidNet := getNetwork()
arkAddr, err := common.EncodeAddress(arkNet.Addr, userPubkey, aspPubkey)
if err != nil {
return
}
p2wpkh := payment.FromPublicKey(userPubkey, liquidNet, nil)
liquidAddr, err := p2wpkh.WitnessPubKeyHash()
if err != nil {
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
onchainAddr = liquidAddr
return
}
func printJSON(resp interface{}) error {
jsonBytes, err := json.MarshalIndent(resp, "", "\t")
if err != nil {
return err
}
fmt.Println(string(jsonBytes))
return nil
}
func handleRoundStream(
ctx context.Context, client arkv1.ArkServiceClient, paymentID string,
vtxosToSign []vtxo, secKey *secp256k1.PrivateKey, receivers []*arkv1.Output,
) (poolTxID string, err error) {
stream, err := client.GetEventStream(ctx, &arkv1.GetEventStreamRequest{})
if err != nil {
return "", err
}
var pingStop func()
pingReq := &arkv1.PingRequest{
PaymentId: paymentID,
}
for pingStop == nil {
pingStop = ping(ctx, client, pingReq)
}
defer pingStop()
for {
event, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if e := event.GetRoundFailed(); e != nil {
pingStop()
return "", fmt.Errorf("round failed: %s", e.GetReason())
}
if e := event.GetRoundFinalization(); e != nil {
// stop pinging as soon as we receive some forfeit txs
pingStop()
fmt.Println("round finalization started")
poolTxStr := e.GetPoolPartialTx()
poolTx, err := psetv2.NewPsetFromBase64(poolTxStr)
if err != nil {
return "", err
}
congestionTree, err := toCongestionTree(e.GetCongestionTree())
if err != nil {
return "", err
}
aspPubkey, err := getAspPublicKey()
if err != nil {
return "", err
}
roundLifetime, err := getRoundLifetime()
if err != nil {
return "", err
}
// validate the congestion tree
if err := tree.ValidateCongestionTree(
congestionTree, poolTxStr, aspPubkey, int64(roundLifetime),
); err != nil {
return "", err
}
exitDelay, err := getUnilateralExitDelay()
if err != nil {
return "", err
}
for _, receiver := range receivers {
isOnChain, onchainScript, userPubkey, err := decodeReceiverAddress(
receiver.Address,
)
if err != nil {
return "", err
}
if isOnChain {
// collaborative exit case
// search for the output in the pool tx
found := false
for _, output := range poolTx.Outputs {
if bytes.Equal(output.Script, onchainScript) {
if output.Value != receiver.Amount {
return "", fmt.Errorf(
"invalid collaborative exit output amount: got %d, want %d",
output.Value, receiver.Amount,
)
}
found = true
break
}
}
if !found {
return "", fmt.Errorf(
"collaborative exit output not found: %s", receiver.Address,
)
}
continue
}
// off-chain send case
// search for the output in congestion tree
found := false
// compute the receiver output taproot key
outputTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(exitDelay),
)
if err != nil {
return "", err
}
leaves := congestionTree.Leaves()
for _, leaf := range leaves {
tx, err := psetv2.NewPsetFromBase64(leaf.Tx)
if err != nil {
return "", err
}
for _, output := range tx.Outputs {
if len(output.Script) == 0 {
continue
}
if bytes.Equal(
output.Script[2:], schnorr.SerializePubKey(outputTapKey),
) {
if output.Value != receiver.Amount {
continue
}
found = true
break
}
}
if found {
break
}
}
if !found {
return "", fmt.Errorf(
"off-chain send output not found: %s", receiver.Address,
)
}
}
fmt.Println("congestion tree validated")
forfeits := e.GetForfeitTxs()
signedForfeits := make([]string, 0)
fmt.Print("signing forfeit txs... ")
explorer := NewExplorer()
for _, forfeit := range forfeits {
pset, err := psetv2.NewPsetFromBase64(forfeit)
if err != nil {
return "", err
}
for _, input := range pset.Inputs {
inputTxid := chainhash.Hash(input.PreviousTxid).String()
for _, coin := range vtxosToSign {
// check if it contains one of the input to sign
if inputTxid == coin.txid {
if err := signPset(pset, explorer, secKey); err != nil {
return "", err
}
signedPset, err := pset.ToBase64()
if err != nil {
return "", err
}
signedForfeits = append(signedForfeits, signedPset)
}
}
}
}
// if no forfeit txs have been signed, start pinging again and wait for the next round
if len(signedForfeits) == 0 {
fmt.Printf("\nno forfeit txs to sign, waiting for the next round...\n")
pingStop = nil
for pingStop == nil {
pingStop = ping(ctx, client, pingReq)
}
continue
}
fmt.Printf("%d signed\n", len(signedForfeits))
fmt.Print("finalizing payment... ")
_, err = client.FinalizePayment(ctx, &arkv1.FinalizePaymentRequest{
SignedForfeitTxs: signedForfeits,
})
if err != nil {
return "", err
}
fmt.Print("done.\n")
fmt.Println("waiting for round finalization...")
continue
}
if event.GetRoundFinalized() != nil {
return event.GetRoundFinalized().GetPoolTxid(), nil
}
}
return "", fmt.Errorf("stream closed unexpectedly")
}
// 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
func ping(
ctx context.Context, client arkv1.ArkServiceClient, req *arkv1.PingRequest,
) func() {
_, err := client.Ping(ctx, req)
if err != nil {
return nil
}
ticker := time.NewTicker(5 * time.Second)
go func(t *time.Ticker) {
for range t.C {
// nolint
client.Ping(ctx, req)
}
}(ticker)
return ticker.Stop
}
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
levels := make(tree.CongestionTree, 0, len(treeFromProto.Levels))
for _, level := range treeFromProto.Levels {
nodes := make([]tree.Node, 0, len(level.Nodes))
for _, node := range level.Nodes {
nodes = append(nodes, tree.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
Leaf: false,
})
}
levels = append(levels, nodes)
}
for j, treeLvl := range levels {
for i, node := range treeLvl {
if len(levels.Children(node.Txid)) == 0 {
levels[j][i].Leaf = true
}
}
}
return levels, nil
}
// castCongestionTree converts a tree.CongestionTree to a repeated arkv1.TreeLevel
func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
levels := make([]*arkv1.TreeLevel, 0, len(congestionTree))
for _, level := range congestionTree {
levelProto := &arkv1.TreeLevel{
Nodes: make([]*arkv1.Node, 0, len(level)),
}
for _, node := range level {
levelProto.Nodes = append(levelProto.Nodes, &arkv1.Node{
Txid: node.Txid,
Tx: node.Tx,
ParentTxid: node.ParentTxid,
})
}
levels = append(levels, levelProto)
}
return &arkv1.Tree{
Levels: levels,
}
}
func decodeReceiverAddress(addr string) (
bool, []byte, *secp256k1.PublicKey, error,
) {
outputScript, err := address.ToOutputScript(addr)
if err != nil {
_, userPubkey, _, err := common.DecodeAddress(addr)
if err != nil {
return false, nil, nil, err
}
return false, nil, userPubkey, nil
}
return true, outputScript, nil, nil
}
func findSweepClosure(
congestionTree tree.CongestionTree,
) (*taproot.TapElementsLeaf, uint, error) {
root, err := congestionTree.Root()
if err != nil {
return nil, 0, err
}
// find the sweep closure
tx, err := psetv2.NewPsetFromBase64(root.Tx)
if err != nil {
return nil, 0, err
}
var seconds uint
var sweepClosure *taproot.TapElementsLeaf
for _, tapLeaf := range tx.Inputs[0].TapLeafScript {
closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(tapLeaf.Script)
if err != nil {
continue
}
if valid && closure.Seconds > seconds {
seconds = closure.Seconds
sweepClosure = &tapLeaf.TapElementsLeaf
}
}
if sweepClosure == nil {
return nil, 0, fmt.Errorf("sweep closure not found")
}
return sweepClosure, seconds, nil
}
func getRedeemBranches(
ctx context.Context, explorer Explorer, client arkv1.ArkServiceClient,
vtxos []vtxo,
) (map[string]*redeemBranch, error) {
congestionTrees := make(map[string]tree.CongestionTree, 0)
redeemBranches := make(map[string]*redeemBranch, 0)
for _, vtxo := range vtxos {
if _, ok := congestionTrees[vtxo.poolTxid]; !ok {
round, err := client.GetRound(ctx, &arkv1.GetRoundRequest{
Txid: vtxo.poolTxid,
})
if err != nil {
return nil, err
}
treeFromRound := round.GetRound().GetCongestionTree()
congestionTree, err := toCongestionTree(treeFromRound)
if err != nil {
return nil, err
}
congestionTrees[vtxo.poolTxid] = congestionTree
}
redeemBranch, err := newRedeemBranch(
ctx, explorer, congestionTrees[vtxo.poolTxid], vtxo,
)
if err != nil {
return nil, err
}
redeemBranches[vtxo.txid] = redeemBranch
}
return redeemBranches, nil
}
func computeVtxoTaprootScript(
userPubkey, aspPubkey *secp256k1.PublicKey, exitDelay uint,
) (*secp256k1.PublicKey, *taproot.TapscriptElementsProof, error) {
redeemClosure := &tree.CSVSigClosure{
Pubkey: userPubkey,
Seconds: exitDelay,
}
forfeitClosure := &tree.ForfeitClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, nil, err
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, nil, err
}
vtxoTaprootTree := taproot.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := vtxoTaprootTree.RootNode.TapHash()
unspendableKey := tree.UnspendableKey()
vtxoTaprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:])
redeemLeafHash := redeemLeaf.TapHash()
proofIndex := vtxoTaprootTree.LeafProofIndex[redeemLeafHash]
proof := vtxoTaprootTree.LeafMerkleProofs[proofIndex]
return vtxoTaprootKey, &proof, nil
}
func addVtxoInput(
updater *psetv2.Updater, inputArgs psetv2.InputArgs, exitDelay uint,
tapLeafProof *taproot.TapscriptElementsProof,
) error {
sequence, err := common.BIP68EncodeAsNumber(exitDelay)
if err != nil {
return nil
}
nextInputIndex := len(updater.Pset.Inputs)
if err := updater.AddInputs([]psetv2.InputArgs{inputArgs}); err != nil {
return err
}
updater.Pset.Inputs[nextInputIndex].Sequence = sequence
return updater.AddInTapLeafScript(
nextInputIndex,
psetv2.NewTapLeafScript(
*tapLeafProof,
tree.UnspendableKey(),
),
)
}
func coinSelectOnchain(
explorer Explorer, targetAmount uint64, exclude []utxo,
) ([]utxo, []utxo, uint64, error) {
_, onchainAddr, _, err := getAddress()
if err != nil {
return nil, nil, 0, err
}
fromExplorer, err := explorer.GetUtxos(onchainAddr)
if err != nil {
return nil, nil, 0, err
}
utxos := make([]utxo, 0)
selectedAmount := uint64(0)
for _, utxo := range fromExplorer {
if selectedAmount >= targetAmount {
break
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
utxos = append(utxos, utxo)
selectedAmount += utxo.Amount
}
if selectedAmount >= targetAmount {
return utxos, nil, selectedAmount - targetAmount, nil
}
userPubkey, err := getWalletPublicKey()
if err != nil {
return nil, nil, 0, err
}
aspPubkey, err := getAspPublicKey()
if err != nil {
return nil, nil, 0, err
}
unilateralExitDelay, err := getUnilateralExitDelay()
if err != nil {
return nil, nil, 0, err
}
vtxoTapKey, _, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil {
return nil, nil, 0, err
}
_, net := getNetwork()
pay, err := payment.FromTweakedKey(vtxoTapKey, net, nil)
if err != nil {
return nil, nil, 0, err
}
addr, err := pay.TaprootAddress()
if err != nil {
return nil, nil, 0, err
}
fromExplorer, err = explorer.GetUtxos(addr)
if err != nil {
return nil, nil, 0, err
}
delayedUtxos := make([]utxo, 0)
for _, utxo := range fromExplorer {
if selectedAmount >= targetAmount {
break
}
availableAt := time.Unix(utxo.Status.Blocktime, 0).Add(
time.Duration(unilateralExitDelay) * time.Second,
)
if availableAt.After(time.Now()) {
continue
}
for _, excluded := range exclude {
if utxo.Txid == excluded.Txid && utxo.Vout == excluded.Vout {
continue
}
}
delayedUtxos = append(delayedUtxos, utxo)
selectedAmount += utxo.Amount
}
if selectedAmount < targetAmount {
return nil, nil, 0, fmt.Errorf(
"not enough funds to cover amount %d", targetAmount,
)
}
return utxos, delayedUtxos, selectedAmount - targetAmount, nil
}
func addInputs(
updater *psetv2.Updater, utxos, delayedUtxos []utxo, net *network.Network,
) error {
_, onchainAddr, _, err := getAddress()
if err != nil {
return err
}
changeScript, err := address.ToOutputScript(onchainAddr)
if err != nil {
return err
}
for _, utxo := range utxos {
if err := updater.AddInputs([]psetv2.InputArgs{
{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
},
}); err != nil {
return err
}
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil {
return err
}
value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil {
return err
}
witnessUtxo := transaction.TxOutput{
Asset: assetID,
Value: value,
Script: changeScript,
Nonce: []byte{0x00},
}
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, &witnessUtxo,
); err != nil {
return err
}
}
if len(delayedUtxos) > 0 {
userPubkey, err := getWalletPublicKey()
if err != nil {
return err
}
aspPubkey, err := getAspPublicKey()
if err != nil {
return err
}
unilateralExitDelay, err := getUnilateralExitDelay()
if err != nil {
return err
}
vtxoTapKey, leafProof, err := computeVtxoTaprootScript(
userPubkey, aspPubkey, uint(unilateralExitDelay),
)
if err != nil {
return err
}
pay, err := payment.FromTweakedKey(vtxoTapKey, net, nil)
if err != nil {
return err
}
addr, err := pay.TaprootAddress()
if err != nil {
return err
}
script, err := address.ToOutputScript(addr)
if err != nil {
return err
}
for _, utxo := range delayedUtxos {
if err := addVtxoInput(
updater,
psetv2.InputArgs{
Txid: utxo.Txid,
TxIndex: utxo.Vout,
},
uint(unilateralExitDelay),
leafProof,
); err != nil {
return err
}
assetID, err := elementsutil.AssetHashToBytes(utxo.Asset)
if err != nil {
return err
}
value, err := elementsutil.ValueToBytes(utxo.Amount)
if err != nil {
return err
}
witnessUtxo := transaction.TxOutput{
Asset: assetID,
Value: value,
Script: script,
Nonce: []byte{0x00},
}
if err := updater.AddInWitnessUtxo(
len(updater.Pset.Inputs)-1, &witnessUtxo,
); err != nil {
return err
}
}
}
return nil
}