diff --git a/client/main.go b/client/main.go index 6ce2537ae..860aa9d46 100644 --- a/client/main.go +++ b/client/main.go @@ -306,13 +306,14 @@ func dumpPrivKey(ctx *cli.Context) error { } func receive(ctx *cli.Context) error { - offchainAddr, boardingAddr, err := arkSdkClient.Receive(ctx.Context) + onchainAddr, offchainAddr, boardingAddr, err := arkSdkClient.Receive(ctx.Context) if err != nil { return err } return printJSON(map[string]interface{}{ "boarding_address": boardingAddr, "offchain_address": offchainAddr, + "onchain_address": onchainAddr, }) } @@ -397,10 +398,6 @@ func redeem(ctx *cli.Context) error { return arkSdkClient.StartUnilateralExit(ctx.Context) } - if address == "" { - return fmt.Errorf("missing destination address") - } - if complete { txID, err := arkSdkClient.CompleteUnilateralExit(ctx.Context, address) if err != nil { diff --git a/common/fees.go b/common/fees.go index f1ca00d82..5c5281607 100644 --- a/common/fees.go +++ b/common/fees.go @@ -11,20 +11,6 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) -var TreeTxSize = (&input.TxWeightEstimator{}). - AddTaprootKeySpendInput(txscript.SigHashDefault). // parent - AddP2TROutput(). // left child - AddP2TROutput(). // right child - VSize() - -var ConnectorTxSize = (&input.TxWeightEstimator{}). - AddTaprootKeySpendInput(txscript.SigHashDefault). - AddP2TROutput(). - AddP2TROutput(). - AddP2TROutput(). - AddP2TROutput(). - VSize() - func ComputeForfeitTxFee( feeRate chainfee.SatPerKVByte, tapscript *waddrmgr.Tapscript, @@ -56,32 +42,3 @@ func ComputeForfeitTxFee( return uint64(feeRate.FeeForVSize(lntypes.VByte(txWeightEstimator.VSize())).ToUnit(btcutil.AmountSatoshi)), nil } - -func ComputeRedeemTxFee( - feeRate chainfee.SatPerKVByte, - vtxos []VtxoInput, - numOutputs int, -) (int64, error) { - if len(vtxos) <= 0 { - return 0, fmt.Errorf("missing vtxos") - } - - redeemTxWeightEstimator := &input.TxWeightEstimator{} - - // Estimate inputs - for _, vtxo := range vtxos { - if vtxo.Tapscript == nil { - txid := vtxo.Outpoint.Hash.String() - return 0, fmt.Errorf("missing tapscript for vtxo %s", txid) - } - - redeemTxWeightEstimator.AddTapscriptInput(lntypes.WeightUnit(vtxo.WitnessSize), vtxo.Tapscript) - } - - // Estimate outputs - for i := 0; i < numOutputs; i++ { - redeemTxWeightEstimator.AddP2TROutput() - } - - return int64(feeRate.FeeForVSize(lntypes.VByte(redeemTxWeightEstimator.VSize())).ToUnit(btcutil.AmountSatoshi)), nil -} diff --git a/common/tree/anchor.go b/common/tree/anchor.go new file mode 100644 index 000000000..c66980497 --- /dev/null +++ b/common/tree/anchor.go @@ -0,0 +1,66 @@ +package tree + +import ( + "bytes" + + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +var ( + ANCHOR_PKSCRIPT = []byte{ + 0x51, 0x02, 0x4e, 0x73, + } + ANCHOR_VALUE = int64(0) +) + +func AnchorOutput() *wire.TxOut { + return &wire.TxOut{ + Value: ANCHOR_VALUE, + PkScript: ANCHOR_PKSCRIPT, + } +} + +// ExtractWithAnchors extracts the final witness and scriptSig from psbt fields and ignores anchor inputs without failing. +func ExtractWithAnchors(p *psbt.Packet) (*wire.MsgTx, error) { + finalTx := p.UnsignedTx.Copy() + + for i, tin := range finalTx.TxIn { + pInput := p.Inputs[i] + + // ignore anchor outputs + if pInput.WitnessUtxo != nil && bytes.Equal(pInput.WitnessUtxo.PkScript, ANCHOR_PKSCRIPT) { + continue + } + + if pInput.FinalScriptSig != nil { + tin.SignatureScript = pInput.FinalScriptSig + } + + if pInput.FinalScriptWitness != nil { + witnessReader := bytes.NewReader( + pInput.FinalScriptWitness, + ) + + witCount, err := wire.ReadVarInt(witnessReader, 0) + if err != nil { + return nil, err + } + + tin.Witness = make(wire.TxWitness, witCount) + for j := uint64(0); j < witCount; j++ { + wit, err := wire.ReadVarBytes( + witnessReader, 0, + txscript.MaxScriptSize, "witness", + ) + if err != nil { + return nil, err + } + tin.Witness[j] = wit + } + } + } + + return finalTx, nil +} diff --git a/common/tree/builder.go b/common/tree/builder.go index 7ec6eb141..a6a870353 100644 --- a/common/tree/builder.go +++ b/common/tree/builder.go @@ -22,15 +22,14 @@ const ( // radix is hardcoded to 2 func CraftSharedOutput( receivers []Leaf, - feeSatsPerNode uint64, sweepTapTreeRoot []byte, ) ([]byte, int64, error) { - root, err := createTxTree(receivers, feeSatsPerNode, sweepTapTreeRoot, vtxoTreeRadix) + root, err := createTxTree(receivers, sweepTapTreeRoot, vtxoTreeRadix) if err != nil { return nil, 0, err } - amount := root.getAmount() + int64(feeSatsPerNode) + amount := root.getAmount() + ANCHOR_VALUE aggregatedKey, err := AggregateKeys(root.getCosigners(), sweepTapTreeRoot) if err != nil { @@ -50,11 +49,10 @@ func CraftSharedOutput( func BuildVtxoTree( initialInput *wire.OutPoint, receivers []Leaf, - feeSatsPerNode uint64, sweepTapTreeRoot []byte, vtxoTreeExpiry common.RelativeLocktime, ) (TxTree, error) { - root, err := createTxTree(receivers, feeSatsPerNode, sweepTapTreeRoot, vtxoTreeRadix) + root, err := createTxTree(receivers, sweepTapTreeRoot, vtxoTreeRadix) if err != nil { return nil, err } @@ -66,14 +64,13 @@ func BuildVtxoTree( // radix is hardcoded to 4 func CraftConnectorsOutput( receivers []Leaf, - feeSatsPerNode uint64, ) ([]byte, int64, error) { - root, err := createTxTree(receivers, feeSatsPerNode, nil, connectorsTreeRadix) + root, err := createTxTree(receivers, nil, connectorsTreeRadix) if err != nil { return nil, 0, err } - amount := root.getAmount() + int64(feeSatsPerNode) + amount := root.getAmount() + ANCHOR_VALUE aggregatedKey, err := AggregateKeys(root.getCosigners(), nil) if err != nil { @@ -93,9 +90,8 @@ func CraftConnectorsOutput( func BuildConnectorsTree( initialInput *wire.OutPoint, receivers []Leaf, - feeSatsPerNode uint64, ) (TxTree, error) { - root, err := createTxTree(receivers, feeSatsPerNode, nil, connectorsTreeRadix) + root, err := createTxTree(receivers, nil, connectorsTreeRadix) if err != nil { return nil, err } @@ -150,7 +146,7 @@ func toTxTree(root node, initialInput *wire.OutPoint, expiry *common.RelativeLoc } type node interface { - getAmount() int64 // returns the input amount of the node = sum of all receivers' amounts + fees + getAmount() int64 // returns the input amount of the node = sum of all receivers' amounts getOutputs() ([]*wire.TxOut, error) getChildren() []node getCosigners() []*secp256k1.PublicKey @@ -167,7 +163,6 @@ type branch struct { cosigners []*secp256k1.PublicKey pkScript []byte children []node - feeAmount int64 } func (b *branch) getCosigners() []*secp256k1.PublicKey { @@ -190,7 +185,7 @@ func (b *branch) getAmount() int64 { amount := int64(0) for _, child := range b.children { amount += child.getAmount() - amount += b.feeAmount + amount += ANCHOR_VALUE } return amount @@ -206,6 +201,7 @@ func (l *leaf) getOutputs() ([]*wire.TxOut, error) { Value: l.amount, PkScript: l.pkScript, }, + AnchorOutput(), }, nil } @@ -214,12 +210,12 @@ func (b *branch) getOutputs() ([]*wire.TxOut, error) { for _, child := range b.children { outputs = append(outputs, &wire.TxOut{ - Value: child.getAmount() + b.feeAmount, + Value: child.getAmount(), PkScript: b.pkScript, }) } - return outputs, nil + return append(outputs, AnchorOutput()), nil } func getTreeNode( @@ -257,7 +253,7 @@ func getTx( return nil, err } - tx, err := psbt.New([]*wire.OutPoint{input}, outputs, 2, 0, []uint32{wire.MaxTxInSequenceNum}) + tx, err := psbt.New([]*wire.OutPoint{input}, outputs, 3, 0, []uint32{wire.MaxTxInSequenceNum}) if err != nil { return nil, err } @@ -290,7 +286,6 @@ func getTx( // from the leaves to the root. func createTxTree( receivers []Leaf, - feeSatsPerNode uint64, tapTreeRoot []byte, radix int, ) (root node, err error) { @@ -372,7 +367,7 @@ func createTxTree( } for len(nodes) > 1 { - nodes, err = createUpperLevel(nodes, int64(feeSatsPerNode), tapTreeRoot, radix) + nodes, err = createUpperLevel(nodes, tapTreeRoot, radix) if err != nil { return nil, fmt.Errorf("failed to create tx tree: %w", err) } @@ -381,20 +376,20 @@ func createTxTree( return nodes[0], nil } -func createUpperLevel(nodes []node, feeAmount int64, tapTreeRoot []byte, radix int) ([]node, error) { +func createUpperLevel(nodes []node, tapTreeRoot []byte, radix int) ([]node, error) { if len(nodes) <= 1 { return nodes, nil } if len(nodes) < radix { - return createUpperLevel(nodes, feeAmount, tapTreeRoot, len(nodes)) + return createUpperLevel(nodes, tapTreeRoot, len(nodes)) } remainder := len(nodes) % radix if remainder != 0 { // Handle nodes that don't form a complete group last := nodes[len(nodes)-remainder:] - groups, err := createUpperLevel(nodes[:len(nodes)-remainder], feeAmount, tapTreeRoot, radix) + groups, err := createUpperLevel(nodes[:len(nodes)-remainder], tapTreeRoot, radix) if err != nil { return nil, err } @@ -425,7 +420,6 @@ func createUpperLevel(nodes []node, feeAmount int64, tapTreeRoot []byte, radix i branchNode := &branch{ pkScript: pkScript, cosigners: cosigners, - feeAmount: feeAmount, children: children, } diff --git a/common/tree/musig2_test.go b/common/tree/musig2_test.go index 0e8aab106..76b73412a 100644 --- a/common/tree/musig2_test.go +++ b/common/tree/musig2_test.go @@ -14,11 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -const ( - minRelayFee = 1000 - exitDelay = 512 -) - var ( vtxoTreeExpiry = common.RelativeLocktime{Type: common.LocktimeTypeBlock, Value: 144} rootInput, _ = wire.NewOutPointFromString("49f8664acc899be91902f8ade781b7eeb9cbe22bdd9efbc36e56195de21bcd12:0") @@ -41,14 +36,14 @@ func TestBuildAndSignVtxoTree(t *testing.T) { for _, v := range testVectors { t.Run(v.name, func(t *testing.T) { sharedOutScript, sharedOutAmount, err := tree.CraftSharedOutput( - v.receivers, minRelayFee, sweepRoot[:], + v.receivers, sweepRoot[:], ) require.NoError(t, err) require.NotNil(t, sharedOutScript) require.NotZero(t, sharedOutAmount) vtxoTree, err := tree.BuildVtxoTree( - rootInput, v.receivers, minRelayFee, sweepRoot[:], vtxoTreeExpiry, + rootInput, v.receivers, sweepRoot[:], vtxoTreeExpiry, ) require.NoError(t, err) require.NotNil(t, vtxoTree) diff --git a/common/tree/pending.go b/common/tree/pending.go index 4819fe0e7..883c0e090 100644 --- a/common/tree/pending.go +++ b/common/tree/pending.go @@ -14,6 +14,9 @@ const ( cltvSequence = wire.MaxTxInSequenceNum - 1 ) +// BuildRedeemTx builds a redeem tx for the given vtxos and outputs. +// The redeem tx is spending VTXOs using collaborative taproot path. +// An anchor output is added to the transaction func BuildRedeemTx( vtxos []common.VtxoInput, outputs []*wire.TxOut, @@ -95,7 +98,7 @@ func BuildRedeemTx( } redeemPtx, err := psbt.New( - ins, outputs, 2, uint32(txLocktime), sequences, + ins, append(outputs, AnchorOutput()), 3, uint32(txLocktime), sequences, ) if err != nil { return "", err diff --git a/common/tree/validation.go b/common/tree/validation.go index 2186db848..c9793cc70 100644 --- a/common/tree/validation.go +++ b/common/tree/validation.go @@ -112,7 +112,7 @@ func ValidateVtxoTree( sumRootValue += output.Value } - if sumRootValue >= roundTxAmount { + if sumRootValue != roundTxAmount { return ErrInvalidAmount } @@ -220,7 +220,7 @@ func validateNodeTransaction(node Node, tree TxTree, tapTreeRoot []byte) error { sumChildAmount += output.Value } - if sumChildAmount >= parentOutput.Value { + if sumChildAmount != parentOutput.Value { return ErrInvalidAmount } } diff --git a/pkg/client-sdk/ark_sdk.go b/pkg/client-sdk/ark_sdk.go index f4b8837df..babb436fc 100644 --- a/pkg/client-sdk/ark_sdk.go +++ b/pkg/client-sdk/ark_sdk.go @@ -20,7 +20,7 @@ type ArkClient interface { Unlock(ctx context.Context, password string) error Lock(ctx context.Context) error Balance(ctx context.Context, computeExpiryDetails bool) (*Balance, error) - Receive(ctx context.Context) (offchainAddr, boardingAddr string, err error) + Receive(ctx context.Context) (onchainAddr, offchainAddr, boardingAddr string, err error) SendOffChain( ctx context.Context, withExpiryCoinselect bool, receivers []Receiver, withZeroFees bool, diff --git a/pkg/client-sdk/base_client.go b/pkg/client-sdk/base_client.go index 9ad05f7e4..a8328829d 100644 --- a/pkg/client-sdk/base_client.go +++ b/pkg/client-sdk/base_client.go @@ -105,21 +105,21 @@ func (a *arkClient) Dump(ctx context.Context) (string, error) { return a.wallet.Dump(ctx) } -func (a *arkClient) Receive(ctx context.Context) (string, string, error) { +func (a *arkClient) Receive(ctx context.Context) (string, string, string, error) { if a.wallet == nil { - return "", "", fmt.Errorf("wallet not initialized") + return "", "", "", fmt.Errorf("wallet not initialized") } - offchainAddr, boardingAddr, err := a.wallet.NewAddress(ctx, false) + onchainAddr, offchainAddr, boardingAddr, err := a.wallet.NewAddress(ctx, false) if err != nil { - return "", "", err + return "", "", "", err } if a.UtxoMaxAmount == 0 { boardingAddr.Address = "" } - return offchainAddr.Address, boardingAddr.Address, nil + return onchainAddr, offchainAddr.Address, boardingAddr.Address, nil } func (a *arkClient) GetTransactionEventChannel(_ context.Context) chan types.TransactionEvent { @@ -163,7 +163,7 @@ func (a *arkClient) Stop() { func (a *arkClient) ListVtxos( ctx context.Context, ) (spendableVtxos, spentVtxos []client.Vtxo, err error) { - offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) + _, offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) if err != nil { return } diff --git a/pkg/client-sdk/client.go b/pkg/client-sdk/client.go index d97a6f9ae..aecc861e0 100644 --- a/pkg/client-sdk/client.go +++ b/pkg/client-sdk/client.go @@ -21,6 +21,7 @@ import ( "github.com/ark-network/ark/common/note" "github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/pkg/client-sdk/client" + "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/indexer" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/redemption" @@ -34,11 +35,17 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" log "github.com/sirupsen/logrus" "golang.org/x/exp/slices" ) +var ( + ErrWaitingForConfirmation = fmt.Errorf("waiting for confirmation(s), please retry later") +) + // SettleOptions is only available for covenantless clients // it allows to customize the vtxo signing process type SettleOptions struct { @@ -329,7 +336,7 @@ func (a *covenantlessArkClient) Balance( return nil, fmt.Errorf("wallet not initialized") } - offchainAddrs, boardingAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx) + onchainAddrs, offchainAddrs, boardingAddrs, redeemAddrs, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, err } @@ -353,7 +360,7 @@ func (a *covenantlessArkClient) Balance( }, nil } - const nbWorkers = 3 + const nbWorkers = 4 wg := &sync.WaitGroup{} wg.Add(nbWorkers * len(offchainAddrs)) @@ -396,6 +403,20 @@ func (a *covenantlessArkClient) Balance( } } + go func() { + defer wg.Done() + totalOnchainBalance := uint64(0) + for _, addr := range onchainAddrs { + balance, err := a.explorer.GetBalance(addr) + if err != nil { + chRes <- balanceRes{err: err} + return + } + totalOnchainBalance += balance + } + chRes <- balanceRes{onchainSpendableBalance: totalOnchainBalance} + }() + go getDelayedBalance(boardingAddr.Address) go getDelayedBalance(redeemAddr.Address) } @@ -462,7 +483,7 @@ func (a *covenantlessArkClient) OnboardAgainAllExpiredBoardings( return "", fmt.Errorf("operation not allowed by the server") } - _, boardingAddr, err := a.wallet.NewAddress(ctx, false) + _, _, boardingAddr, err := a.wallet.NewAddress(ctx, false) if err != nil { return "", err } @@ -508,7 +529,7 @@ func (a *covenantlessArkClient) SendOffChain( } } - offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) + _, offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) if err != nil { return "", err } @@ -637,7 +658,7 @@ func (a *covenantlessArkClient) RedeemNotes(ctx context.Context, notes []string, amount += uint64(v.Value) } - offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) + _, offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) if err != nil { return "", err } @@ -677,39 +698,209 @@ func (a *covenantlessArkClient) StartUnilateralExit(ctx context.Context) error { return err } + isWaitingForConfirmation := false + for _, branch := range redeemBranches { branchTxs, err := branch.RedeemPath() if err != nil { + if err, ok := err.(redemption.ErrPendingConfirmation); ok { + // the branch tx is in the mempool, we must wait for confirmation + // print only, do not make the function to fail + // continue to try other branches + log.Info(err.Error()) + isWaitingForConfirmation = true + continue + } + return err } - for _, txHex := range branchTxs { - if _, ok := transactionsMap[txHex]; !ok { - transactions = append(transactions, txHex) - transactionsMap[txHex] = struct{}{} - } + if len(branchTxs) <= 0 { + continue + } + + // due to current P2A relay policy, we can't broadcast the branch tx until its parent tx is confirmed + // so we'll broadcast only the first tx of every branch + firstTx := branchTxs[0] + + if _, ok := transactionsMap[firstTx]; !ok { + transactions = append(transactions, firstTx) + transactionsMap[firstTx] = struct{}{} } } - for i, txHex := range transactions { - for { - txid, err := a.explorer.Broadcast(txHex) - if err != nil { - if strings.Contains(strings.ToLower(err.Error()), "bad-txns-inputs-missingorspent") { - time.Sleep(1 * time.Second) - } else { - return err - } - } + if len(transactions) == 0 { + if isWaitingForConfirmation { + return ErrWaitingForConfirmation + } + + return nil + } + + for _, parent := range transactions { + var parentTx wire.MsgTx + if err := parentTx.Deserialize(hex.NewDecoder(strings.NewReader(parent))); err != nil { + return err + } + + child, err := a.bumpAnchorTx(ctx, &parentTx) + if err != nil { + return err + } + + // broadcast the package (parent + child) + packageResponse, err := a.explorer.Broadcast(parent, child) + if err != nil { + return err + } - if len(txid) > 0 { - log.Infof("(%d/%d) broadcasted tx %s", i+1, len(transactions), txid) + log.Infof("package broadcasted: %s", packageResponse) + } + + return nil +} + +// bumpAnchorTx is crafting and signing a transaction bumping the fees for a given tx with P2A output +// it is using the onchain P2TR account to select UTXOs +func (a *covenantlessArkClient) bumpAnchorTx(ctx context.Context, parent *wire.MsgTx) (string, error) { + anchor, err := findAnchorOutpoint(parent) + if err != nil { + return "", err + } + + // estimate for the size of the bump transaction + weightEstimator := input.TxWeightEstimator{} + + // WeightEstimator doesn't support P2A size, using P2WSH will lead to a small overestimation + // TODO use the exact P2A size + weightEstimator.AddNestedP2WSHInput(lntypes.VByte(3).ToWU()) + + // We assume only one UTXO will be selected to have a correct estimation + weightEstimator.AddTaprootKeySpendInput(txscript.SigHashDefault) + weightEstimator.AddP2TROutput() + + childVSize := weightEstimator.Weight().ToVB() + + packageSize := childVSize + computeVSize(parent) + feeRate, err := a.explorer.GetFeeRate() + if err != nil { + return "", err + } + + fees := uint64(math.Ceil(float64(packageSize) * feeRate)) + + addresses, _, _, _, err := a.wallet.GetAddresses(ctx) + if err != nil { + return "", err + } + + selectedCoins := make([]explorer.Utxo, 0) + selectedAmount := uint64(0) + amountToSelect := int64(fees) - tree.ANCHOR_VALUE + for _, addr := range addresses { + utxos, err := a.explorer.GetUtxos(addr) + if err != nil { + return "", err + } + + for _, utxo := range utxos { + selectedCoins = append(selectedCoins, utxo) + selectedAmount += utxo.Amount + amountToSelect -= int64(selectedAmount) + if amountToSelect <= 0 { break } } } - return nil + if amountToSelect > 0 { + return "", fmt.Errorf("not enough funds to select %d", amountToSelect) + } + + changeAmount := selectedAmount - fees + + newAddr, _, _, err := a.wallet.NewAddress(ctx, true) + if err != nil { + return "", err + } + + addr, err := btcutil.DecodeAddress(newAddr, nil) + if err != nil { + return "", err + } + + pkScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return "", err + } + + inputs := []*wire.OutPoint{anchor} + sequences := []uint32{ + wire.MaxTxInSequenceNum, + } + + for _, utxo := range selectedCoins { + txid, err := chainhash.NewHashFromStr(utxo.Txid) + if err != nil { + return "", err + } + inputs = append(inputs, &wire.OutPoint{ + Hash: *txid, + Index: utxo.Vout, + }) + sequences = append(sequences, wire.MaxTxInSequenceNum) + } + + ptx, err := psbt.New( + inputs, + []*wire.TxOut{ + { + Value: int64(changeAmount), + PkScript: pkScript, + }, + }, + 3, + 0, + sequences, + ) + if err != nil { + return "", err + } + + ptx.Inputs[0].WitnessUtxo = tree.AnchorOutput() + + b64, err := ptx.B64Encode() + if err != nil { + return "", err + } + + tx, err := a.wallet.SignTransaction(ctx, a.explorer, b64) + if err != nil { + return "", err + } + + signedPtx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true) + if err != nil { + return "", err + } + + for inIndex := range signedPtx.Inputs[1:] { + if _, err := psbt.MaybeFinalize(signedPtx, inIndex+1); err != nil { + return "", err + } + } + + childTx, err := tree.ExtractWithAnchors(signedPtx) + if err != nil { + return "", err + } + + var serializedTx bytes.Buffer + if err := childTx.Serialize(&serializedTx); err != nil { + return "", err + } + + return hex.EncodeToString(serializedTx.Bytes()), nil } func (a *covenantlessArkClient) CompleteUnilateralExit( @@ -719,7 +910,14 @@ func (a *covenantlessArkClient) CompleteUnilateralExit( return "", err } - if _, err := btcutil.DecodeAddress(to, nil); err != nil { + if len(to) == 0 { + newAddr, _, _, err := a.wallet.NewAddress(ctx, false) + if err != nil { + return "", err + } + + to = newAddr + } else if _, err := btcutil.DecodeAddress(to, nil); err != nil { return "", fmt.Errorf("invalid receiver address '%s': must be onchain", to) } @@ -764,7 +962,7 @@ func (a *covenantlessArkClient) CollaborativeExit( } if changeAmount > 0 { - offchainAddr, _, err := a.wallet.NewAddress(ctx, true) + _, offchainAddr, _, err := a.wallet.NewAddress(ctx, true) if err != nil { return "", err } @@ -852,7 +1050,7 @@ func (a *covenantlessArkClient) listenForArkTxs(ctx context.Context) { continue } - offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) + _, offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) if err != nil { log.WithError(err).Error("failed to get offchain addresses") continue @@ -1012,7 +1210,7 @@ func (a *covenantlessArkClient) listenForBoardingTxs(ctx context.Context) { for { select { case <-ticker.C: - _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) + _, _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { log.WithError(err).Error("failed to get all boarding addresses") continue @@ -1207,12 +1405,12 @@ func (a *covenantlessArkClient) sendExpiredBoardingUtxos( return "", err } - size := updater.Upsbt.UnsignedTx.SerializeSize() + vbytes := computeVSize(updater.Upsbt.UnsignedTx) feeRate, err := a.explorer.GetFeeRate() if err != nil { return "", err } - feeAmount := uint64(math.Ceil(float64(size)*feeRate) + 50) + feeAmount := uint64(math.Ceil(float64(vbytes)*feeRate) + 50) if targetAmount-feeAmount <= a.Dust { return "", fmt.Errorf("not enough funds to cover network fees") @@ -1289,13 +1487,13 @@ func (a *covenantlessArkClient) completeUnilateralExit( return "", err } - size := updater.Upsbt.UnsignedTx.SerializeSize() + vbytes := computeVSize(updater.Upsbt.UnsignedTx) feeRate, err := a.explorer.GetFeeRate() if err != nil { return "", err } - feeAmount := uint64(math.Ceil(float64(size)*feeRate) + 50) + feeAmount := uint64(math.Ceil(float64(vbytes)*feeRate) + 50) if targetAmount-feeAmount <= a.Dust { return "", fmt.Errorf("not enough funds to cover network fees") @@ -1330,7 +1528,7 @@ func (a *covenantlessArkClient) selectFunds( selectRecoverableVtxos bool, amount uint64, ) ([]types.Utxo, []client.TapscriptsVtxo, uint64, error) { - offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) + _, offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, nil, 0, err } @@ -1444,7 +1642,7 @@ func (a *covenantlessArkClient) sendOffchain( return "", err } - offchainAddr, _, err := a.wallet.NewAddress(ctx, false) + _, offchainAddr, _, err := a.wallet.NewAddress(ctx, false) if err != nil { return "", err } @@ -1592,7 +1790,7 @@ func (a *covenantlessArkClient) addInputs( utxos []types.Utxo, ) error { // TODO works only with single-key wallet - offchain, _, err := a.wallet.NewAddress(ctx, false) + _, offchain, _, err := a.wallet.NewAddress(ctx, false) if err != nil { return err } @@ -2417,7 +2615,7 @@ func (a *covenantlessArkClient) createAndSignForfeits( func (a *covenantlessArkClient) getMatureUtxos( ctx context.Context, ) ([]types.Utxo, error) { - _, _, redemptionAddrs, err := a.wallet.GetAddresses(ctx) + _, _, _, redemptionAddrs, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, err } @@ -2508,7 +2706,7 @@ func (a *covenantlessArkClient) getOffchainBalance( } func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]types.Utxo, map[string]struct{}, error) { - _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) + _, _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, nil, err } @@ -2611,7 +2809,7 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos( } func (a *covenantlessArkClient) getExpiredBoardingUtxos(ctx context.Context, opts *CoinSelectOptions) ([]types.Utxo, error) { - _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) + _, _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { return nil, err } @@ -3331,15 +3529,6 @@ func buildRedeemTx( }) } - fees, err := common.ComputeRedeemTxFee(feeRate.FeePerKVByte(), ins, len(receivers)) - if err != nil { - return "", err - } - - if fees >= int64(receivers[len(receivers)-1].Amount()) { - return "", fmt.Errorf("redeem tx fee is higher than the amount of the change receiver") - } - outs := make([]*wire.TxOut, 0, len(receivers)) for i, receiver := range receivers { @@ -3357,16 +3546,8 @@ func buildRedeemTx( return "", err } - value := receiver.Amount() - if !withZeroFees { - // Deduct the min relay fee from the very last receiver which is supposed - // to be the change in case it's not a send-all. - if i == len(receivers)-1 { - value -= uint64(fees) - } - } outs = append(outs, &wire.TxOut{ - Value: int64(value), + Value: int64(receiver.Amount()), PkScript: newVtxoScript, }) } @@ -3651,6 +3832,32 @@ func toTypesVtxo(src client.Vtxo) types.Vtxo { } } +func computeVSize(tx *wire.MsgTx) lntypes.VByte { + baseSize := tx.SerializeSizeStripped() + totalSize := tx.SerializeSize() // including witness + weight := totalSize + baseSize*3 + return lntypes.WeightUnit(uint64(weight)).ToVB() +} + +func findAnchorOutpoint(tx *wire.MsgTx) (*wire.OutPoint, error) { + anchorIndex := -1 + for outIndex, out := range tx.TxOut { + if bytes.Equal(out.PkScript, tree.ANCHOR_PKSCRIPT) { + anchorIndex = outIndex + break + } + } + + if anchorIndex == -1 { + return nil, fmt.Errorf("anchor not found") + } + + return &wire.OutPoint{ + Hash: tx.TxHash(), + Index: uint32(anchorIndex), + }, nil +} + // custom BIP322 finalizer function handling note vtxo inputs func finalizeWithNotes(notesWitnesses map[int][]byte) func(ptx *psbt.Packet) error { return func(ptx *psbt.Packet) error { diff --git a/pkg/client-sdk/example/alice_to_bob.go b/pkg/client-sdk/example/alice_to_bob.go index 7c4287928..de30fd44e 100644 --- a/pkg/client-sdk/example/alice_to_bob.go +++ b/pkg/client-sdk/example/alice_to_bob.go @@ -56,7 +56,7 @@ func main() { defer aliceArkClient.Lock(ctx) log.Info("alice is acquiring onchain funds...") - _, boardingAddress, err := aliceArkClient.Receive(ctx) + _, _, boardingAddress, err := aliceArkClient.Receive(ctx) if err != nil { log.Fatal(err) } @@ -101,7 +101,7 @@ func main() { //nolint:all defer bobArkClient.Lock(ctx) - bobOffchainAddr, _, err := bobArkClient.Receive(ctx) + _, bobOffchainAddr, _, err := bobArkClient.Receive(ctx) if err != nil { log.Fatal(err) } diff --git a/pkg/client-sdk/explorer/explorer.go b/pkg/client-sdk/explorer/explorer.go index ef42d02e2..9cda45072 100644 --- a/pkg/client-sdk/explorer/explorer.go +++ b/pkg/client-sdk/explorer/explorer.go @@ -24,11 +24,11 @@ const ( type Explorer interface { GetTxHex(txid string) (string, error) - Broadcast(txHex string) (string, error) + Broadcast(txs ...string) (string, error) GetTxs(addr string) ([]tx, error) IsRBFTx(txid, txHex string) (bool, string, int64, error) GetTxOutspends(tx string) ([]spentStatus, error) - GetUtxos(addr string) ([]utxo, error) + GetUtxos(addr string) ([]Utxo, error) GetBalance(addr string) (uint64, error) GetRedeemedVtxosBalance( addr string, unilateralExitDelay common.RelativeLocktime, @@ -107,27 +107,64 @@ func (e *explorerSvc) GetTxHex(txid string) (string, error) { return txHex, nil } -func (e *explorerSvc) Broadcast(txStr string) (string, error) { - clone := strings.Clone(txStr) - txStr, txid, err := parseBitcoinTx(clone) - if err != nil { - return "", err +func (e *explorerSvc) Broadcast(txs ...string) (string, error) { + if len(txs) == 0 { + return "", fmt.Errorf("no txs to broadcast") } - e.cache.Set(txid, txStr) + for _, tx := range txs { + txStr, txid, err := parseBitcoinTx(tx) + if err != nil { + return "", err + } + + e.cache.Set(txid, txStr) + } - txid, err = e.broadcast(txStr) - if err != nil { - if strings.Contains( - strings.ToLower(err.Error()), "transaction already in block chain", - ) { - return txid, nil + if len(txs) == 1 { + txid, err := e.broadcast(txs[0]) + if err != nil { + if strings.Contains( + strings.ToLower(err.Error()), "transaction already in block chain", + ) { + return txid, nil + } + + return "", err } + return txid, nil + } + + // package + return e.broadcastPackage(txs...) +} + +func (e *explorerSvc) broadcastPackage(txs ...string) (string, error) { + url := fmt.Sprintf("%s/txs/package", e.baseUrl) + + // body is a json array of txs hex + body := bytes.NewBuffer(nil) + if err := json.NewEncoder(body).Encode(txs); err != nil { return "", err } - return txid, nil + resp, err := http.Post(url, "application/json", body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + bodyResponse, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to broadcast package: %s", string(bodyResponse)) + } + + return string(bodyResponse), nil } func (e *explorerSvc) GetTxs(addr string) ([]tx, error) { @@ -207,7 +244,7 @@ func (e *explorerSvc) GetTxOutspends(txid string) ([]spentStatus, error) { return spentStatuses, nil } -func (e *explorerSvc) GetUtxos(addr string) ([]utxo, error) { +func (e *explorerSvc) GetUtxos(addr string) ([]Utxo, error) { resp, err := http.Get(fmt.Sprintf("%s/address/%s/utxo", e.baseUrl, addr)) if err != nil { return nil, err @@ -222,7 +259,7 @@ func (e *explorerSvc) GetUtxos(addr string) ([]utxo, error) { if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to get utxos: %s", string(body)) } - payload := []utxo{} + payload := []Utxo{} if err := json.Unmarshal(body, &payload); err != nil { return nil, err } @@ -448,7 +485,7 @@ func parseBitcoinTx(txStr string) (string, string, error) { return txhex, txid, nil } -func newUtxo(explorerUtxo utxo, delay common.RelativeLocktime, tapscripts []string) types.Utxo { +func newUtxo(explorerUtxo Utxo, delay common.RelativeLocktime, tapscripts []string) types.Utxo { utxoTime := explorerUtxo.Status.Blocktime createdAt := time.Unix(utxoTime, 0) if utxoTime == 0 { diff --git a/pkg/client-sdk/explorer/types.go b/pkg/client-sdk/explorer/types.go index 7757bd603..acb9b63ab 100644 --- a/pkg/client-sdk/explorer/types.go +++ b/pkg/client-sdk/explorer/types.go @@ -36,7 +36,7 @@ type replacement struct { Replaces []replacement `json:"replaces"` } -type utxo struct { +type Utxo struct { Txid string `json:"txid"` Vout uint32 `json:"vout"` Amount uint64 `json:"value"` @@ -47,6 +47,6 @@ type utxo struct { } `json:"status"` } -func (e utxo) ToUtxo(delay common.RelativeLocktime, tapscripts []string) types.Utxo { +func (e Utxo) ToUtxo(delay common.RelativeLocktime, tapscripts []string) types.Utxo { return newUtxo(e, delay, tapscripts) } diff --git a/pkg/client-sdk/redemption/redeem.go b/pkg/client-sdk/redemption/redeem.go index 725c1a014..209d014a2 100644 --- a/pkg/client-sdk/redemption/redeem.go +++ b/pkg/client-sdk/redemption/redeem.go @@ -141,10 +141,19 @@ func (r *CovenantlessRedeemBranch) OffchainPath() ([]*psbt.Packet, error) { ptx := r.branch[i] txHash := ptx.UnsignedTx.TxHash().String() - if _, err := r.explorer.GetTxHex(txHash); err != nil { + confirmed, _, err := r.explorer.GetTxBlockTime(txHash) + + // if the tx is not found, it's offchain, let's continue + if err != nil { continue } + // if found but not confirmed, it means the tx is in the mempool + // an unilateral exit is running, we must wait for it to be confirmed + if !confirmed { + return nil, ErrPendingConfirmation{Txid: txHash} + } + // if no error, the tx exists onchain, so we can remove it (+ the parents) from the branch if i == len(r.branch)-1 { offchainPath = []*psbt.Packet{} @@ -157,3 +166,13 @@ func (r *CovenantlessRedeemBranch) OffchainPath() ([]*psbt.Packet, error) { return offchainPath, nil } + +// ErrPendingConfirmation is returned when computing the offchain path of a redeem branch. Due to P2A relay policy, only 1C1P packages are accepted. +// This error is returned when the tx is found onchain but not confirmed yet, allowing the user to know when to wait for the tx to be confirmed or to continue with the redemption. +type ErrPendingConfirmation struct { + Txid string +} + +func (e ErrPendingConfirmation) Error() string { + return fmt.Sprintf("unilateral exit is running, please wait for tx %s to be confirmed", e.Txid) +} diff --git a/pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go b/pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go index acd23582d..0a601db4f 100644 --- a/pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go +++ b/pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go @@ -45,20 +45,20 @@ func NewBitcoinWallet( func (w *bitcoinWallet) GetAddresses( ctx context.Context, -) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) { - offchainAddr, boardingAddr, err := w.getAddress(ctx) +) ([]string, []wallet.TapscriptsAddress, []wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) { + offchainAddr, boardingAddr, err := w.getArkAddresses(ctx) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } encodedOffchainAddr, err := offchainAddr.Address.Encode() if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } data, err := w.configStore.GetData(ctx) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } netParams := utils.ToBitcoinNetwork(data.Network) @@ -68,7 +68,7 @@ func (w *bitcoinWallet) GetAddresses( &netParams, ) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } offchainAddrs := []wallet.TapscriptsAddress{ @@ -89,23 +89,34 @@ func (w *bitcoinWallet) GetAddresses( Address: redemptionAddr.EncodeAddress(), }, } - return offchainAddrs, boardingAddrs, redemptionAddrs, nil + + onchainAddr, err := w.getP2TRAddress(ctx) + if err != nil { + return nil, nil, nil, nil, err + } + + return []string{onchainAddr.EncodeAddress()}, offchainAddrs, boardingAddrs, redemptionAddrs, nil } func (w *bitcoinWallet) NewAddress( ctx context.Context, _ bool, -) (*wallet.TapscriptsAddress, *wallet.TapscriptsAddress, error) { - offchainAddr, boardingAddr, err := w.getAddress(ctx) +) (string, *wallet.TapscriptsAddress, *wallet.TapscriptsAddress, error) { + offchainAddr, boardingAddr, err := w.getArkAddresses(ctx) if err != nil { - return nil, nil, err + return "", nil, nil, err } encodedOffchainAddr, err := offchainAddr.Address.Encode() if err != nil { - return nil, nil, err + return "", nil, nil, err + } + + onchainAddr, err := w.getP2TRAddress(ctx) + if err != nil { + return "", nil, nil, err } - return &wallet.TapscriptsAddress{ + return onchainAddr.EncodeAddress(), &wallet.TapscriptsAddress{ Tapscripts: offchainAddr.Tapscripts, Address: encodedOffchainAddr, }, boardingAddr, nil @@ -113,10 +124,10 @@ func (w *bitcoinWallet) NewAddress( func (w *bitcoinWallet) NewAddresses( ctx context.Context, _ bool, num int, -) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) { - offchainAddr, boardingAddr, err := w.getAddress(ctx) +) ([]string, []wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) { + offchainAddr, boardingAddr, err := w.getArkAddresses(ctx) if err != nil { - return nil, nil, err + return nil, nil, nil, err } offchainAddrs := make([]wallet.TapscriptsAddress, 0, num) @@ -124,7 +135,7 @@ func (w *bitcoinWallet) NewAddresses( for i := 0; i < num; i++ { encodedOffchainAddr, err := offchainAddr.Address.Encode() if err != nil { - return nil, nil, err + return nil, nil, nil, err } offchainAddrs = append(offchainAddrs, wallet.TapscriptsAddress{ @@ -136,7 +147,17 @@ func (w *bitcoinWallet) NewAddresses( Address: boardingAddr.Address, }) } - return offchainAddrs, boardingAddrs, nil + + onchainAddrs := make([]string, 0, num) + for i := 0; i < num; i++ { + onchainAddr, err := w.getP2TRAddress(ctx) + if err != nil { + return nil, nil, nil, err + } + onchainAddrs = append(onchainAddrs, onchainAddr.EncodeAddress()) + } + + return onchainAddrs, offchainAddrs, boardingAddrs, nil } func (s *bitcoinWallet) SignTransaction( @@ -194,90 +215,34 @@ func (s *bitcoinWallet) SignTransaction( ) txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher) - myPubkey := schnorr.SerializePubKey(s.walletData.PubKey) + + onchainPkScript, err := common.P2TRScript(txscript.ComputeTaprootKeyNoScript(s.walletData.PubKey)) + if err != nil { + return "", err + } for i, input := range ptx.Inputs { if len(input.TaprootLeafScript) > 0 { - for _, leaf := range input.TaprootLeafScript { - closure, err := tree.DecodeClosure(leaf.Script) - if err != nil { - // in case of invalid script, we skip signing the input - continue - } + if err := s.signTapscriptSpend(updater, input, i, txsighashes, prevoutFetcher); err != nil { + return "", err + } + continue + } - sign := false - - switch c := closure.(type) { - case *tree.CSVMultisigClosure: - for _, key := range c.PubKeys { - if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) { - sign = true - break - } - } - case *tree.MultisigClosure: - for _, key := range c.PubKeys { - if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) { - sign = true - break - } - } - case *tree.CLTVMultisigClosure: - for _, key := range c.PubKeys { - if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) { - sign = true - break - } - } - case *tree.ConditionMultisigClosure: - for _, key := range c.PubKeys { - if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) { - sign = true - break - } - } - } + if input.WitnessUtxo != nil { + // onchain P2TR + if bytes.Equal(input.WitnessUtxo.PkScript, onchainPkScript) { + updater.Upsbt.Inputs[i].TaprootInternalKey = schnorr.SerializePubKey(txscript.ComputeTaprootKeyNoScript(s.walletData.PubKey)) + input = updater.Upsbt.Inputs[i] + } + } - if sign { - if err := updater.AddInSighashType(txscript.SigHashDefault, i); err != nil { - return "", err - } - - hash := txscript.NewTapLeaf(leaf.LeafVersion, leaf.Script).TapHash() - - preimage, err := txscript.CalcTapscriptSignaturehash( - txsighashes, - txscript.SigHashDefault, - ptx.UnsignedTx, - i, - prevoutFetcher, - txscript.NewBaseTapLeaf(leaf.Script), - ) - if err != nil { - return "", err - } - - sig, err := schnorr.Sign(s.privateKey, preimage) - if err != nil { - return "", err - } - - if !sig.Verify(preimage, s.walletData.PubKey) { - return "", fmt.Errorf("signature verification failed") - } - - if len(updater.Upsbt.Inputs[i].TaprootScriptSpendSig) == 0 { - updater.Upsbt.Inputs[i].TaprootScriptSpendSig = make([]*psbt.TaprootScriptSpendSig, 0) - } - - updater.Upsbt.Inputs[i].TaprootScriptSpendSig = append(updater.Upsbt.Inputs[i].TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{ - XOnlyPubKey: myPubkey, - LeafHash: hash.CloneBytes(), - Signature: sig.Serialize(), - SigHash: txscript.SigHashDefault, - }) - } + // taproot key path spend + if len(input.TaprootInternalKey) > 0 { + if err := s.signTaprootKeySpend(updater, input, i, txsighashes, prevoutFetcher); err != nil { + return "", err } + continue } } @@ -285,6 +250,135 @@ func (s *bitcoinWallet) SignTransaction( return ptx.B64Encode() } +func (w *bitcoinWallet) signTapscriptSpend( + updater *psbt.Updater, + input psbt.PInput, + inputIndex int, + txsighashes *txscript.TxSigHashes, + prevoutFetcher *txscript.MultiPrevOutFetcher, +) error { + myPubkey := schnorr.SerializePubKey(w.walletData.PubKey) + + for _, leaf := range input.TaprootLeafScript { + closure, err := tree.DecodeClosure(leaf.Script) + if err != nil { + // skip unknown leaf + continue + } + + sign := false + + switch c := closure.(type) { + case *tree.CSVMultisigClosure: + for _, key := range c.PubKeys { + if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) { + sign = true + break + } + } + case *tree.MultisigClosure: + for _, key := range c.PubKeys { + if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) { + sign = true + break + } + } + case *tree.CLTVMultisigClosure: + for _, key := range c.PubKeys { + if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) { + sign = true + break + } + } + case *tree.ConditionMultisigClosure: + for _, key := range c.PubKeys { + if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) { + sign = true + break + } + } + } + + if sign { + if err := updater.AddInSighashType(txscript.SigHashDefault, inputIndex); err != nil { + return err + } + + hash := txscript.NewTapLeaf(leaf.LeafVersion, leaf.Script).TapHash() + + preimage, err := txscript.CalcTapscriptSignaturehash( + txsighashes, + txscript.SigHashDefault, + updater.Upsbt.UnsignedTx, + inputIndex, + prevoutFetcher, + txscript.NewBaseTapLeaf(leaf.Script), + ) + if err != nil { + return err + } + + sig, err := schnorr.Sign(w.privateKey, preimage) + if err != nil { + return err + } + + if len(updater.Upsbt.Inputs[inputIndex].TaprootScriptSpendSig) == 0 { + updater.Upsbt.Inputs[inputIndex].TaprootScriptSpendSig = make([]*psbt.TaprootScriptSpendSig, 0) + } + + updater.Upsbt.Inputs[inputIndex].TaprootScriptSpendSig = append(updater.Upsbt.Inputs[inputIndex].TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{ + XOnlyPubKey: myPubkey, + LeafHash: hash.CloneBytes(), + Signature: sig.Serialize(), + SigHash: txscript.SigHashDefault, + }) + } + } + + return nil +} + +func (w *bitcoinWallet) signTaprootKeySpend( + updater *psbt.Updater, + input psbt.PInput, + inputIndex int, + txsighashes *txscript.TxSigHashes, + prevoutFetcher *txscript.MultiPrevOutFetcher, +) error { + if len(input.TaprootKeySpendSig) > 0 { + // already signed, skip + return nil + } + + xOnlyPubkey := schnorr.SerializePubKey(txscript.ComputeTaprootKeyNoScript(w.walletData.PubKey)) + if !bytes.Equal(xOnlyPubkey, input.TaprootInternalKey) { + // not the wallet's key, skip + return nil + } + + preimage, err := txscript.CalcTaprootSignatureHash( + txsighashes, + txscript.SigHashDefault, + updater.Upsbt.UnsignedTx, + inputIndex, + prevoutFetcher, + ) + + if err != nil { + return err + } + + sig, err := schnorr.Sign(txscript.TweakTaprootPrivKey(*w.privateKey, nil), preimage) + if err != nil { + return err + } + + updater.Upsbt.Inputs[inputIndex].TaprootKeySpendSig = sig.Serialize() + + return nil +} + func (w *bitcoinWallet) NewVtxoTreeSigner( ctx context.Context, derivationPath string, ) (tree.SignerSession, error) { @@ -352,7 +446,34 @@ type addressWithTapscripts struct { Tapscripts []string } -func (w *bitcoinWallet) getAddress( +func (w *bitcoinWallet) getP2TRAddress( + ctx context.Context, +) (*btcutil.AddressTaproot, error) { + if w.walletData == nil { + return nil, fmt.Errorf("wallet not initialized") + } + + data, err := w.configStore.GetData(ctx) + if err != nil { + return nil, err + } + + if data == nil { + return nil, fmt.Errorf("config not set, cannot create P2TR address") + } + + netParams := utils.ToBitcoinNetwork(data.Network) + + tapKey := txscript.ComputeTaprootKeyNoScript(w.walletData.PubKey) + addr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(tapKey), &netParams) + if err != nil { + return nil, err + } + + return addr, nil +} + +func (w *bitcoinWallet) getArkAddresses( ctx context.Context, ) ( *addressWithTapscripts, diff --git a/pkg/client-sdk/wallet/wallet.go b/pkg/client-sdk/wallet/wallet.go index 18959a78c..b73ef9240 100644 --- a/pkg/client-sdk/wallet/wallet.go +++ b/pkg/client-sdk/wallet/wallet.go @@ -26,13 +26,13 @@ type WalletService interface { IsLocked() bool GetAddresses( ctx context.Context, - ) (offchainAddresses, boardingAddresses, redemptionAddresses []TapscriptsAddress, err error) + ) (onchainAddresses []string, offchainAddresses, boardingAddresses, redemptionAddresses []TapscriptsAddress, err error) NewAddress( ctx context.Context, change bool, - ) (offchainAddr, onchainAddr *TapscriptsAddress, err error) + ) (onchainAddr string, offchainAddr, boardingAddr *TapscriptsAddress, err error) NewAddresses( ctx context.Context, change bool, num int, - ) (offchainAddresses, onchainAddresses []TapscriptsAddress, err error) + ) (onchainAddresses []string, offchainAddresses, boardingAddresses []TapscriptsAddress, err error) SignTransaction( ctx context.Context, explorerSvc explorer.Explorer, tx string, ) (signedTx string, err error) diff --git a/pkg/client-sdk/wallet/wallet_test.go b/pkg/client-sdk/wallet/wallet_test.go index 9a60b9b9f..448e449e1 100644 --- a/pkg/client-sdk/wallet/wallet_test.go +++ b/pkg/client-sdk/wallet/wallet_test.go @@ -69,48 +69,54 @@ func TestWallet(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, key) - offchainAddr, onchainAddr, err := walletSvc.NewAddress(ctx, false) + onchainAddr, offchainAddr, boardingAddr, err := walletSvc.NewAddress(ctx, false) require.NoError(t, err) require.NotEmpty(t, offchainAddr) require.NotEmpty(t, onchainAddr) + require.NotEmpty(t, boardingAddr) - offchainAddrs, onchainAddrs, redemptionAddrs, err := walletSvc.GetAddresses(ctx) + onchainAddrs, offchainAddrs, boardingAddrs, redemptionAddrs, err := walletSvc.GetAddresses(ctx) require.NoError(t, err) require.Len(t, offchainAddrs, 1) require.Len(t, onchainAddrs, 1) require.Len(t, redemptionAddrs, 1) + require.Len(t, boardingAddrs, 1) - offchainAddr, onchainAddr, err = walletSvc.NewAddress(ctx, true) + onchainAddr, offchainAddr, boardingAddr, err = walletSvc.NewAddress(ctx, true) require.NoError(t, err) require.NotEmpty(t, offchainAddr) require.NotEmpty(t, onchainAddr) + require.NotEmpty(t, boardingAddr) expectedNumOfAddresses := 2 if strings.Contains(tt.name, wallet.SingleKeyWallet) { expectedNumOfAddresses = 1 } - offchainAddrs, onchainAddrs, redemptionAddrs, err = walletSvc.GetAddresses(ctx) + onchainAddrs, offchainAddrs, boardingAddrs, redemptionAddrs, err = walletSvc.GetAddresses(ctx) require.NoError(t, err) require.Len(t, offchainAddrs, expectedNumOfAddresses) require.Len(t, onchainAddrs, expectedNumOfAddresses) require.Len(t, redemptionAddrs, expectedNumOfAddresses) + require.Len(t, boardingAddrs, expectedNumOfAddresses) num := 3 - offchainAddrs, onchainAddrs, err = walletSvc.NewAddresses(ctx, false, num) + onchainAddrs, offchainAddrs, boardingAddrs, err = walletSvc.NewAddresses(ctx, false, num) require.NoError(t, err) require.Len(t, offchainAddrs, num) + require.Len(t, boardingAddrs, num) require.Len(t, onchainAddrs, num) expectedNumOfAddresses += num if strings.Contains(tt.name, wallet.SingleKeyWallet) { expectedNumOfAddresses = 1 } - offchainAddrs, onchainAddrs, redemptionAddrs, err = walletSvc.GetAddresses(ctx) + onchainAddrs, offchainAddrs, boardingAddrs, redemptionAddrs, err = walletSvc.GetAddresses(ctx) require.NoError(t, err) require.Len(t, offchainAddrs, expectedNumOfAddresses) require.Len(t, onchainAddrs, expectedNumOfAddresses) require.Len(t, redemptionAddrs, expectedNumOfAddresses) + require.Len(t, boardingAddrs, expectedNumOfAddresses) // Check no password is required to unlock if wallet is already unlocked. alreadyUnlocked, err := walletSvc.Unlock(ctx, password) diff --git a/server/cmd/arkd/main.go b/server/cmd/arkd/main.go index 872064314..2428fa11f 100755 --- a/server/cmd/arkd/main.go +++ b/server/cmd/arkd/main.go @@ -65,10 +65,6 @@ func mainAction(_ *cli.Context) error { TLSExtraDomains: cfg.TLSExtraDomains, } - if cfg.AllowZeroFees { - log.Warn("WARNING: AllowZeroFees is enabled") - } - svc, err := grpcservice.NewService(Version, svcConfig, cfg) if err != nil { return err diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 7075ffac9..5050cd92c 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -75,9 +75,6 @@ type Config struct { MarketHourRoundInterval time.Duration OtelCollectorEndpoint string - // TODO remove with transactions version 3 - AllowZeroFees bool - EsploraURL string UnlockerType string @@ -128,7 +125,6 @@ var ( MarketHourPeriod = "MARKET_HOUR_PERIOD" MarketHourRoundInterval = "MARKET_HOUR_ROUND_INTERVAL" OtelCollectorEndpoint = "OTEL_COLLECTOR_ENDPOINT" - AllowZeroFees = "ALLOW_ZERO_FEES" RoundMaxParticipantsCount = "ROUND_MAX_PARTICIPANTS_COUNT" UtxoMaxAmount = "UTXO_MAX_AMOUNT" VtxoMaxAmount = "VTXO_MAX_AMOUNT" @@ -158,7 +154,6 @@ var ( defaultVtxoMinAmount = -1 // -1 means native dust limit (default) defaultVtxoMaxAmount = -1 // -1 means no limit (default) - defaultAllowZeroFees = false defaultRoundMaxParticipantsCount = 128 ) @@ -184,7 +179,6 @@ func LoadConfig() (*Config, error) { viper.SetDefault(MarketHourEndTime, defaultMarketHourEndTime) viper.SetDefault(MarketHourPeriod, defaultMarketHourPeriod) viper.SetDefault(MarketHourRoundInterval, defaultMarketHourInterval) - viper.SetDefault(AllowZeroFees, defaultAllowZeroFees) viper.SetDefault(RoundMaxParticipantsCount, defaultRoundMaxParticipantsCount) viper.SetDefault(UtxoMaxAmount, defaultUtxoMaxAmount) viper.SetDefault(UtxoMinAmount, defaultUtxoMinAmount) @@ -226,7 +220,6 @@ func LoadConfig() (*Config, error) { MarketHourPeriod: viper.GetDuration(MarketHourPeriod), MarketHourRoundInterval: viper.GetDuration(MarketHourRoundInterval), OtelCollectorEndpoint: viper.GetString(OtelCollectorEndpoint), - AllowZeroFees: viper.GetBool(AllowZeroFees), RoundMaxParticipantsCount: viper.GetInt64(RoundMaxParticipantsCount), UtxoMaxAmount: viper.GetInt64(UtxoMaxAmount), UtxoMinAmount: viper.GetInt64(UtxoMinAmount), @@ -478,7 +471,7 @@ func (c *Config) appService() error { *c.network, c.RoundInterval, c.VtxoTreeExpiry, c.UnilateralExitDelay, c.BoardingExitDelay, c.wallet, c.repo, c.txBuilder, c.scanner, c.scheduler, c.NoteUriPrefix, c.MarketHourStartTime, c.MarketHourEndTime, c.MarketHourPeriod, c.MarketHourRoundInterval, - c.AllowZeroFees, c.RoundMaxParticipantsCount, c.UtxoMaxAmount, c.UtxoMinAmount, c.VtxoMaxAmount, c.VtxoMinAmount, + c.RoundMaxParticipantsCount, c.UtxoMaxAmount, c.UtxoMinAmount, c.VtxoMaxAmount, c.VtxoMinAmount, ) if err != nil { return err diff --git a/server/internal/core/application/service.go b/server/internal/core/application/service.go index 905baa343..6cd8b152c 100644 --- a/server/internal/core/application/service.go +++ b/server/internal/core/application/service.go @@ -25,7 +25,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/lightningnetwork/lnd/lnwallet/chainfee" log "github.com/sirupsen/logrus" ) @@ -62,10 +61,6 @@ type covenantlessService struct { serverSigningKey *secp256k1.PrivateKey serverSigningPubKey *secp256k1.PublicKey - // allowZeroFees is a temporary flag letting to submit redeem txs with zero miner fees - // this should be removed after we migrate to transactions version 3 - allowZeroFees bool - numOfBoardingInputs int numOfBoardingInputsMtx sync.RWMutex @@ -88,7 +83,6 @@ func NewService( noteUriPrefix string, marketHourStartTime, marketHourEndTime time.Time, marketHourPeriod, marketHourRoundInterval time.Duration, - allowZeroFees bool, roundMaxParticipantsCount int64, utxoMaxAmount int64, utxoMinAmount int64, @@ -151,7 +145,6 @@ func NewService( boardingExitDelay: boardingExitDelay, serverSigningKey: serverSigningKey, serverSigningPubKey: serverSigningKey.PubKey(), - allowZeroFees: allowZeroFees, forfeitsBoardingSigsChan: make(chan struct{}, 1), roundMaxParticipantsCount: roundMaxParticipantsCount, utxoMaxAmount: utxoMaxAmount, @@ -565,11 +558,16 @@ func (s *covenantlessService) SubmitRedeemTx( }) } - outputs := ptx.UnsignedTx.TxOut - - sumOfOutputs := int64(0) - for _, out := range outputs { - sumOfOutputs += out.Value + outputs := make([]*wire.TxOut, 0) // outputs excluding the anchor + foundAnchor := false + for _, out := range ptx.UnsignedTx.TxOut { + if bytes.Equal(out.PkScript, tree.ANCHOR_PKSCRIPT) { + if foundAnchor { + return "", "", fmt.Errorf("invalid tx, multiple anchor outputs") + } + foundAnchor = true + continue + } if s.vtxoMaxAmount >= 0 { if out.Value > s.vtxoMaxAmount { @@ -581,24 +579,12 @@ func (s *covenantlessService) SubmitRedeemTx( return "", "", fmt.Errorf("output amount is lower than min utxo amount:%d", s.vtxoMinAmount) } } - } - fees := sumOfInputs - sumOfOutputs - if fees < 0 { - return "", "", fmt.Errorf("invalid fees, inputs are less than outputs") + outputs = append(outputs, out) } - if !s.allowZeroFees { - minFeeRate := s.wallet.MinRelayFeeRate(ctx) - - minFees, err := common.ComputeRedeemTxFee(chainfee.SatPerKVByte(minFeeRate), ins, len(outputs)) - if err != nil { - return "", "", fmt.Errorf("failed to compute min fees: %s", err) - } - - if fees < minFees { - return "", "", fmt.Errorf("min relay fee not met, %d < %d", fees, minFees) - } + if !foundAnchor { + return "", "", fmt.Errorf("invalid tx, missing anchor output") } // recompute redeem tx @@ -695,9 +681,6 @@ func (s *covenantlessService) RegisterIntent(ctx context.Context, bip322signatur vtxosInputs := make([]domain.Vtxo, 0) // the boarding utxos to add in the commitment tx boardingInputs := make([]ports.BoardingInput, 0) - // custodial vtxos = the vtxos to recover (swept but unspent) + note vtxos - // do not require forfeit transactions - // custodialVtxos := make([]domain.Vtxo, 0) boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex diff --git a/server/internal/infrastructure/tx-builder/covenantless/builder.go b/server/internal/infrastructure/tx-builder/covenantless/builder.go index 91cfe776c..d626cfbf7 100644 --- a/server/internal/infrastructure/tx-builder/covenantless/builder.go +++ b/server/internal/infrastructure/tx-builder/covenantless/builder.go @@ -524,11 +524,6 @@ func (b *txBuilder) BuildRoundTx( return "", nil, "", nil, err } - feeAmount, err := b.minRelayFeeTreeTx() - if err != nil { - return "", nil, "", nil, err - } - sweepScript, err := (&tree.CSVMultisigClosure{ MultisigClosure: tree.MultisigClosure{ PubKeys: []*secp256k1.PublicKey{serverPubkey}, @@ -543,7 +538,7 @@ func (b *txBuilder) BuildRoundTx( if !requests.HaveOnlyOnchainOutput() { sharedOutputScript, sharedOutputAmount, err = tree.CraftSharedOutput( - receivers, feeAmount, sweepTapscriptRoot[:], + receivers, sweepTapscriptRoot[:], ) if err != nil { return "", nil, "", nil, err @@ -557,11 +552,6 @@ func (b *txBuilder) BuildRoundTx( return "", nil, "", nil, err } - minRelayFeeConnectorTx, err := b.minRelayFeeConnectorTx() - if err != nil { - return "", nil, "", nil, err - } - var nextConnectorAddress string var connectorsTreePkScript []byte var connectorsTreeAmount int64 @@ -610,7 +600,6 @@ func (b *txBuilder) BuildRoundTx( connectorsTreePkScript, connectorsTreeAmount, err = tree.CraftConnectorsOutput( connectorsTreeLeaves, - minRelayFeeConnectorTx, ) if err != nil { return "", nil, "", nil, err @@ -641,7 +630,7 @@ func (b *txBuilder) BuildRoundTx( } vtxoTree, err = tree.BuildVtxoTree( - initialOutpoint, receivers, feeAmount, sweepTapscriptRoot[:], b.vtxoTreeExpiry, + initialOutpoint, receivers, sweepTapscriptRoot[:], b.vtxoTreeExpiry, ) if err != nil { return "", nil, "", nil, err @@ -665,7 +654,6 @@ func (b *txBuilder) BuildRoundTx( connectors, err := tree.BuildConnectorsTree( rootConnectorsOutpoint, connectorsTreeLeaves, - minRelayFeeConnectorTx, ) if err != nil { return "", nil, "", nil, err @@ -1044,10 +1032,6 @@ func (b *txBuilder) createRoundTx( return ptx, nil } -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 { @@ -1120,10 +1104,6 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string, return roundTx.B64Encode() } -func (b *txBuilder) minRelayFeeTreeTx() (uint64, error) { - return b.wallet.MinRelayFee(context.Background(), uint64(common.TreeTxSize)) -} - func (b *txBuilder) selectUtxos( ctx context.Context, connectorAddresses []string, amount uint64, ) ([]ports.TxInput, uint64, error) { diff --git a/server/test/e2e/e2e_test.go b/server/test/e2e/e2e_test.go index c04da8773..b3a107a06 100644 --- a/server/test/e2e/e2e_test.go +++ b/server/test/e2e/e2e_test.go @@ -74,10 +74,10 @@ func TestSettleInSameRound(t *testing.T) { defer bob.Stop() defer grpcBob.Close() - aliceAddr, aliceBoardingAddress, err := alice.Receive(ctx) + _, aliceAddr, aliceBoardingAddress, err := alice.Receive(ctx) require.NoError(t, err) - bobAddr, bobBoardingAddress, err := bob.Receive(ctx) + _, bobAddr, bobBoardingAddress, err := bob.Receive(ctx) require.NoError(t, err) _, err = utils.RunCommand("nigiri", "faucet", aliceBoardingAddress) @@ -141,10 +141,10 @@ func TestSettleInSameRound(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, bobVtxos) - aliceOffchainAddr, _, err := alice.Receive(ctx) + _, aliceOffchainAddr, _, err := alice.Receive(ctx) require.NoError(t, err) - bobOffchainAddr, _, err := bob.Receive(ctx) + _, bobOffchainAddr, _, err := bob.Receive(ctx) require.NoError(t, err) // Alice sends to Bob @@ -264,6 +264,11 @@ func TestUnilateralExit(t *testing.T) { require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance)) require.NotZero(t, balance.Offchain.Total) + _, err = utils.RunCommand("nigiri", "faucet", receive.Onchain) + require.NoError(t, err) + + time.Sleep(5 * time.Second) + _, err = runArkCommand("redeem", "--force", "--password", utils.Password) require.NoError(t, err) @@ -305,12 +310,15 @@ func TestCollaborativeExit(t *testing.T) { } func TestReactToRedemptionOfRefreshedVtxos(t *testing.T) { + // TODO remove when checkpoint is implemented + t.Skip() + ctx := context.Background() client, grpcClient := setupArkSDK(t) defer client.Stop() defer grpcClient.Close() - arkAddr, boardingAddress, err := client.Receive(ctx) + _, arkAddr, boardingAddress, err := client.Receive(ctx) require.NoError(t, err) _, err = utils.RunCommand("nigiri", "faucet", boardingAddress) @@ -375,13 +383,16 @@ func TestReactToRedemptionOfRefreshedVtxos(t *testing.T) { } func TestReactToRedemptionOfVtxosSpentAsync(t *testing.T) { + // TODO remove when checkpoint is implemented + t.Skip() + t.Run("default vtxo script", func(t *testing.T) { ctx := context.Background() sdkClient, grpcClient := setupArkSDK(t) defer sdkClient.Stop() defer grpcClient.Close() - offchainAddress, boardingAddress, err := sdkClient.Receive(ctx) + _, offchainAddress, boardingAddress, err := sdkClient.Receive(ctx) require.NoError(t, err) _, err = utils.RunCommand("nigiri", "faucet", boardingAddress) @@ -500,7 +511,7 @@ func TestReactToRedemptionOfVtxosSpentAsync(t *testing.T) { bobPubKey := bobPrivKey.PubKey() // Fund Alice's account - offchainAddr, boardingAddress, err := alice.Receive(ctx) + _, offchainAddr, boardingAddress, err := alice.Receive(ctx) require.NoError(t, err) aliceAddr, err := common.DecodeAddress(offchainAddr) @@ -647,7 +658,7 @@ func TestReactToRedemptionOfVtxosSpentAsync(t *testing.T) { }, []*wire.TxOut{ { - Value: bobOutput.Value - 500, + Value: bobOutput.Value, PkScript: alicePkScript, }, }, @@ -779,10 +790,10 @@ func TestCollisionBetweenInRoundAndRedeemVtxo(t *testing.T) { defer bob.Stop() defer grpcBob.Close() - _, aliceBoardingAddress, err := alice.Receive(ctx) + _, _, aliceBoardingAddress, err := alice.Receive(ctx) require.NoError(t, err) - bobAddr, _, err := bob.Receive(ctx) + _, bobAddr, _, err := bob.Receive(ctx) require.NoError(t, err) _, err = utils.RunCommand("nigiri", "faucet", aliceBoardingAddress, "0.00005000") @@ -851,7 +862,7 @@ func TestAliceSendsSeveralTimesToBob(t *testing.T) { defer bob.Stop() defer grpcBob.Close() - aliceAddr, boardingAddress, err := alice.Receive(ctx) + _, aliceAddr, boardingAddress, err := alice.Receive(ctx) require.NoError(t, err) _, err = utils.RunCommand("nigiri", "faucet", boardingAddress) @@ -872,7 +883,7 @@ func TestAliceSendsSeveralTimesToBob(t *testing.T) { wg.Wait() - bobAddress, _, err := bob.Receive(ctx) + _, bobAddress, _, err := bob.Receive(ctx) require.NoError(t, err) wg.Add(1) @@ -1002,7 +1013,7 @@ func TestSendToCLTVMultisigClosure(t *testing.T) { bobPubKey := bobPrivKey.PubKey() // Fund Alice's account - offchainAddr, boardingAddress, err := alice.Receive(ctx) + _, offchainAddr, boardingAddress, err := alice.Receive(ctx) require.NoError(t, err) aliceAddr, err := common.DecodeAddress(offchainAddr) @@ -1137,7 +1148,7 @@ func TestSendToCLTVMultisigClosure(t *testing.T) { }, []*wire.TxOut{ { - Value: bobOutput.Value - 500, + Value: bobOutput.Value, PkScript: alicePkScript, }, }, @@ -1195,7 +1206,7 @@ func TestSendToConditionMultisigClosure(t *testing.T) { bobPubKey := bobPrivKey.PubKey() // Fund Alice's account - offchainAddr, boardingAddress, err := alice.Receive(ctx) + _, offchainAddr, boardingAddress, err := alice.Receive(ctx) require.NoError(t, err) aliceAddr, err := common.DecodeAddress(offchainAddr) @@ -1342,7 +1353,7 @@ func TestSendToConditionMultisigClosure(t *testing.T) { }, []*wire.TxOut{ { - Value: bobOutput.Value - 500, + Value: bobOutput.Value, PkScript: alicePkScript, }, }, diff --git a/server/test/e2e/test_utils.go b/server/test/e2e/test_utils.go index 4726f4513..88c71fe49 100644 --- a/server/test/e2e/test_utils.go +++ b/server/test/e2e/test_utils.go @@ -31,6 +31,7 @@ type ArkBalance struct { type ArkReceive struct { Offchain string `json:"offchain_address"` Boarding string `json:"boarding_address"` + Onchain string `json:"onchain_address"` } func GenerateBlock() error {