mirror of
https://github.com/aljazceru/ark.git
synced 2026-01-18 11:14:20 +01:00
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:
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -449,6 +449,7 @@ func testEndFinalization(t *testing.T) {
|
||||
Stage: domain.Stage{
|
||||
Code: domain.FinalizationStage,
|
||||
},
|
||||
Payments: paymentsById,
|
||||
},
|
||||
forfeitTxs: nil,
|
||||
txid: txid,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user