diff --git a/nak.Dockerfile b/nak.Dockerfile index 22b804d8c..708685eef 100644 --- a/nak.Dockerfile +++ b/nak.Dockerfile @@ -1,5 +1,5 @@ # Use official Go image as base -FROM golang:1.23.3-alpine +FROM golang:1.24.1-alpine # Install git (needed for go install) RUN apk add --no-cache git diff --git a/server/internal/core/application/covenant.go b/server/internal/core/application/covenant.go index ed826f702..050600461 100644 --- a/server/internal/core/application/covenant.go +++ b/server/internal/core/application/covenant.go @@ -397,6 +397,14 @@ func (s *covenantService) SignVtxos(ctx context.Context, forfeitTxs []string) er } func (s *covenantService) SignRoundTx(ctx context.Context, signedRoundTx string) error { + numSignedInputs, err := s.builder.CountSignedTaprootInputs(signedRoundTx) + if err != nil { + return fmt.Errorf("failed to count number of signed boarding inputs: %s", err) + } + if numSignedInputs == 0 { + return nil + } + s.currentRoundLock.Lock() defer s.currentRoundLock.Unlock() currentRound := s.currentRound diff --git a/server/internal/core/application/covenantless.go b/server/internal/core/application/covenantless.go index 07cdf2bda..4a5a6f583 100644 --- a/server/internal/core/application/covenantless.go +++ b/server/internal/core/application/covenantless.go @@ -759,6 +759,10 @@ func (s *covenantlessService) UpdateTxRequestStatus(_ context.Context, id string } func (s *covenantlessService) SignVtxos(ctx context.Context, forfeitTxs []string) error { + if len(forfeitTxs) <= 0 { + return nil + } + if err := s.forfeitTxs.sign(forfeitTxs); err != nil { return err } @@ -774,6 +778,14 @@ func (s *covenantlessService) SignVtxos(ctx context.Context, forfeitTxs []string } func (s *covenantlessService) SignRoundTx(ctx context.Context, signedRoundTx string) error { + numSignedInputs, err := s.builder.CountSignedTaprootInputs(signedRoundTx) + if err != nil { + return fmt.Errorf("failed to count number of signed boarding inputs: %s", err) + } + if numSignedInputs == 0 { + return nil + } + s.currentRoundLock.Lock() defer s.currentRoundLock.Unlock() currentRound := s.currentRound @@ -1469,31 +1481,6 @@ func (s *covenantlessService) finalizeRound(notes []note.Note, roundEndTime time } }() - remainingTime := time.Until(roundEndTime) - select { - case <-s.forfeitsBoardingSigsChan: - log.Debug("all forfeit txs and boarding inputs signatures have been sent") - case <-time.After(remainingTime): - log.Debug("timeout waiting for forfeit txs and boarding inputs signatures") - } - - forfeitTxs, err := s.forfeitTxs.pop() - if err != nil { - changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err)) - log.WithError(err).Warn("failed to finalize round") - return - } - - if err := s.verifyForfeitTxsSigs(forfeitTxs); err != nil { - changes = round.Fail(err) - log.WithError(err).Warn("failed to validate forfeit txs") - return - } - - log.Debugf("signing round transaction %s\n", round.Id) - - boardingInputsIndexes := make([]int, 0) - boardingInputs := make([]domain.VtxoKey, 0) roundTx, err := psbt.NewFromRawBytes(strings.NewReader(round.UnsignedTx), true) if err != nil { log.Debugf("failed to parse round tx: %s", round.UnsignedTx) @@ -1501,36 +1488,88 @@ func (s *covenantlessService) finalizeRound(notes []note.Note, roundEndTime time log.WithError(err).Warn("failed to parse round tx") return } + includesBoardingInputs := false + for _, in := range roundTx.Inputs { + // TODO: this is ok as long as the server doesn't use taproot address too! + // We need to find a better way to understand if an in input is ours or if + // it's a boarding one. + scriptType := txscript.GetScriptClass(in.WitnessUtxo.PkScript) + if scriptType == txscript.WitnessV1TaprootTy { + includesBoardingInputs = true + break + } + } - 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 - } + txToSign := round.UnsignedTx + boardingInputs := make([]domain.VtxoKey, 0) + forfeitTxs := make([]string, 0) - boardingInputsIndexes = append(boardingInputsIndexes, i) - boardingInputs = append(boardingInputs, domain.VtxoKey{ - Txid: roundTx.UnsignedTx.TxIn[i].PreviousOutPoint.Hash.String(), - VOut: roundTx.UnsignedTx.TxIn[i].PreviousOutPoint.Index, - }) + if len(s.forfeitTxs.forfeitTxs) > 0 || includesBoardingInputs { + remainingTime := time.Until(roundEndTime) + select { + case <-s.forfeitsBoardingSigsChan: + log.Debug("all forfeit txs and boarding inputs signatures have been sent") + case <-time.After(remainingTime): + log.Debug("timeout waiting for forfeit txs and boarding inputs signatures") } - } - signedRoundTx := round.UnsignedTx + s.currentRoundLock.Lock() + round := s.currentRound + s.currentRoundLock.Unlock() + + roundTx, err := psbt.NewFromRawBytes(strings.NewReader(round.UnsignedTx), true) + 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 + } + txToSign = round.UnsignedTx - if len(boardingInputsIndexes) > 0 { - signedRoundTx, err = s.wallet.SignTransactionTapscript(ctx, signedRoundTx, boardingInputsIndexes) + forfeitTxs, err = s.forfeitTxs.pop() if err != nil { - changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err)) - log.WithError(err).Warn("failed to sign round tx") + changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err)) + log.WithError(err).Warn("failed to finalize round") + return + } + + if err := s.verifyForfeitTxsSigs(forfeitTxs); err != nil { + changes = round.Fail(err) + log.WithError(err).Warn("failed to validate forfeit txs") return } + + boardingInputsIndexes := make([]int, 0) + 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 + } + + boardingInputsIndexes = append(boardingInputsIndexes, i) + boardingInputs = append(boardingInputs, domain.VtxoKey{ + Txid: roundTx.UnsignedTx.TxIn[i].PreviousOutPoint.Hash.String(), + VOut: roundTx.UnsignedTx.TxIn[i].PreviousOutPoint.Index, + }) + } + } + + if len(boardingInputsIndexes) > 0 { + txToSign, err = s.wallet.SignTransactionTapscript(ctx, txToSign, boardingInputsIndexes) + 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) + log.Debugf("signing transaction %s\n", round.Id) + + signedRoundTx, err := s.wallet.SignTransaction(ctx, txToSign, 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") diff --git a/server/internal/core/ports/tx_builder.go b/server/internal/core/ports/tx_builder.go index 5e2107a1d..b5d2e747e 100644 --- a/server/internal/core/ports/tx_builder.go +++ b/server/internal/core/ports/tx_builder.go @@ -57,5 +57,6 @@ type TxBuilder interface { // FindLeaves returns all the leaves txs that are reachable from the given outpoint FindLeaves(vtxoTree tree.TxTree, fromtxid string, vout uint32) (leaves []tree.Node, err error) VerifyAndCombinePartialTx(dest string, src string) (string, error) + CountSignedTaprootInputs(tx string) (int, error) GetTxID(tx string) (string, error) } diff --git a/server/internal/infrastructure/tx-builder/covenant/builder.go b/server/internal/infrastructure/tx-builder/covenant/builder.go index c072cbe83..eaad5b89e 100644 --- a/server/internal/infrastructure/tx-builder/covenant/builder.go +++ b/server/internal/infrastructure/tx-builder/covenant/builder.go @@ -1000,6 +1000,22 @@ func (b *txBuilder) minRelayFeeConnectorTx() (uint64, error) { return b.wallet.MinRelayFee(context.Background(), uint64(common.ConnectorTxSize)) } +func (b *txBuilder) CountSignedTaprootInputs(tx string) (int, error) { + ptx, err := psetv2.NewPsetFromBase64(tx) + if err != nil { + return -1, err + } + + signedInputsCount := 0 + for _, input := range ptx.Inputs { + if len(input.TapScriptSig) == 0 || len(input.TapLeafScript) == 0 { + continue + } + signedInputsCount++ + } + return signedInputsCount, 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) diff --git a/server/internal/infrastructure/tx-builder/covenantless/builder.go b/server/internal/infrastructure/tx-builder/covenantless/builder.go index 5df81b908..35ac3627e 100644 --- a/server/internal/infrastructure/tx-builder/covenantless/builder.go +++ b/server/internal/infrastructure/tx-builder/covenantless/builder.go @@ -1049,6 +1049,23 @@ func (b *txBuilder) minRelayFeeConnectorTx() (uint64, error) { return b.wallet.MinRelayFee(context.Background(), uint64(common.ConnectorTxSize)) } +func (b *txBuilder) CountSignedTaprootInputs(tx string) (int, error) { + ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true) + if err != nil { + return -1, err + } + + signedInputsCount := 0 + for _, in := range ptx.Inputs { + if len(in.TaprootScriptSpendSig) == 0 || len(in.TaprootLeafScript) == 0 { + continue + } + + signedInputsCount++ + } + return signedInputsCount, nil +} + func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string, error) { roundTx, err := psbt.NewFromRawBytes(strings.NewReader(dest), true) if err != nil { @@ -1064,12 +1081,10 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string, return "", fmt.Errorf("txids do not match") } - for i, in := range sourceTx.Inputs { - isMultisigTaproot := len(in.TaprootLeafScript) > 0 + for i, sourceInput := range sourceTx.Inputs { + isMultisigTaproot := len(sourceInput.TaprootLeafScript) > 0 if isMultisigTaproot { // check if the source tx signs the leaf - sourceInput := sourceTx.Inputs[i] - if len(sourceInput.TaprootScriptSpendSig) == 0 { continue }