New boarding protocol (#279)

* [domain] add reverse boarding inputs in Payment struct

* [tx-builder] support reverse boarding script

* [wallet] add GetTransaction

* [api-spec][application] add reverse boarding support in covenantless

* [config] add reverse boarding config

* [api-spec] add ReverseBoardingAddress RPC

* [domain][application] support empty forfeits txs in EndFinalization events

* [tx-builder] optional connector output in round tx

* [btc-embedded] fix getTx and taproot finalizer

* whitelist ReverseBoardingAddress RPC

* [test] add reverse boarding integration test

* [client] support reverse boarding

* [sdk] support reverse boarding

* [e2e] add sleep time after faucet

* [test] run using bitcoin-core RPC

* [tx-builder] fix GetSweepInput

* [application][tx-builder] support reverse onboarding in covenant

* [cli] support reverse onboarding in covenant CLI

* [test] rework integration tests

* [sdk] remove onchain wallet, replace by onboarding address

* remove old onboarding protocols

* [sdk] Fix RegisterPayment

* [e2e] add more funds to covenant ASP

* [e2e] add sleeping time

* several fixes

* descriptor boarding

* remove boarding delay from info

* [sdk] implement descriptor boarding

* go mod tidy

* fixes and revert error msgs

* move descriptor pkg to common

* add replace in go.mod

* [sdk] fix unit tests

* rename DescriptorInput --> BoardingInput

* genrest in SDK

* remove boarding input from domain

* remove all "reverse boarding"

* rename "onboarding" ==> "boarding"

* remove outdate payment unit test

* use tmpfs docker volument for compose testing files

* several fixes
This commit is contained in:
Louis Singer
2024-09-04 19:21:26 +02:00
committed by GitHub
parent 8cba9c9d42
commit 4da76ec88b
113 changed files with 5627 additions and 4430 deletions

View File

@@ -63,6 +63,7 @@ type Config struct {
MinRelayFee uint64
RoundLifetime int64
UnilateralExitDelay int64
BoardingExitDelay int64
EsploraURL string
NeutrinoPeer string
@@ -126,6 +127,12 @@ func (c *Config) Validate() error {
)
}
if c.BoardingExitDelay < minAllowedSequence {
return fmt.Errorf(
"invalid boarding exit delay, must at least %d", minAllowedSequence,
)
}
if c.RoundLifetime%minAllowedSequence != 0 {
c.RoundLifetime -= c.RoundLifetime % minAllowedSequence
log.Infof(
@@ -142,6 +149,14 @@ func (c *Config) Validate() error {
)
}
if c.BoardingExitDelay%minAllowedSequence != 0 {
c.BoardingExitDelay -= c.BoardingExitDelay % minAllowedSequence
log.Infof(
"boarding exit delay must be a multiple of %d, rounded to %d",
minAllowedSequence, c.BoardingExitDelay,
)
}
if err := c.repoManager(); err != nil {
return err
}
@@ -275,11 +290,11 @@ func (c *Config) txBuilderService() error {
switch c.TxBuilderType {
case "covenant":
svc = txbuilder.NewTxBuilder(
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay,
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
)
case "covenantless":
svc = cltxbuilder.NewTxBuilder(
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay,
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
)
default:
err = fmt.Errorf("unknown tx builder type")
@@ -323,7 +338,7 @@ func (c *Config) schedulerService() error {
func (c *Config) appService() error {
if common.IsLiquid(c.Network) {
svc, err := application.NewCovenantService(
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay,
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
c.MinRelayFee, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
)
if err != nil {
@@ -335,7 +350,7 @@ func (c *Config) appService() error {
}
svc, err := application.NewCovenantlessService(
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay,
c.Network, c.RoundInterval, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
c.MinRelayFee, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler,
)
if err != nil {

View File

@@ -29,6 +29,7 @@ type Config struct {
MinRelayFee uint64
RoundLifetime int64
UnilateralExitDelay int64
BoardingExitDelay int64
EsploraURL string
NeutrinoPeer string
BitcoindRpcUser string
@@ -54,6 +55,7 @@ var (
MinRelayFee = "MIN_RELAY_FEE"
RoundLifetime = "ROUND_LIFETIME"
UnilateralExitDelay = "UNILATERAL_EXIT_DELAY"
BoardingExitDelay = "BOARDING_EXIT_DELAY"
EsploraURL = "ESPLORA_URL"
NeutrinoPeer = "NEUTRINO_PEER"
BitcoindRpcUser = "BITCOIND_RPC_USER"
@@ -79,6 +81,7 @@ var (
defaultMinRelayFee = 30 // 0.1 sat/vbyte on Liquid
defaultRoundLifetime = 604672
defaultUnilateralExitDelay = 1024
defaultBoardingExitDelay = 604672
defaultNoMacaroons = false
defaultNoTLS = false
)
@@ -104,6 +107,7 @@ func LoadConfig() (*Config, error) {
viper.SetDefault(UnilateralExitDelay, defaultUnilateralExitDelay)
viper.SetDefault(BlockchainScannerType, defaultBlockchainScannerType)
viper.SetDefault(NoMacaroons, defaultNoMacaroons)
viper.SetDefault(BoardingExitDelay, defaultBoardingExitDelay)
net, err := getNetwork()
if err != nil {
@@ -132,6 +136,7 @@ func LoadConfig() (*Config, error) {
MinRelayFee: viper.GetUint64(MinRelayFee),
RoundLifetime: viper.GetInt64(RoundLifetime),
UnilateralExitDelay: viper.GetInt64(UnilateralExitDelay),
BoardingExitDelay: viper.GetInt64(BoardingExitDelay),
EsploraURL: viper.GetString(EsploraURL),
NeutrinoPeer: viper.GetString(NeutrinoPeer),
BitcoindRpcUser: viper.GetString(BitcoindRpcUser),

View File

@@ -10,14 +10,18 @@ import (
"time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
var (
@@ -30,6 +34,7 @@ type covenantService struct {
roundLifetime int64
roundInterval int64
unilateralExitDelay int64
boardingExitDelay int64
minRelayFee uint64
wallet ports.WalletService
@@ -41,23 +46,22 @@ type covenantService struct {
paymentRequests *paymentsMap
forfeitTxs *forfeitTxsMap
eventsCh chan domain.RoundEvent
onboardingCh chan onboarding
eventsCh chan domain.RoundEvent
lastEvent domain.RoundEvent
currentRound *domain.Round
currentRoundLock sync.Mutex
currentRound *domain.Round
lastEvent domain.RoundEvent
}
func NewCovenantService(
network common.Network,
roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64,
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, minRelayFee uint64,
walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService,
) (Service, error) {
eventsCh := make(chan domain.RoundEvent)
onboardingCh := make(chan onboarding)
paymentRequests := newPaymentsMap(nil)
paymentRequests := newPaymentsMap()
forfeitTxs := newForfeitTxsMap(builder)
pubkey, err := walletSvc.GetPubkey(context.Background())
@@ -69,9 +73,9 @@ func NewCovenantService(
svc := &covenantService{
network, pubkey,
roundLifetime, roundInterval, unilateralExitDelay, minRelayFee,
roundLifetime, roundInterval, unilateralExitDelay, boardingExitDelay, minRelayFee,
walletSvc, repoManager, builder, scanner, sweeper,
paymentRequests, forfeitTxs, eventsCh, onboardingCh, nil, nil,
paymentRequests, forfeitTxs, eventsCh, sync.Mutex{}, nil, nil,
}
repoManager.RegisterEventsHandler(
func(round *domain.Round) {
@@ -88,7 +92,6 @@ func NewCovenantService(
return nil, fmt.Errorf("failed to restore watching vtxos: %s", err)
}
go svc.listenToScannerNotifications()
go svc.listenToOnboarding()
return svc, nil
}
@@ -116,30 +119,164 @@ func (s *covenantService) Stop() {
s.repoManager.Close()
log.Debug("closed connection to db")
close(s.eventsCh)
close(s.onboardingCh)
}
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs)
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error) {
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
return addr, nil
}
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) {
vtxosInputs := make([]domain.VtxoKey, 0)
boardingInputs := make([]Input, 0)
for _, in := range inputs {
if in.IsVtxo() {
vtxosInputs = append(vtxosInputs, in.VtxoKey())
continue
}
boardingInputs = append(boardingInputs, in)
}
vtxos := make([]domain.Vtxo, 0)
if len(vtxosInputs) > 0 {
var err error
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
if v.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut)
}
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
}
}
boardingTxs := make(map[string]string, 0) // txid -> txhex
now := time.Now().Unix()
for _, in := range boardingInputs {
if _, ok := boardingTxs[in.Txid]; !ok {
txhex, err := s.wallet.GetTransaction(ctx, in.Txid)
if err != nil {
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
}
if !confirmed {
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
}
if blocktime+int64(s.boardingExitDelay) < now {
return "", fmt.Errorf("tx %s expired", in.Txid)
}
boardingTxs[in.Txid] = txhex
}
}
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
for _, in := range boardingInputs {
desc, err := in.GetDescriptor()
if err != nil {
log.WithError(err).Debugf("failed to parse boarding input descriptor")
return "", fmt.Errorf("failed to parse descriptor %s for input %s:%d", in.Descriptor, in.Txid, in.Index)
}
input, err := s.newBoardingInput(boardingTxs[in.Txid], in.Index, *desc)
if err != nil {
log.WithError(err).Debugf("failed to create boarding input")
return "", fmt.Errorf("input %s:%d is not a valid boarding input", in.Txid, in.Index)
}
utxos = append(utxos, input)
}
payment, err := domain.NewPayment(vtxos)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment); err != nil {
if err := s.paymentRequests.push(*payment, utxos); err != nil {
return "", err
}
return payment.Id, nil
}
func (s *covenantService) newBoardingInput(
txhex string, vout uint32, desc descriptor.TaprootDescriptor,
) (ports.BoardingInput, error) {
tx, err := transaction.NewTxFromHex(txhex)
if err != nil {
return nil, fmt.Errorf("failed to parse tx: %s", err)
}
if len(tx.Outputs) <= int(vout) {
return nil, fmt.Errorf("output not found")
}
out := tx.Outputs[vout]
script := out.Script
if len(out.RangeProof) > 0 || len(out.SurjectionProof) > 0 {
return nil, fmt.Errorf("output is confidential")
}
scriptFromDescriptor, err := tree.ComputeOutputScript(desc)
if err != nil {
return nil, fmt.Errorf("failed to compute output script: %s", err)
}
if !bytes.Equal(script, scriptFromDescriptor) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
pubkey, timeout, err := descriptor.ParseBoardingDescriptor(desc)
if err != nil {
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
if timeout != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey)
if err != nil {
return nil, fmt.Errorf("failed to compute boarding script: %s", err)
}
if !bytes.Equal(script, expectedScript) {
return nil, fmt.Errorf("output script mismatch expected script")
}
value, err := elementsutil.ValueFromBytes(out.Value)
if err != nil {
return nil, fmt.Errorf("failed to parse value: %s", err)
}
return &boardingInput{
txId: tx.TxHash(),
vout: vout,
boardingPubKey: pubkey,
amount: value,
}, nil
}
func (s *covenantService) ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error {
// Check credentials
payment, ok := s.paymentRequests.view(creds)
@@ -178,6 +315,19 @@ func (s *covenantService) SignVtxos(ctx context.Context, forfeitTxs []string) er
return s.forfeitTxs.sign(forfeitTxs)
}
func (s *covenantService) SignRoundTx(ctx context.Context, signedRoundTx string) error {
s.currentRoundLock.Lock()
defer s.currentRoundLock.Unlock()
combined, err := s.builder.VerifyAndCombinePartialTx(s.currentRound.UnsignedTx, signedRoundTx)
if err != nil {
return err
}
s.currentRound.UnsignedTx = combined
return nil
}
func (s *covenantService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
@@ -209,50 +359,17 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
RoundInterval: s.roundInterval,
Network: s.network.Name,
MinRelayFee: int64(s.minRelayFee),
BoardingDescriptorTemplate: fmt.Sprintf(
descriptor.BoardingDescriptorTemplate,
hex.EncodeToString(tree.UnspendableKey().SerializeCompressed()),
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
"USER",
s.boardingExitDelay,
"USER",
),
}, nil
}
func (s *covenantService) Onboard(
ctx context.Context, boardingTx string,
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
) error {
ptx, err := psetv2.NewPsetFromBase64(boardingTx)
if err != nil {
return fmt.Errorf("failed to parse boarding tx: %s", err)
}
if err := tree.ValidateCongestionTree(
congestionTree, boardingTx, s.pubkey, s.roundLifetime,
); err != nil {
return err
}
extracted, err := psetv2.Extract(ptx)
if err != nil {
return fmt.Errorf("failed to extract boarding tx: %s", err)
}
boardingTxHex, err := extracted.ToHex()
if err != nil {
return fmt.Errorf("failed to convert boarding tx to hex: %s", err)
}
txid, err := s.wallet.BroadcastTransaction(ctx, boardingTxHex)
if err != nil {
return fmt.Errorf("failed to broadcast boarding tx: %s", err)
}
log.Debugf("broadcasted boarding tx %s", txid)
s.onboardingCh <- onboarding{
tx: boardingTx,
congestionTree: congestionTree,
userPubkey: userPubkey,
}
return nil
}
func (s *covenantService) RegisterCosignerPubkey(ctx context.Context, paymentId string, _ string) error {
// if the user sends an ephemeral pubkey, something is going wrong client-side
// we should delete the associated payment
@@ -329,7 +446,7 @@ func (s *covenantService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, _ := s.paymentRequests.pop(num)
payments, boardingInputs, _ := s.paymentRequests.pop(num)
if _, err := round.RegisterPayments(payments); err != nil {
round.Fail(fmt.Errorf("failed to register payments: %s", err))
log.WithError(err).Warn("failed to register payments")
@@ -343,7 +460,7 @@ func (s *covenantService) startFinalization() {
return
}
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds)
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, boardingInputs, s.minRelayFee, sweptRounds)
if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx")
@@ -351,16 +468,26 @@ func (s *covenantService) startFinalization() {
}
log.Debugf("pool tx created for round %s", round.Id)
// TODO BTC make the senders sign the tree
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
return
needForfeits := false
for _, pay := range payments {
if len(pay.Inputs) > 0 {
needForfeits = true
break
}
}
log.Debugf("forfeit transactions created for round %s", round.Id)
var forfeitTxs, connectors []string
if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
return
}
log.Debugf("forfeit transactions created for round %s", round.Id)
}
if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedPoolTx,
@@ -401,71 +528,63 @@ func (s *covenantService) finalizeRound() {
}
log.Debugf("signing round transaction %s\n", round.Id)
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
boardingInputs := make([]int, 0)
roundTx, err := psetv2.NewPsetFromBase64(round.UnsignedTx)
if err != nil {
log.Debugf("failed to parse round tx: %s", round.UnsignedTx)
changes = round.Fail(fmt.Errorf("failed to parse round tx: %s", err))
log.WithError(err).Warn("failed to parse round tx")
return
}
for i, in := range roundTx.Inputs {
if len(in.TapLeafScript) > 0 {
if len(in.TapScriptSig) == 0 {
err = fmt.Errorf("missing tapscript spend sig for input %d", i)
changes = round.Fail(err)
log.WithError(err).Warn("missing boarding sig")
return
}
boardingInputs = append(boardingInputs, i)
}
}
signedRoundTx := round.UnsignedTx
if len(boardingInputs) > 0 {
signedRoundTx, err = s.wallet.SignTransactionTapscript(ctx, signedRoundTx, boardingInputs)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
}
signedRoundTx, err = s.wallet.SignTransaction(ctx, signedRoundTx, true)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
txid, err := s.wallet.BroadcastTransaction(ctx, signedRoundTx)
if err != nil {
log.Debugf("failed to broadcast round tx: %s", signedRoundTx)
changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err))
log.WithError(err).Warn("failed to broadcast pool tx")
return
}
changes, _ = round.EndFinalization(forfeitTxs, txid)
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
}
func (s *covenantService) listenToOnboarding() {
for onboarding := range s.onboardingCh {
go s.handleOnboarding(onboarding)
}
}
func (s *covenantService) handleOnboarding(onboarding onboarding) {
ctx := context.Background()
ptx, _ := psetv2.NewPsetFromBase64(onboarding.tx)
utx, _ := psetv2.Extract(ptx)
txid := utx.TxHash().String()
// wait for the tx to be confirmed with a timeout
timeout := time.NewTimer(5 * time.Minute)
defer timeout.Stop()
isConfirmed := false
for !isConfirmed {
select {
case <-timeout.C:
log.WithError(fmt.Errorf("operation timed out")).Warnf("failed to get confirmation for boarding tx %s", txid)
return
default:
var err error
isConfirmed, _, err = s.wallet.IsTransactionConfirmed(ctx, txid)
if err != nil {
log.WithError(err).Warn("failed to check tx confirmation")
}
if err != nil || !isConfirmed {
time.Sleep(5 * time.Second)
}
}
}
pubkey := hex.EncodeToString(onboarding.userPubkey.SerializeCompressed())
payments := getPaymentsFromOnboardingLiquid(onboarding.congestionTree, pubkey)
round := domain.NewFinalizedRound(
dustAmount, pubkey, txid, onboarding.tx, onboarding.congestionTree, payments,
)
if err := s.saveEvents(ctx, round.Id, round.Events()); err != nil {
log.WithError(err).Warn("failed to store new round events")
changes, err = round.EndFinalization(forfeitTxs, txid)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
log.WithError(err).Warn("failed to finalize round")
return
}
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
}
func (s *covenantService) listenToScannerNotifications() {
@@ -867,23 +986,6 @@ func (s *covenantService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
}
func getPaymentsFromOnboardingLiquid(
congestionTree tree.CongestionTree, userKey string,
) []domain.Payment {
leaves := congestionTree.Leaves()
receivers := make([]domain.Receiver, 0, len(leaves))
for _, node := range leaves {
ptx, _ := psetv2.NewPsetFromBase64(node.Tx)
receiver := domain.Receiver{
Pubkey: userKey,
Amount: ptx.Outputs[0].Value,
}
receivers = append(receivers, receiver)
}
payment := domain.NewPaymentUnsafe(nil, receivers)
return []domain.Payment{*payment}
}
func findForfeitTxLiquid(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
) (string, error) {

View File

@@ -11,12 +11,15 @@ import (
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus"
)
@@ -27,6 +30,7 @@ type covenantlessService struct {
roundLifetime int64
roundInterval int64
unilateralExitDelay int64
boardingExitDelay int64
minRelayFee uint64
wallet ports.WalletService
@@ -38,11 +42,11 @@ type covenantlessService struct {
paymentRequests *paymentsMap
forfeitTxs *forfeitTxsMap
eventsCh chan domain.RoundEvent
onboardingCh chan onboarding
eventsCh chan domain.RoundEvent
// cached data for the current round
lastEvent domain.RoundEvent
currentRoundLock sync.Mutex
currentRound *domain.Round
treeSigningSessions map[string]*musigSigningSession
asyncPaymentsCache map[domain.VtxoKey]struct {
@@ -53,14 +57,13 @@ type covenantlessService struct {
func NewCovenantlessService(
network common.Network,
roundInterval, roundLifetime, unilateralExitDelay int64, minRelayFee uint64,
roundInterval, roundLifetime, unilateralExitDelay, boardingExitDelay int64, minRelayFee uint64,
walletSvc ports.WalletService, repoManager ports.RepoManager,
builder ports.TxBuilder, scanner ports.BlockchainScanner,
scheduler ports.SchedulerService,
) (Service, error) {
eventsCh := make(chan domain.RoundEvent)
onboardingCh := make(chan onboarding)
paymentRequests := newPaymentsMap(nil)
paymentRequests := newPaymentsMap()
forfeitTxs := newForfeitTxsMap(builder)
pubkey, err := walletSvc.GetPubkey(context.Background())
@@ -89,9 +92,10 @@ func NewCovenantlessService(
paymentRequests: paymentRequests,
forfeitTxs: forfeitTxs,
eventsCh: eventsCh,
onboardingCh: onboardingCh,
currentRoundLock: sync.Mutex{},
asyncPaymentsCache: asyncPaymentsCache,
treeSigningSessions: make(map[string]*musigSigningSession),
boardingExitDelay: boardingExitDelay,
}
repoManager.RegisterEventsHandler(
@@ -109,7 +113,6 @@ func NewCovenantlessService(
return nil, fmt.Errorf("failed to restore watching vtxos: %s", err)
}
go svc.listenToScannerNotifications()
go svc.listenToOnboarding()
return svc, nil
}
@@ -137,7 +140,6 @@ func (s *covenantlessService) Stop() {
s.repoManager.Close()
log.Debug("closed connection to db")
close(s.eventsCh)
close(s.onboardingCh)
}
func (s *covenantlessService) CompleteAsyncPayment(
@@ -242,27 +244,147 @@ func (s *covenantlessService) CreateAsyncPayment(
return res.RedeemTx, res.UnconditionalForfeitTxs, nil
}
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) {
vtxosInputs := make([]domain.VtxoKey, 0)
boardingInputs := make([]Input, 0)
for _, input := range inputs {
if input.IsVtxo() {
vtxosInputs = append(vtxosInputs, input.VtxoKey())
continue
}
boardingInputs = append(boardingInputs, input)
}
vtxos := make([]domain.Vtxo, 0)
if len(vtxosInputs) > 0 {
var err error
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
if v.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut)
}
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
}
}
boardingTxs := make(map[string]string, 0) // txid -> txhex
now := time.Now().Unix()
for _, in := range boardingInputs {
if _, ok := boardingTxs[in.Txid]; !ok {
// check if the tx exists and is confirmed
txhex, err := s.wallet.GetTransaction(ctx, in.Txid)
if err != nil {
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
}
if !confirmed {
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
}
// if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(s.boardingExitDelay) < now {
return "", fmt.Errorf("tx %s expired", in.Txid)
}
boardingTxs[in.Txid] = txhex
}
}
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
for _, in := range boardingInputs {
desc, err := in.GetDescriptor()
if err != nil {
log.WithError(err).Debugf("failed to parse boarding input descriptor")
return "", fmt.Errorf("failed to parse descriptor %s for input %s:%d", in.Descriptor, in.Txid, in.Index)
}
input, err := s.newBoardingInput(boardingTxs[in.Txid], in.Index, *desc)
if err != nil {
log.WithError(err).Debugf("failed to create boarding input")
return "", fmt.Errorf("input %s:%d is not a valid boarding input", in.Txid, in.Index)
}
utxos = append(utxos, input)
}
payment, err := domain.NewPayment(vtxos)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment); err != nil {
if err := s.paymentRequests.push(*payment, utxos); err != nil {
return "", err
}
return payment.Id, nil
}
func (s *covenantlessService) newBoardingInput(txhex string, vout uint32, desc descriptor.TaprootDescriptor) (ports.BoardingInput, error) {
var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
return nil, fmt.Errorf("failed to deserialize tx: %s", err)
}
if len(tx.TxOut) <= int(vout) {
return nil, fmt.Errorf("output not found")
}
out := tx.TxOut[vout]
script := out.PkScript
scriptFromDescriptor, err := bitcointree.ComputeOutputScript(desc)
if err != nil {
return nil, fmt.Errorf("failed to compute output script: %s", err)
}
if !bytes.Equal(script, scriptFromDescriptor) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
pubkey, timeout, err := descriptor.ParseBoardingDescriptor(desc)
if err != nil {
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
if timeout != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey)
if err != nil {
return nil, fmt.Errorf("failed to get boarding script: %s", err)
}
if !bytes.Equal(script, expectedScript) {
return nil, fmt.Errorf("invalid boarding input output script")
}
return &boardingInput{
txId: tx.TxHash(),
vout: vout,
boardingPubKey: pubkey,
amount: uint64(out.Value),
}, nil
}
func (s *covenantlessService) ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error {
// Check credentials
payment, ok := s.paymentRequests.view(creds)
@@ -293,6 +415,19 @@ func (s *covenantlessService) SignVtxos(ctx context.Context, forfeitTxs []string
return s.forfeitTxs.sign(forfeitTxs)
}
func (s *covenantlessService) SignRoundTx(ctx context.Context, signedRoundTx string) error {
s.currentRoundLock.Lock()
defer s.currentRoundLock.Unlock()
combined, err := s.builder.VerifyAndCombinePartialTx(s.currentRound.UnsignedTx, signedRoundTx)
if err != nil {
return fmt.Errorf("failed to verify and combine partial tx: %s", err)
}
s.currentRound.UnsignedTx = combined
return nil
}
func (s *covenantlessService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
@@ -324,50 +459,26 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
RoundInterval: s.roundInterval,
Network: s.network.Name,
MinRelayFee: int64(s.minRelayFee),
BoardingDescriptorTemplate: fmt.Sprintf(
descriptor.BoardingDescriptorTemplate,
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
"USER",
s.boardingExitDelay,
"USER",
),
}, nil
}
// TODO clArk changes the onboard flow (2 rounds ?)
func (s *covenantlessService) Onboard(
ctx context.Context, boardingTx string,
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
) error {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(boardingTx), true)
func (s *covenantlessService) GetBoardingAddress(
ctx context.Context, userPubkey *secp256k1.PublicKey,
) (string, error) {
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
if err != nil {
return fmt.Errorf("failed to parse boarding tx: %s", err)
return "", fmt.Errorf("failed to compute boarding script: %s", err)
}
if err := bitcointree.ValidateCongestionTree(
congestionTree, boardingTx, s.pubkey, s.roundLifetime, int64(s.minRelayFee),
); err != nil {
return err
}
extracted, err := psbt.Extract(ptx)
if err != nil {
return fmt.Errorf("failed to extract boarding tx: %s", err)
}
var serialized bytes.Buffer
if err := extracted.Serialize(&serialized); err != nil {
return fmt.Errorf("failed to serialize boarding tx: %s", err)
}
txid, err := s.wallet.BroadcastTransaction(ctx, hex.EncodeToString(serialized.Bytes()))
if err != nil {
return fmt.Errorf("failed to broadcast boarding tx: %s", err)
}
log.Debugf("broadcasted boarding tx %s", txid)
s.onboardingCh <- onboarding{
tx: boardingTx,
congestionTree: congestionTree,
userPubkey: userPubkey,
}
return nil
return addr, nil
}
func (s *covenantlessService) RegisterCosignerPubkey(ctx context.Context, paymentId string, pubkey string) error {
@@ -504,7 +615,7 @@ func (s *covenantlessService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, cosigners := s.paymentRequests.pop(num)
payments, boardingInputs, cosigners := s.paymentRequests.pop(num)
if len(payments) > len(cosigners) {
err := fmt.Errorf("missing ephemeral key for payments")
round.Fail(fmt.Errorf("round aborted: %s", err))
@@ -534,7 +645,7 @@ func (s *covenantlessService) startFinalization() {
cosigners = append(cosigners, ephemeralKey.PubKey())
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds, cosigners...)
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, boardingInputs, s.minRelayFee, sweptRounds, cosigners...)
if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx")
@@ -683,14 +794,25 @@ func (s *covenantlessService) startFinalization() {
tree = signedTree
}
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
return
needForfeits := false
for _, pay := range payments {
if len(pay.Inputs) > 0 {
needForfeits = true
break
}
}
log.Debugf("forfeit transactions created for round %s", round.Id)
var forfeitTxs, connectors []string
if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
return
}
log.Debugf("forfeit transactions created for round %s", round.Id)
}
if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedPoolTx,
@@ -763,73 +885,61 @@ func (s *covenantlessService) finalizeRound() {
}
log.Debugf("signing round transaction %s\n", round.Id)
signedPoolTx, err := s.wallet.SignTransaction(ctx, round.UnsignedTx, true)
boardingInputs := make([]int, 0)
roundTx, err := psbt.NewFromRawBytes(strings.NewReader(round.UnsignedTx), true)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to parse round tx: %s", err))
log.WithError(err).Warn("failed to parse round tx")
return
}
for i, in := range roundTx.Inputs {
if len(in.TaprootLeafScript) > 0 {
if len(in.TaprootScriptSpendSig) == 0 {
err = fmt.Errorf("missing tapscript spend sig for input %d", i)
changes = round.Fail(err)
log.WithError(err).Warn("missing boarding sig")
return
}
boardingInputs = append(boardingInputs, i)
}
}
signedRoundTx := round.UnsignedTx
if len(boardingInputs) > 0 {
signedRoundTx, err = s.wallet.SignTransactionTapscript(ctx, signedRoundTx, boardingInputs)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
}
signedRoundTx, err = s.wallet.SignTransaction(ctx, signedRoundTx, true)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err))
log.WithError(err).Warn("failed to sign round tx")
return
}
txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx)
txid, err := s.wallet.BroadcastTransaction(ctx, signedRoundTx)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err))
log.WithError(err).Warn("failed to broadcast pool tx")
return
}
changes, _ = round.EndFinalization(forfeitTxs, txid)
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
}
func (s *covenantlessService) listenToOnboarding() {
for onboarding := range s.onboardingCh {
go s.handleOnboarding(onboarding)
}
}
func (s *covenantlessService) handleOnboarding(onboarding onboarding) {
ctx := context.Background()
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(onboarding.tx), true)
txid := ptx.UnsignedTx.TxHash().String()
// wait for the tx to be confirmed with a timeout
timeout := time.NewTimer(15 * time.Minute)
defer timeout.Stop()
isConfirmed := false
for !isConfirmed {
select {
case <-timeout.C:
log.WithError(fmt.Errorf("operation timed out")).Warnf("failed to get confirmation for boarding tx %s", txid)
return
default:
var err error
isConfirmed, _, err = s.wallet.IsTransactionConfirmed(ctx, txid)
if err != nil {
log.WithError(err).Warn("failed to check tx confirmation")
}
if err != nil || !isConfirmed {
log.Debugf("waiting for boarding tx %s to be confirmed", txid)
time.Sleep(5 * time.Second)
}
}
}
log.Debugf("boarding tx %s confirmed", txid)
pubkey := hex.EncodeToString(onboarding.userPubkey.SerializeCompressed())
payments := getPaymentsFromOnboardingBitcoin(onboarding.congestionTree, pubkey)
round := domain.NewFinalizedRound(
dustAmount, pubkey, txid, onboarding.tx, onboarding.congestionTree, payments,
)
if err := s.saveEvents(ctx, round.Id, round.Events()); err != nil {
log.WithError(err).Warn("failed to store new round events")
changes, err = round.EndFinalization(forfeitTxs, txid)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
log.WithError(err).Warn("failed to finalize round")
return
}
log.Debugf("finalized round %s with pool tx %s", round.Id, round.Txid)
}
func (s *covenantlessService) listenToScannerNotifications() {
@@ -1232,24 +1342,6 @@ func (s *covenantlessService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
}
func getPaymentsFromOnboardingBitcoin(
congestionTree tree.CongestionTree, userKey string,
) []domain.Payment {
leaves := congestionTree.Leaves()
receivers := make([]domain.Receiver, 0, len(leaves))
for _, node := range leaves {
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(node.Tx), true)
receiver := domain.Receiver{
Pubkey: userKey,
Amount: uint64(ptx.UnsignedTx.TxOut[0].Value),
}
receivers = append(receivers, receiver)
}
payment := domain.NewPaymentUnsafe(nil, receivers)
return []domain.Payment{*payment}
}
func findForfeitTxBitcoin(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
) (string, error) {

View File

@@ -2,8 +2,9 @@ package application
import (
"context"
"fmt"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
@@ -16,9 +17,10 @@ var (
type Service interface {
Start() error
Stop()
SpendVtxos(ctx context.Context, inputs []domain.VtxoKey) (string, error)
SpendVtxos(ctx context.Context, inputs []Input) (string, error)
ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error
SignVtxos(ctx context.Context, forfeitTxs []string) error
SignRoundTx(ctx context.Context, roundTx string) error
GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error)
GetRoundById(ctx context.Context, id string) (*domain.Round, error)
GetCurrentRound(ctx context.Context) (*domain.Round, error)
@@ -30,10 +32,6 @@ type Service interface {
ctx context.Context, pubkey *secp256k1.PublicKey,
) (spendableVtxos, spentVtxos []domain.Vtxo, err error)
GetInfo(ctx context.Context) (*ServiceInfo, error)
Onboard(
ctx context.Context, boardingTx string,
congestionTree tree.CongestionTree, userPubkey *secp256k1.PublicKey,
) error
// Async payments
CreateAsyncPayment(
ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver,
@@ -41,6 +39,7 @@ type Service interface {
CompleteAsyncPayment(
ctx context.Context, redeemTx string, unconditionalForfeitTxs []string,
) error
GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error)
// Tree signing methods
RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error
RegisterCosignerNonces(
@@ -54,12 +53,13 @@ type Service interface {
}
type ServiceInfo struct {
PubKey string
RoundLifetime int64
UnilateralExitDelay int64
RoundInterval int64
Network string
MinRelayFee int64
PubKey string
RoundLifetime int64
UnilateralExitDelay int64
RoundInterval int64
Network string
MinRelayFee int64
BoardingDescriptorTemplate string
}
type WalletStatus struct {
@@ -68,10 +68,28 @@ type WalletStatus struct {
IsSynced bool
}
type onboarding struct {
tx string
congestionTree tree.CongestionTree
userPubkey *secp256k1.PublicKey
type Input struct {
Txid string
Index uint32
Descriptor string
}
func (i Input) IsVtxo() bool {
return len(i.Descriptor) <= 0
}
func (i Input) VtxoKey() domain.VtxoKey {
return domain.VtxoKey{
Txid: i.Txid,
VOut: i.Index,
}
}
func (i Input) GetDescriptor() (*descriptor.TaprootDescriptor, error) {
if i.IsVtxo() {
return nil, fmt.Errorf("input is not a boarding input")
}
return descriptor.ParseTaprootDescriptor(i.Descriptor)
}
type txOutpoint struct {

View File

@@ -10,14 +10,16 @@ import (
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/sirupsen/logrus"
)
type timedPayment struct {
domain.Payment
timestamp time.Time
pingTimestamp time.Time
boardingInputs []ports.BoardingInput
timestamp time.Time
pingTimestamp time.Time
}
type paymentsMap struct {
@@ -26,11 +28,8 @@ type paymentsMap struct {
ephemeralKeys map[string]*secp256k1.PublicKey
}
func newPaymentsMap(payments []domain.Payment) *paymentsMap {
func newPaymentsMap() *paymentsMap {
paymentsById := make(map[string]*timedPayment)
for _, p := range payments {
paymentsById[p.Id] = &timedPayment{p, time.Now(), time.Time{}}
}
lock := &sync.RWMutex{}
return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)}
}
@@ -60,7 +59,7 @@ func (m *paymentsMap) delete(id string) error {
return nil
}
func (m *paymentsMap) push(payment domain.Payment) error {
func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.BoardingInput) error {
m.lock.Lock()
defer m.lock.Unlock()
@@ -68,7 +67,7 @@ func (m *paymentsMap) push(payment domain.Payment) error {
return fmt.Errorf("duplicated inputs")
}
m.payments[payment.Id] = &timedPayment{payment, time.Now(), time.Time{}}
m.payments[payment.Id] = &timedPayment{payment, boardingInputs, time.Now(), time.Time{}}
return nil
}
@@ -84,7 +83,7 @@ func (m *paymentsMap) pushEphemeralKey(paymentId string, pubkey *secp256k1.Publi
return nil
}
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey) {
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, []*secp256k1.PublicKey) {
m.lock.Lock()
defer m.lock.Unlock()
@@ -109,8 +108,10 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey)
}
payments := make([]domain.Payment, 0, num)
boardingInputs := make([]ports.BoardingInput, 0)
cosigners := make([]*secp256k1.PublicKey, 0, num)
for _, p := range paymentsByTime[:num] {
boardingInputs = append(boardingInputs, p.boardingInputs...)
payments = append(payments, p.Payment)
if pubkey, ok := m.ephemeralKeys[p.Payment.Id]; ok {
cosigners = append(cosigners, pubkey)
@@ -118,7 +119,7 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []*secp256k1.PublicKey)
}
delete(m.payments, p.Id)
}
return payments, cosigners
return payments, boardingInputs, cosigners
}
func (m *paymentsMap) update(payment domain.Payment) error {
@@ -312,3 +313,26 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
}
return vtxos
}
type boardingInput struct {
txId chainhash.Hash
vout uint32
boardingPubKey *secp256k1.PublicKey
amount uint64
}
func (b boardingInput) GetHash() chainhash.Hash {
return b.txId
}
func (b boardingInput) GetIndex() uint32 {
return b.vout
}
func (b boardingInput) GetAmount() uint64 {
return b.amount
}
func (b boardingInput) GetBoardingPubkey() *secp256k1.PublicKey {
return b.boardingPubKey
}

View File

@@ -28,14 +28,6 @@ func NewPayment(inputs []Vtxo) (*Payment, error) {
return p, nil
}
func NewPaymentUnsafe(inputs []Vtxo, receivers []Receiver) *Payment {
return &Payment{
Id: uuid.New().String(),
Inputs: inputs,
Receivers: receivers,
}
}
func (p *Payment) AddReceivers(receivers []Receiver) (err error) {
if p.Receivers == nil {
p.Receivers = make([]Receiver, 0)
@@ -70,18 +62,13 @@ func (p Payment) validate(ignoreOuts bool) error {
if len(p.Id) <= 0 {
return fmt.Errorf("missing id")
}
if len(p.Inputs) <= 0 {
return fmt.Errorf("missing inputs")
}
if ignoreOuts {
return nil
}
if len(p.Receivers) <= 0 {
return fmt.Errorf("missing outputs")
}
// Check that input and output and output amounts match.
inAmount := p.TotalInputAmount()
outAmount := uint64(0)
for _, r := range p.Receivers {
if len(r.OnchainAddress) <= 0 && len(r.Pubkey) <= 0 {
return fmt.Errorf("missing receiver destination")
@@ -89,10 +76,6 @@ func (p Payment) validate(ignoreOuts bool) error {
if r.Amount < dustAmount {
return fmt.Errorf("receiver amount must be greater than dust")
}
outAmount += r.Amount
}
if inAmount != outAmount {
return fmt.Errorf("input and output amounts mismatch")
}
return nil
}

View File

@@ -22,7 +22,7 @@ var inputs = []domain.Vtxo{
func TestPayment(t *testing.T) {
t.Run("new_payment", func(t *testing.T) {
t.Run("vaild", func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
payment, err := domain.NewPayment(inputs)
require.NoError(t, err)
require.NotNil(t, payment)
@@ -30,24 +30,6 @@ func TestPayment(t *testing.T) {
require.Exactly(t, inputs, payment.Inputs)
require.Empty(t, payment.Receivers)
})
t.Run("invaild", func(t *testing.T) {
fixtures := []struct {
inputs []domain.Vtxo
expectedErr string
}{
{
inputs: nil,
expectedErr: "missing inputs",
},
}
for _, f := range fixtures {
payment, err := domain.NewPayment(f.inputs)
require.EqualError(t, err, f.expectedErr)
require.Nil(t, payment)
}
})
})
t.Run("add_receivers", func(t *testing.T) {
@@ -87,15 +69,6 @@ func TestPayment(t *testing.T) {
},
expectedErr: "receiver amount must be greater than dust",
},
{
receivers: []domain.Receiver{
{
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001",
Amount: 600,
},
},
expectedErr: "input and output amounts mismatch",
},
}
payment, err := domain.NewPayment(inputs)

View File

@@ -60,39 +60,6 @@ func NewRound(dustAmount uint64) *Round {
}
}
func NewFinalizedRound(
dustAmount uint64, userKey, poolTxid, poolTx string,
congestionTree tree.CongestionTree, payments []Payment,
) *Round {
r := NewRound(dustAmount)
events := []RoundEvent{
RoundStarted{
Id: r.Id,
Timestamp: time.Now().Unix(),
},
PaymentsRegistered{
Id: r.Id,
Payments: payments,
},
RoundFinalizationStarted{
Id: r.Id,
CongestionTree: congestionTree,
PoolTx: poolTx,
},
RoundFinalized{
Id: r.Id,
Txid: poolTxid,
Timestamp: time.Now().Unix(),
},
}
for _, event := range events {
r.raise(event)
}
return r
}
func NewRoundFromEvents(events []RoundEvent) *Round {
r := &Round{}
@@ -205,7 +172,11 @@ func (r *Round) StartFinalization(connectorAddress string, connectors []string,
func (r *Round) EndFinalization(forfeitTxs []string, txid string) ([]RoundEvent, error) {
if len(forfeitTxs) <= 0 {
return nil, fmt.Errorf("missing list of signed forfeit txs")
for _, p := range r.Payments {
if len(p.Inputs) > 0 {
return nil, fmt.Errorf("missing list of signed forfeit txs")
}
}
}
if len(txid) <= 0 {
return nil, fmt.Errorf("missing pool txid")
@@ -216,6 +187,10 @@ func (r *Round) EndFinalization(forfeitTxs []string, txid string) ([]RoundEvent,
if r.Stage.Ended {
return nil, fmt.Errorf("round already finalized")
}
if forfeitTxs == nil {
forfeitTxs = make([]string, 0)
}
event := RoundFinalized{
Id: r.Id,
Txid: txid,

View File

@@ -449,6 +449,7 @@ func testEndFinalization(t *testing.T) {
Stage: domain.Stage{
Code: domain.FinalizationStage,
},
Payments: paymentsById,
},
forfeitTxs: nil,
txid: txid,

View File

@@ -16,9 +16,16 @@ type SweepInput interface {
GetInternalKey() *secp256k1.PublicKey
}
type BoardingInput interface {
GetAmount() uint64
GetIndex() uint32
GetHash() chainhash.Hash
GetBoardingPubkey() *secp256k1.PublicKey
}
type TxBuilder interface {
BuildPoolTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round,
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, minRelayFee uint64, sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey,
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error)
BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFee uint64) (connectors []string, forfeitTxs []string, err error)
@@ -33,4 +40,6 @@ type TxBuilder interface {
vtxosToSpend []domain.Vtxo,
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, minRelayFee uint64,
) (*domain.AsyncPaymentTxs, error)
GetBoardingScript(userPubkey, aspPubkey *secp256k1.PublicKey) (addr string, script []byte, err error)
VerifyAndCombinePartialTx(dest string, src string) (string, error)
}

View File

@@ -33,6 +33,7 @@ type WalletService interface {
MainAccountBalance(ctx context.Context) (uint64, uint64, error)
ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error)
LockConnectorUtxos(ctx context.Context, utxos []TxOutpoint) error
GetTransaction(ctx context.Context, txid string) (string, error)
Close()
}

View File

@@ -95,7 +95,7 @@ func deserializeEvent(buf []byte) (domain.RoundEvent, error) {
}
{
var event = domain.RoundFinalizationStarted{}
if err := json.Unmarshal(buf, &event); err == nil && len(event.Connectors) > 0 {
if err := json.Unmarshal(buf, &event); err == nil && len(event.PoolTx) > 0 {
return event, nil
}
}

View File

@@ -11,12 +11,15 @@ import (
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
)
const (
@@ -25,10 +28,11 @@ const (
)
type txBuilder struct {
wallet ports.WalletService
net common.Network
roundLifetime int64 // in seconds
exitDelay int64 // in seconds
wallet ports.WalletService
net common.Network
roundLifetime int64 // in seconds
exitDelay int64 // in seconds
boardingExitDelay int64 // in seconds
}
func NewTxBuilder(
@@ -36,8 +40,18 @@ func NewTxBuilder(
net common.Network,
roundLifetime int64,
exitDelay int64,
boardingExitDelay int64,
) ports.TxBuilder {
return &txBuilder{wallet, net, roundLifetime, exitDelay}
return &txBuilder{wallet, net, roundLifetime, exitDelay, boardingExitDelay}
}
func (b *txBuilder) GetBoardingScript(owner, asp *secp256k1.PublicKey) (string, []byte, error) {
addr, script, _, err := b.getBoardingTaproot(owner, asp)
if err != nil {
return "", nil, err
}
return addr, script, nil
}
func (b *txBuilder) GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) {
@@ -112,7 +126,11 @@ func (b *txBuilder) BuildForfeitTxs(
}
func (b *txBuilder) BuildPoolTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round,
aspPubkey *secp256k1.PublicKey,
payments []domain.Payment,
boardingInputs []ports.BoardingInput,
minRelayFee uint64,
sweptRounds []domain.Round,
_ ...*secp256k1.PublicKey, // cosigners are not used in the covenant
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
// The creation of the tree and the pool tx are tightly coupled:
@@ -146,7 +164,7 @@ func (b *txBuilder) BuildPoolTx(
}
ptx, err := b.createPoolTx(
sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
sharedOutputAmount, sharedOutputScript, payments, boardingInputs, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
)
if err != nil {
return
@@ -197,9 +215,19 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
expirationTime := parentblocktime + lifetime
amount := uint64(0)
for _, out := range pset.Outputs {
amount += out.Value
txhex, err := b.wallet.GetTransaction(context.Background(), txid)
if err != nil {
return -1, nil, err
}
tx, err := transaction.NewTxFromHex(txhex)
if err != nil {
return -1, nil, err
}
inputValue, err := elementsutil.ValueFromBytes(tx.Outputs[index].Value)
if err != nil {
return -1, nil, err
}
sweepInput = &sweepLiquidInput{
@@ -208,7 +236,7 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
TxIndex: index,
},
sweepLeaf: sweepLeaf,
amount: amount,
amount: inputValue,
}
return expirationTime, sweepInput, nil
@@ -362,8 +390,11 @@ func (b *txBuilder) getLeafScriptAndTree(
}
func (b *txBuilder) createPoolTx(
sharedOutputAmount uint64, sharedOutputScript []byte,
payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
sharedOutputAmount uint64,
sharedOutputScript []byte,
payments []domain.Payment,
boardingInputs []ports.BoardingInput,
aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
sweptRounds []domain.Round,
) (*psetv2.Pset, error) {
aspScript, err := p2wpkhScript(aspPubKey, b.onchainNetwork())
@@ -396,11 +427,13 @@ func (b *txBuilder) createPoolTx(
})
}
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.onchainNetwork().AssetID,
Amount: connectorsAmount,
Script: connectorScript,
})
if connectorsAmount > 0 {
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.onchainNetwork().AssetID,
Amount: connectorsAmount,
Script: connectorScript,
})
}
for _, receiver := range receivers {
targetAmount += receiver.Amount
@@ -417,6 +450,9 @@ func (b *txBuilder) createPoolTx(
})
}
for _, in := range boardingInputs {
targetAmount -= in.GetAmount()
}
ctx := context.Background()
utxos, change, err := b.selectUtxos(ctx, sweptRounds, targetAmount)
if err != nil {
@@ -447,6 +483,48 @@ func (b *txBuilder) createPoolTx(
return nil, err
}
for _, in := range boardingInputs {
if err := updater.AddInputs(
[]psetv2.InputArgs{
{
Txid: in.GetHash().String(),
TxIndex: in.GetIndex(),
},
},
); err != nil {
return nil, err
}
index := len(ptx.Inputs) - 1
assetBytes, err := elementsutil.AssetHashToBytes(b.onchainNetwork().AssetID)
if err != nil {
return nil, fmt.Errorf("failed to convert asset to bytes: %s", err)
}
valueBytes, err := elementsutil.ValueToBytes(in.GetAmount())
if err != nil {
return nil, fmt.Errorf("failed to convert value to bytes: %s", err)
}
_, script, tapLeafProof, err := b.getBoardingTaproot(in.GetBoardingPubkey(), aspPubKey)
if err != nil {
return nil, err
}
if err := updater.AddInWitnessUtxo(index, transaction.NewTxOutput(assetBytes, valueBytes, script)); err != nil {
return nil, err
}
if err := updater.AddInTapLeafScript(index, psetv2.NewTapLeafScript(*tapLeafProof, tree.UnspendableKey())); err != nil {
return nil, err
}
if err := updater.AddInSighashType(index, txscript.SigHashDefault); err != nil {
return nil, err
}
}
if err := addInputs(updater, utxos); err != nil {
return nil, err
}
@@ -471,14 +549,23 @@ func (b *txBuilder) createPoolTx(
if feeAmount == change {
// fees = change, remove change output
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
ptx.Global.OutputCount--
feeAmount += change
} else if feeAmount < change {
// change covers the fees, reduce change amount
ptx.Outputs[len(ptx.Outputs)-1].Value = change - feeAmount
if change-feeAmount < dustLimit {
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
ptx.Global.OutputCount--
feeAmount += change
} else {
ptx.Outputs[len(ptx.Outputs)-1].Value = change - feeAmount
}
} else {
// change is not enough to cover fees, re-select utxos
if change > 0 {
// remove change output if present
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
ptx.Global.OutputCount--
}
newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-change)
if err != nil {
@@ -486,14 +573,18 @@ func (b *txBuilder) createPoolTx(
}
if change > 0 {
if err := updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: b.onchainNetwork().AssetID,
Amount: change,
Script: aspScript,
},
}); err != nil {
return nil, err
if change < dustLimit {
feeAmount += change
} else {
if err := updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: b.onchainNetwork().AssetID,
Amount: change,
Script: aspScript,
},
}); err != nil {
return nil, err
}
}
}
@@ -541,6 +632,77 @@ func (b *txBuilder) createPoolTx(
return ptx, nil
}
// This method aims to verify and add partial signature from boarding input
func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string, error) {
roundPset, err := psetv2.NewPsetFromBase64(dest)
if err != nil {
return "", err
}
sourcePset, err := psetv2.NewPsetFromBase64(src)
if err != nil {
return "", err
}
roundUtx, err := roundPset.UnsignedTx()
if err != nil {
return "", err
}
sourceUtx, err := sourcePset.UnsignedTx()
if err != nil {
return "", err
}
if roundUtx.TxHash().String() != sourceUtx.TxHash().String() {
return "", fmt.Errorf("txid mismatch")
}
roundSigner, err := psetv2.NewSigner(roundPset)
if err != nil {
return "", err
}
for i, input := range sourcePset.Inputs {
if len(input.TapScriptSig) == 0 || len(input.TapLeafScript) == 0 {
continue
}
partialSig := input.TapScriptSig[0]
leafHash, err := chainhash.NewHash(partialSig.LeafHash)
if err != nil {
return "", err
}
preimage, err := b.getTaprootPreimage(src, i, leafHash)
if err != nil {
return "", err
}
sig, err := schnorr.ParseSignature(partialSig.Signature)
if err != nil {
return "", err
}
pubkey, err := schnorr.ParsePubKey(partialSig.PubKey)
if err != nil {
return "", err
}
if !sig.Verify(preimage, pubkey) {
return "", fmt.Errorf("invalid signature")
}
if err := roundSigner.SignTaprootInputTapscriptSig(i, partialSig); err != nil {
return "", err
}
}
return roundSigner.Pset.ToBase64()
}
func (b *txBuilder) createConnectors(
poolTx string, payments []domain.Payment, connectorAddress string, minRelayFee uint64,
) ([]*psetv2.Pset, error) {
@@ -733,6 +895,52 @@ func (b *txBuilder) onchainNetwork() *network.Network {
}
}
func (b *txBuilder) getBoardingTaproot(owner, asp *secp256k1.PublicKey) (string, []byte, *taproot.TapscriptElementsProof, error) {
multisigClosure := tree.ForfeitClosure{
Pubkey: owner,
AspPubkey: asp,
}
csvClosure := tree.CSVSigClosure{
Pubkey: owner,
Seconds: uint(b.boardingExitDelay),
}
multisigLeaf, err := multisigClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
csvLeaf, err := csvClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
tapTree := taproot.AssembleTaprootScriptTree(*multisigLeaf, *csvLeaf)
root := tapTree.RootNode.TapHash()
tapKey := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), root[:])
p2tr, err := payment.FromTweakedKey(tapKey, b.onchainNetwork(), nil)
if err != nil {
return "", nil, nil, err
}
addr, err := p2tr.TaprootAddress()
if err != nil {
return "", nil, nil, err
}
tapLeaf, err := multisigClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
leafProofIndex := tapTree.LeafProofIndex[tapLeaf.TapHash()]
leafProof := tapTree.LeafMerkleProofs[leafProofIndex]
return addr, p2tr.Script, &leafProof, nil
}
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) {
for _, leaf := range input.TapLeafScript {
closure := &tree.CSVSigClosure{}

View File

@@ -25,6 +25,7 @@ const (
minRelayFee = uint64(30)
roundLifetime = int64(1209344)
unilateralExitDelay = int64(512)
boardingExitDelay = int64(512)
)
var (
@@ -49,7 +50,7 @@ func TestMain(m *testing.M) {
func TestBuildPoolTx(t *testing.T) {
builder := txbuilder.NewTxBuilder(
wallet, common.Liquid, roundLifetime, unilateralExitDelay,
wallet, common.Liquid, roundLifetime, unilateralExitDelay, boardingExitDelay,
)
fixtures, err := parsePoolTxFixtures()
@@ -60,7 +61,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee, []domain.Round{},
pubkey, f.Payments, []ports.BoardingInput{}, minRelayFee, []domain.Round{},
)
require.NoError(t, err)
require.NotEmpty(t, poolTx)
@@ -81,7 +82,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee, []domain.Round{},
pubkey, f.Payments, []ports.BoardingInput{}, minRelayFee, []domain.Round{},
)
require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, poolTx)
@@ -94,7 +95,7 @@ func TestBuildPoolTx(t *testing.T) {
func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder(
wallet, common.Liquid, 1209344, unilateralExitDelay,
wallet, common.Liquid, 1209344, unilateralExitDelay, boardingExitDelay,
)
fixtures, err := parseForfeitTxsFixtures()

View File

@@ -229,6 +229,16 @@ func (m *mockedWallet) MainAccountBalance(ctx context.Context) (uint64, uint64,
return res, res2, args.Error(2)
}
func (m *mockedWallet) GetTransaction(ctx context.Context, txid string) (string, error) {
args := m.Called(ctx, txid)
var res string
if a := args.Get(0); a != nil {
res = a.(string)
}
return res, args.Error(1)
}
type mockedInput struct {
mock.Mock
}

View File

@@ -28,16 +28,17 @@ const (
)
type txBuilder struct {
wallet ports.WalletService
net common.Network
roundLifetime int64 // in seconds
exitDelay int64 // in seconds
wallet ports.WalletService
net common.Network
roundLifetime int64 // in seconds
exitDelay int64 // in seconds
boardingExitDelay int64 // in seconds
}
func NewTxBuilder(
wallet ports.WalletService, net common.Network, roundLifetime int64, exitDelay int64,
wallet ports.WalletService, net common.Network, roundLifetime, exitDelay, boardingExitDelay int64,
) ports.TxBuilder {
return &txBuilder{wallet, net, roundLifetime, exitDelay}
return &txBuilder{wallet, net, roundLifetime, exitDelay, boardingExitDelay}
}
func (b *txBuilder) VerifyForfeitTx(tx string) (bool, string, error) {
@@ -45,6 +46,7 @@ func (b *txBuilder) VerifyForfeitTx(tx string) (bool, string, error) {
txid := ptx.UnsignedTx.TxHash().String()
for index, input := range ptx.Inputs {
// TODO (@louisinger): verify control block
for _, tapScriptSig := range input.TaprootScriptSpendSig {
preimage, err := b.getTaprootPreimage(
tx,
@@ -178,7 +180,12 @@ func (b *txBuilder) BuildForfeitTxs(
}
func (b *txBuilder) BuildPoolTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round, cosigners ...*secp256k1.PublicKey,
aspPubkey *secp256k1.PublicKey,
payments []domain.Payment,
boardingInputs []ports.BoardingInput,
minRelayFee uint64,
sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey,
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
var sharedOutputScript []byte
var sharedOutputAmount int64
@@ -204,7 +211,7 @@ func (b *txBuilder) BuildPoolTx(
}
ptx, err := b.createPoolTx(
sharedOutputAmount, sharedOutputScript, payments, connectorAddress, minRelayFee, sweptRounds,
aspPubkey, sharedOutputAmount, sharedOutputScript, payments, boardingInputs, connectorAddress, minRelayFee, sweptRounds,
)
if err != nil {
return
@@ -253,9 +260,14 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
expirationTime := parentblocktime + lifetime
amount := int64(0)
for _, out := range partialTx.UnsignedTx.TxOut {
amount += out.Value
txhex, err := b.wallet.GetTransaction(context.Background(), txid.String())
if err != nil {
return -1, nil, err
}
var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
return -1, nil, err
}
sweepInput = &sweepBitcoinInput{
@@ -265,7 +277,7 @@ func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expira
},
internalPubkey: internalKey,
sweepLeaf: sweepLeaf,
amount: amount,
amount: tx.TxOut[index].Value,
}
return expirationTime, sweepInput, nil
@@ -468,6 +480,15 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
}, nil
}
func (b *txBuilder) GetBoardingScript(userPubkey, aspPubkey *secp256k1.PublicKey) (string, []byte, error) {
addr, script, _, err := b.craftBoardingTaproot(userPubkey, aspPubkey)
if err != nil {
return "", nil, err
}
return addr, script, nil
}
func (b *txBuilder) getLeafScriptAndTree(
userPubkey, aspPubkey *secp256k1.PublicKey,
) ([]byte, *txscript.IndexedTapScriptTree, error) {
@@ -508,8 +529,9 @@ func (b *txBuilder) getLeafScriptAndTree(
}
func (b *txBuilder) createPoolTx(
aspPubKey *secp256k1.PublicKey,
sharedOutputAmount int64, sharedOutputScript []byte,
payments []domain.Payment, connectorAddress string, minRelayFee uint64,
payments []domain.Payment, boardingInputs []ports.BoardingInput, connectorAddress string, minRelayFee uint64,
sweptRounds []domain.Round,
) (*psbt.Packet, error) {
connectorAddr, err := btcutil.DecodeAddress(connectorAddress, b.onchainNetwork())
@@ -541,10 +563,12 @@ func (b *txBuilder) createPoolTx(
})
}
outputs = append(outputs, &wire.TxOut{
Value: int64(connectorAmount),
PkScript: connectorScript,
})
if connectorsAmount > 0 {
outputs = append(outputs, &wire.TxOut{
Value: int64(connectorsAmount),
PkScript: connectorScript,
})
}
for _, receiver := range receivers {
targetAmount += receiver.Amount
@@ -565,6 +589,10 @@ func (b *txBuilder) createPoolTx(
})
}
for _, input := range boardingInputs {
targetAmount -= input.GetAmount()
}
ctx := context.Background()
utxos, change, err := b.selectUtxos(ctx, sweptRounds, targetAmount)
if err != nil {
@@ -601,6 +629,9 @@ func (b *txBuilder) createPoolTx(
ins := make([]*wire.OutPoint, 0)
nSequences := make([]uint32, 0)
witnessUtxos := make(map[int]*wire.TxOut)
boardingTapLeaves := make(map[int]*psbt.TaprootTapLeafScript)
nextIndex := 0
for _, utxo := range utxos {
txhash, err := chainhash.NewHashFromStr(utxo.GetTxid())
@@ -613,6 +644,37 @@ func (b *txBuilder) createPoolTx(
Index: utxo.GetIndex(),
})
nSequences = append(nSequences, wire.MaxTxInSequenceNum)
script, err := hex.DecodeString(utxo.GetScript())
if err != nil {
return nil, err
}
witnessUtxos[nextIndex] = &wire.TxOut{
Value: int64(utxo.GetValue()),
PkScript: script,
}
nextIndex++
}
for _, input := range boardingInputs {
ins = append(ins, &wire.OutPoint{
Hash: input.GetHash(),
Index: input.GetIndex(),
})
nSequences = append(nSequences, wire.MaxTxInSequenceNum)
_, script, tapLeaf, err := b.craftBoardingTaproot(input.GetBoardingPubkey(), aspPubKey)
if err != nil {
return nil, err
}
boardingTapLeaves[nextIndex] = tapLeaf
witnessUtxos[nextIndex] = &wire.TxOut{
Value: int64(input.GetAmount()),
PkScript: script,
}
nextIndex++
}
ptx, err := psbt.New(ins, outputs, 2, 0, nSequences)
@@ -624,20 +686,20 @@ func (b *txBuilder) createPoolTx(
if err != nil {
return nil, err
}
for inIndex, utxo := range utxos {
script, err := hex.DecodeString(utxo.GetScript())
if err != nil {
return nil, err
}
if err := updater.AddInWitnessUtxo(&wire.TxOut{
Value: int64(utxo.GetValue()),
PkScript: script,
}, inIndex); err != nil {
for inIndex, utxo := range witnessUtxos {
if err := updater.AddInWitnessUtxo(utxo, inIndex); err != nil {
return nil, err
}
}
unspendableInternalKey := schnorr.SerializePubKey(bitcointree.UnspendableKey())
for inIndex, tapLeaf := range boardingTapLeaves {
updater.Upsbt.Inputs[inIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapLeaf}
updater.Upsbt.Inputs[inIndex].TaprootInternalKey = unspendableInternalKey
}
b64, err := ptx.B64Encode()
if err != nil {
return nil, err
@@ -793,6 +855,62 @@ func (b *txBuilder) createPoolTx(
return ptx, nil
}
func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string, error) {
roundTx, err := psbt.NewFromRawBytes(strings.NewReader(dest), true)
if err != nil {
return "", err
}
sourceTx, err := psbt.NewFromRawBytes(strings.NewReader(src), true)
if err != nil {
return "", err
}
if sourceTx.UnsignedTx.TxHash().String() != roundTx.UnsignedTx.TxHash().String() {
return "", fmt.Errorf("txids do not match")
}
for i, in := range sourceTx.Inputs {
isMultisigTaproot := len(in.TaprootLeafScript) > 0
if isMultisigTaproot {
// check if the source tx signs the leaf
sourceInput := sourceTx.Inputs[i]
if len(sourceInput.TaprootScriptSpendSig) == 0 {
continue
}
partialSig := sourceInput.TaprootScriptSpendSig[0]
preimage, err := b.getTaprootPreimage(src, i, sourceInput.TaprootLeafScript[0].Script)
if err != nil {
return "", err
}
sig, err := schnorr.ParseSignature(partialSig.Signature)
if err != nil {
return "", err
}
pubkey, err := schnorr.ParsePubKey(partialSig.XOnlyPubKey)
if err != nil {
return "", err
}
if !sig.Verify(preimage, pubkey) {
return "", fmt.Errorf(
"invalid signature for input %s:%d",
sourceTx.UnsignedTx.TxIn[i].PreviousOutPoint.Hash.String(),
sourceTx.UnsignedTx.TxIn[i].PreviousOutPoint.Index,
)
}
roundTx.Inputs[i].TaprootScriptSpendSig = sourceInput.TaprootScriptSpendSig
}
}
return roundTx.B64Encode()
}
func (b *txBuilder) createConnectors(
poolTx string, payments []domain.Payment, connectorScript []byte, minRelayFee uint64,
) ([]*psbt.Packet, error) {
@@ -858,9 +976,7 @@ func (b *txBuilder) createConnectors(
func (b *txBuilder) createForfeitTxs(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psbt.Packet, minRelayFee uint64,
) ([]string, error) {
// TODO (@louisinger): are we sure about this change?
aspScript, err := p2trScript(aspPubkey, b.onchainNetwork())
// aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork())
if err != nil {
return nil, err
}
@@ -1026,6 +1142,61 @@ func (b *txBuilder) onchainNetwork() *chaincfg.Params {
}
}
// craftBoardingTaproot returns the addr, script and the leaf belonging to the ASP
func (b *txBuilder) craftBoardingTaproot(userPubkey, aspPubkey *secp256k1.PublicKey) (string, []byte, *psbt.TaprootTapLeafScript, error) {
multisigClosure := bitcointree.MultisigClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
csvClosure := bitcointree.CSVSigClosure{
Pubkey: userPubkey,
Seconds: uint(b.boardingExitDelay),
}
multisigLeaf, err := multisigClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
csvLeaf, err := csvClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
tree := txscript.AssembleTaprootScriptTree(*multisigLeaf, *csvLeaf)
root := tree.RootNode.TapHash()
taprootKey := txscript.ComputeTaprootOutputKey(bitcointree.UnspendableKey(), root[:])
script, err := txscript.PayToTaprootScript(taprootKey)
if err != nil {
return "", nil, nil, err
}
addr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(taprootKey), b.onchainNetwork())
if err != nil {
return "", nil, nil, err
}
proofIndex := tree.LeafProofIndex[multisigLeaf.TapHash()]
proof := tree.LeafMerkleProofs[proofIndex]
ctrlBlock := proof.ToControlBlock(bitcointree.UnspendableKey())
ctrlBlockBytes, err := ctrlBlock.ToBytes()
if err != nil {
return "", nil, nil, err
}
tapLeaf := &psbt.TaprootTapLeafScript{
ControlBlock: ctrlBlockBytes,
Script: multisigLeaf.Script,
LeafVersion: txscript.BaseLeafVersion,
}
return addr.String(), script, tapLeaf, nil
}
func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint {
outpoints := make([]ports.TxOutpoint, 0, len(inputs))
for _, input := range inputs {

View File

@@ -25,6 +25,7 @@ const (
minRelayFee = uint64(30)
roundLifetime = int64(1209344)
unilateralExitDelay = int64(512)
boardingExitDelay = int64(512)
)
var (
@@ -49,7 +50,7 @@ func TestMain(m *testing.M) {
func TestBuildPoolTx(t *testing.T) {
builder := txbuilder.NewTxBuilder(
wallet, common.Bitcoin, roundLifetime, unilateralExitDelay,
wallet, common.Bitcoin, roundLifetime, unilateralExitDelay, boardingExitDelay,
)
fixtures, err := parsePoolTxFixtures()
@@ -72,7 +73,7 @@ func TestBuildPoolTx(t *testing.T) {
}
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee, []domain.Round{}, cosigners...,
pubkey, f.Payments, []ports.BoardingInput{}, minRelayFee, []domain.Round{}, cosigners...,
)
require.NoError(t, err)
require.NotEmpty(t, poolTx)
@@ -93,7 +94,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee, []domain.Round{},
pubkey, f.Payments, []ports.BoardingInput{}, minRelayFee, []domain.Round{},
)
require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, poolTx)
@@ -106,7 +107,7 @@ func TestBuildPoolTx(t *testing.T) {
func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder(
wallet, common.Bitcoin, 1209344, unilateralExitDelay,
wallet, common.Bitcoin, 1209344, unilateralExitDelay, boardingExitDelay,
)
fixtures, err := parseForfeitTxsFixtures()

View File

@@ -201,6 +201,16 @@ func (m *mockedWallet) WaitForSync(ctx context.Context, txid string) error {
return args.Error(0)
}
func (m *mockedWallet) GetTransaction(ctx context.Context, txid string) (string, error) {
args := m.Called(ctx, txid)
var res string
if a := args.Get(0); a != nil {
res = a.(string)
}
return res, args.Error(1)
}
func (m *mockedWallet) ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error) {
panic("not implemented")
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
log "github.com/sirupsen/logrus"
)
@@ -54,6 +55,31 @@ func (f *esploraClient) broadcast(txhex string) error {
return nil
}
func (f *esploraClient) getTx(txid string) (*wire.MsgTx, error) {
endpoint, err := url.JoinPath(f.url, "tx", txid, "raw")
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Get(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("tx endpoint HTTP error: " + resp.Status)
}
var tx wire.MsgTx
if err := tx.Deserialize(resp.Body); err != nil {
return nil, err
}
return &tx, nil
}
func (f *esploraClient) getTxStatus(txid string) (isConfirmed bool, blocktime int64, err error) {
endpoint, err := url.JoinPath(f.url, "tx", txid)
if err != nil {

View File

@@ -11,7 +11,7 @@ import (
log "github.com/sirupsen/logrus"
)
func (s *service) signPsbt(packet *psbt.Packet) ([]uint32, error) {
func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, error) {
// iterates over the inputs and set the default sighash flags
updater, err := psbt.NewUpdater(packet)
if err != nil {
@@ -54,6 +54,19 @@ func (s *service) signPsbt(packet *psbt.Packet) ([]uint32, error) {
continue
}
if len(inputsToSign) > 0 {
found := false
for _, i := range inputsToSign {
if i == idx {
found = true
break
}
}
if !found {
continue
}
}
var managedAddress waddrmgr.ManagedPubKeyAddress
var isTaproot bool

View File

@@ -10,8 +10,10 @@ import (
"time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
@@ -501,9 +503,11 @@ func (s *service) SelectUtxos(ctx context.Context, _ string, amount uint64) ([]p
selectedUtxos = append(selectedUtxos, coinTxInput{coin})
}
change := selectedAmount - amount
if selectedAmount < amount {
return nil, 0, fmt.Errorf("insufficient funds to select %d, only %d available", amount, selectedAmount)
}
return selectedUtxos, change, nil
return selectedUtxos, selectedAmount - amount, nil
}
func (s *service) SignTransaction(ctx context.Context, partialTx string, extractRawTx bool) (string, error) {
@@ -515,7 +519,7 @@ func (s *service) SignTransaction(ctx context.Context, partialTx string, extract
return "", err
}
signedInputs, err := s.signPsbt(ptx)
signedInputs, err := s.signPsbt(ptx, nil)
if err != nil {
return "", err
}
@@ -525,8 +529,55 @@ func (s *service) SignTransaction(ctx context.Context, partialTx string, extract
return "", fmt.Errorf("not all inputs are signed, unable to finalize the psbt")
}
if err := psbt.MaybeFinalizeAll(ptx); err != nil {
return "", err
for i, in := range ptx.Inputs {
isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript)
if isTaproot && len(in.TaprootLeafScript) > 0 {
closure, err := bitcointree.DecodeClosure(in.TaprootLeafScript[0].Script)
if err != nil {
return "", err
}
witness := make(wire.TxWitness, 4)
castClosure, isTaprootMultisig := closure.(*bitcointree.MultisigClosure)
if isTaprootMultisig {
ownerPubkey := schnorr.SerializePubKey(castClosure.Pubkey)
aspKey := schnorr.SerializePubKey(castClosure.AspPubkey)
for _, sig := range in.TaprootScriptSpendSig {
if bytes.Equal(sig.XOnlyPubKey, ownerPubkey) {
witness[0] = sig.Signature
}
if bytes.Equal(sig.XOnlyPubKey, aspKey) {
witness[1] = sig.Signature
}
}
witness[2] = in.TaprootLeafScript[0].Script
witness[3] = in.TaprootLeafScript[0].ControlBlock
for idw, w := range witness {
if w == nil {
return "", fmt.Errorf("missing witness element %d, cannot finalize taproot mutlisig input %d", idw, i)
}
}
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
}
}
if err := psbt.Finalize(ptx, i); err != nil {
return "", fmt.Errorf("failed to finalize input %d: %w", i, err)
}
}
extracted, err := psbt.Extract(ptx)
@@ -561,7 +612,7 @@ func (s *service) SignTransactionTapscript(ctx context.Context, partialTx string
}
}
signedInputs, err := s.signPsbt(partial)
signedInputs, err := s.signPsbt(partial, inputIndexes)
if err != nil {
return "", err
}
@@ -719,6 +770,24 @@ func (s *service) IsTransactionConfirmed(
return s.esploraClient.getTxStatus(txid)
}
func (s *service) GetTransaction(ctx context.Context, txid string) (string, error) {
tx, err := s.esploraClient.getTx(txid)
if err != nil {
return "", err
}
if tx == nil {
return "", fmt.Errorf("transaction not found")
}
var buf bytes.Buffer
if err := tx.Serialize(&buf); err != nil {
return "", err
}
return hex.EncodeToString(buf.Bytes()), nil
}
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string]ports.VtxoWithValue {
vtxos := make(map[string]ports.VtxoWithValue)

View File

@@ -1,6 +1,7 @@
package oceanwallet
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
@@ -11,6 +12,8 @@ import (
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/vulpemventures/go-elements/elementsutil"
@@ -22,7 +25,7 @@ const (
)
func (s *service) SignTransaction(
ctx context.Context, pset string, extractRawTx bool,
ctx context.Context, pset string, finalizeAndExtractRawTx bool,
) (string, error) {
res, err := s.txClient.SignPset(ctx, &pb.SignPsetRequest{
Pset: pset,
@@ -32,7 +35,7 @@ func (s *service) SignTransaction(
}
signedPset := res.GetPset()
if !extractRawTx {
if !finalizeAndExtractRawTx {
return signedPset, nil
}
@@ -41,8 +44,61 @@ func (s *service) SignTransaction(
return "", err
}
if err := psetv2.MaybeFinalizeAll(ptx); err != nil {
return "", fmt.Errorf("failed to finalize signed pset: %s", err)
for i, in := range ptx.Inputs {
if in.WitnessUtxo == nil {
return "", fmt.Errorf("missing witness utxo, cannot finalize tx")
}
if len(in.TapLeafScript) > 0 {
tapLeaf := in.TapLeafScript[0]
closure, err := tree.DecodeClosure(tapLeaf.Script)
if err != nil {
return "", err
}
switch c := closure.(type) {
case *tree.ForfeitClosure:
asp := schnorr.SerializePubKey(c.AspPubkey)
owner := schnorr.SerializePubKey(c.Pubkey)
witness := make([][]byte, 4)
for _, sig := range in.TapScriptSig {
if bytes.Equal(sig.PubKey, owner) {
witness[0] = sig.Signature
continue
}
if bytes.Equal(sig.PubKey, asp) {
witness[1] = sig.Signature
}
}
witness[2] = tapLeaf.Script
controlBlock, err := tapLeaf.ControlBlock.ToBytes()
if err != nil {
return "", err
}
witness[3] = controlBlock
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
default:
return "", fmt.Errorf("unexpected closure type %T", c)
}
}
if err := psetv2.Finalize(ptx, i); err != nil {
return "", fmt.Errorf("failed to finalize signed pset: %s", err)
}
}
extractedTx, err := psetv2.Extract(ptx)
@@ -281,6 +337,15 @@ func (s *service) EstimateFees(
return fee.GetFeeAmount() + 5, nil
}
func (s *service) GetTransaction(ctx context.Context, txid string) (string, error) {
txHex, _, _, err := s.getTransaction(ctx, txid)
if err != nil {
return "", err
}
return txHex, nil
}
func (s *service) getTransaction(
ctx context.Context, txid string,
) (string, bool, int64, error) {

View File

@@ -60,11 +60,20 @@ func (h *handler) CompletePayment(ctx context.Context, req *arkv1.CompletePaymen
}
func (h *handler) CreatePayment(ctx context.Context, req *arkv1.CreatePaymentRequest) (*arkv1.CreatePaymentResponse, error) {
vtxosKeys, err := parseInputs(req.GetInputs())
inputs, err := parseInputs(req.GetInputs())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
vtxosKeys := make([]domain.VtxoKey, 0, len(inputs))
for _, input := range inputs {
if !input.IsVtxo() {
return nil, status.Error(codes.InvalidArgument, "only vtxos input allowed")
}
vtxosKeys = append(vtxosKeys, input.VtxoKey())
}
receivers, err := parseReceivers(req.GetOutputs())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
@@ -83,37 +92,6 @@ func (h *handler) CreatePayment(ctx context.Context, req *arkv1.CreatePaymentReq
}, nil
}
func (h *handler) Onboard(ctx context.Context, req *arkv1.OnboardRequest) (*arkv1.OnboardResponse, error) {
if req.GetUserPubkey() == "" {
return nil, status.Error(codes.InvalidArgument, "missing user pubkey")
}
pubKey, err := hex.DecodeString(req.GetUserPubkey())
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid user pubkey")
}
decodedPubKey, err := secp256k1.ParsePubKey(pubKey)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid user pubkey")
}
if req.GetBoardingTx() == "" {
return nil, status.Error(codes.InvalidArgument, "missing boarding tx id")
}
tree, err := toCongestionTree(req.GetCongestionTree())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if err := h.svc.Onboard(ctx, req.GetBoardingTx(), tree, decodedPubKey); err != nil {
return nil, err
}
return &arkv1.OnboardResponse{}, nil
}
func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.PingResponse, error) {
if req.GetPaymentId() == "" {
return nil, status.Error(codes.InvalidArgument, "missing payment id")
@@ -193,12 +171,11 @@ func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.Ping
}
func (h *handler) RegisterPayment(ctx context.Context, req *arkv1.RegisterPaymentRequest) (*arkv1.RegisterPaymentResponse, error) {
vtxosKeys, err := parseInputs(req.GetInputs())
inputs, err := parseInputs(req.GetInputs())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
id, err := h.svc.SpendVtxos(ctx, vtxosKeys)
id, err := h.svc.SpendVtxos(ctx, inputs)
if err != nil {
return nil, err
}
@@ -229,12 +206,23 @@ func (h *handler) ClaimPayment(ctx context.Context, req *arkv1.ClaimPaymentReque
}
func (h *handler) FinalizePayment(ctx context.Context, req *arkv1.FinalizePaymentRequest) (*arkv1.FinalizePaymentResponse, error) {
forfeitTxs, err := parseTxs(req.GetSignedForfeitTxs())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
forfeitTxs := req.GetSignedForfeitTxs()
roundTx := req.GetSignedRoundTx()
if len(forfeitTxs) <= 0 && roundTx == "" {
return nil, status.Error(codes.InvalidArgument, "missing forfeit txs or round tx")
}
if err := h.svc.SignVtxos(ctx, forfeitTxs); err != nil {
return nil, err
if len(forfeitTxs) > 0 {
if err := h.svc.SignVtxos(ctx, forfeitTxs); err != nil {
return nil, err
}
}
if roundTx != "" {
if err := h.svc.SignRoundTx(ctx, roundTx); err != nil {
return nil, err
}
}
return &arkv1.FinalizePaymentResponse{}, nil
@@ -355,12 +343,39 @@ func (h *handler) GetInfo(ctx context.Context, req *arkv1.GetInfoRequest) (*arkv
}
return &arkv1.GetInfoResponse{
Pubkey: info.PubKey,
RoundLifetime: info.RoundLifetime,
UnilateralExitDelay: info.UnilateralExitDelay,
RoundInterval: info.RoundInterval,
Network: info.Network,
MinRelayFee: info.MinRelayFee,
Pubkey: info.PubKey,
RoundLifetime: info.RoundLifetime,
UnilateralExitDelay: info.UnilateralExitDelay,
RoundInterval: info.RoundInterval,
Network: info.Network,
MinRelayFee: info.MinRelayFee,
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
}, nil
}
func (h *handler) GetBoardingAddress(ctx context.Context, req *arkv1.GetBoardingAddressRequest) (*arkv1.GetBoardingAddressResponse, error) {
pubkey := req.GetPubkey()
if pubkey == "" {
return nil, status.Error(codes.InvalidArgument, "missing pubkey")
}
pubkeyBytes, err := hex.DecodeString(pubkey)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid pubkey (invalid hex)")
}
userPubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid pubkey (parse error)")
}
addr, err := h.svc.GetBoardingAddress(ctx, userPubkey)
if err != nil {
return nil, err
}
return &arkv1.GetBoardingAddressResponse{
Address: addr,
}, nil
}
@@ -548,8 +563,12 @@ func (v vtxoList) toProto(hrp string, aspKey *secp256k1.PublicKey) []*arkv1.Vtxo
}
list = append(list, &arkv1.Vtxo{
Outpoint: &arkv1.Input{
Txid: vv.Txid,
Vout: vv.VOut,
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: vv.Txid,
Vout: vv.VOut,
},
},
},
Receiver: &arkv1.Output{
Address: addr,
@@ -590,36 +609,3 @@ func castCongestionTree(congestionTree tree.CongestionTree) *arkv1.Tree {
Levels: levels,
}
}
func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) {
if treeFromProto == nil {
return nil, nil
}
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
}

View File

@@ -6,23 +6,11 @@ import (
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/server/internal/core/application"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
func parseTxs(txs []string) ([]string, error) {
if len(txs) <= 0 {
return nil, fmt.Errorf("missing list of forfeit txs")
}
// TODO abstract this ?
// for _, tx := range txs {
// if _, err := psetv2.NewPsetFromBase64(tx); err != nil {
// return nil, fmt.Errorf("invalid tx format")
// }
// }
return txs, nil
}
func parseAddress(addr string) (string, *secp256k1.PublicKey, *secp256k1.PublicKey, error) {
if len(addr) <= 0 {
return "", nil, nil, fmt.Errorf("missing address")
@@ -30,19 +18,31 @@ func parseAddress(addr string) (string, *secp256k1.PublicKey, *secp256k1.PublicK
return common.DecodeAddress(addr)
}
func parseInputs(ins []*arkv1.Input) ([]domain.VtxoKey, error) {
func parseInputs(ins []*arkv1.Input) ([]application.Input, error) {
if len(ins) <= 0 {
return nil, fmt.Errorf("missing inputs")
}
vtxos := make([]domain.VtxoKey, 0, len(ins))
inputs := make([]application.Input, 0, len(ins))
for _, input := range ins {
vtxos = append(vtxos, domain.VtxoKey{
Txid: input.GetTxid(),
VOut: input.GetVout(),
if input.GetBoardingInput() != nil {
desc := input.GetBoardingInput().GetDescriptor_()
inputs = append(inputs, application.Input{
Txid: input.GetBoardingInput().GetTxid(),
Index: input.GetBoardingInput().GetVout(),
Descriptor: desc,
})
continue
}
inputs = append(inputs, application.Input{
Txid: input.GetVtxoInput().GetTxid(),
Index: input.GetVtxoInput().GetVout(),
})
}
return vtxos, nil
return inputs, nil
}
func parseReceivers(outs []*arkv1.Output) ([]domain.Receiver, error) {

View File

@@ -145,10 +145,6 @@ func Whitelist() map[string][]bakery.Op {
Entity: EntityArk,
Action: "read",
}},
fmt.Sprintf("/%s/Onboard", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",
}},
fmt.Sprintf("/%s/CreatePayment", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",
@@ -161,6 +157,10 @@ func Whitelist() map[string][]bakery.Op {
Entity: EntityHealth,
Action: "read",
}},
fmt.Sprintf("/%s/GetBoardingAddress", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "read",
}},
fmt.Sprintf("/%s/SendTreeNonces", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",