Skip to content

Commit 6246f89

Browse files
authored
Add 0-value P2A output to offchain transactions (#566)
* P2A in vtxo tree and redeem txs * update validation.go * fix conflict * fix linter error * fix test unilateral exit * fix bitcoin_wallet.go * e2e test: faucet onchain * fixes * NewAddresses returns onchain P2TR addresses * e2e tests: add TODOs * fixes after reviews
1 parent 5d2671c commit 6246f89

File tree

23 files changed

+716
-350
lines changed

23 files changed

+716
-350
lines changed

client/main.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,14 @@ func dumpPrivKey(ctx *cli.Context) error {
306306
}
307307

308308
func receive(ctx *cli.Context) error {
309-
offchainAddr, boardingAddr, err := arkSdkClient.Receive(ctx.Context)
309+
onchainAddr, offchainAddr, boardingAddr, err := arkSdkClient.Receive(ctx.Context)
310310
if err != nil {
311311
return err
312312
}
313313
return printJSON(map[string]interface{}{
314314
"boarding_address": boardingAddr,
315315
"offchain_address": offchainAddr,
316+
"onchain_address": onchainAddr,
316317
})
317318
}
318319

@@ -397,10 +398,6 @@ func redeem(ctx *cli.Context) error {
397398
return arkSdkClient.StartUnilateralExit(ctx.Context)
398399
}
399400

400-
if address == "" {
401-
return fmt.Errorf("missing destination address")
402-
}
403-
404401
if complete {
405402
txID, err := arkSdkClient.CompleteUnilateralExit(ctx.Context, address)
406403
if err != nil {

common/fees.go

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,6 @@ import (
1111
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
1212
)
1313

14-
var TreeTxSize = (&input.TxWeightEstimator{}).
15-
AddTaprootKeySpendInput(txscript.SigHashDefault). // parent
16-
AddP2TROutput(). // left child
17-
AddP2TROutput(). // right child
18-
VSize()
19-
20-
var ConnectorTxSize = (&input.TxWeightEstimator{}).
21-
AddTaprootKeySpendInput(txscript.SigHashDefault).
22-
AddP2TROutput().
23-
AddP2TROutput().
24-
AddP2TROutput().
25-
AddP2TROutput().
26-
VSize()
27-
2814
func ComputeForfeitTxFee(
2915
feeRate chainfee.SatPerKVByte,
3016
tapscript *waddrmgr.Tapscript,
@@ -56,32 +42,3 @@ func ComputeForfeitTxFee(
5642

5743
return uint64(feeRate.FeeForVSize(lntypes.VByte(txWeightEstimator.VSize())).ToUnit(btcutil.AmountSatoshi)), nil
5844
}
59-
60-
func ComputeRedeemTxFee(
61-
feeRate chainfee.SatPerKVByte,
62-
vtxos []VtxoInput,
63-
numOutputs int,
64-
) (int64, error) {
65-
if len(vtxos) <= 0 {
66-
return 0, fmt.Errorf("missing vtxos")
67-
}
68-
69-
redeemTxWeightEstimator := &input.TxWeightEstimator{}
70-
71-
// Estimate inputs
72-
for _, vtxo := range vtxos {
73-
if vtxo.Tapscript == nil {
74-
txid := vtxo.Outpoint.Hash.String()
75-
return 0, fmt.Errorf("missing tapscript for vtxo %s", txid)
76-
}
77-
78-
redeemTxWeightEstimator.AddTapscriptInput(lntypes.WeightUnit(vtxo.WitnessSize), vtxo.Tapscript)
79-
}
80-
81-
// Estimate outputs
82-
for i := 0; i < numOutputs; i++ {
83-
redeemTxWeightEstimator.AddP2TROutput()
84-
}
85-
86-
return int64(feeRate.FeeForVSize(lntypes.VByte(redeemTxWeightEstimator.VSize())).ToUnit(btcutil.AmountSatoshi)), nil
87-
}

common/tree/anchor.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package tree
2+
3+
import (
4+
"bytes"
5+
6+
"github.com/btcsuite/btcd/btcutil/psbt"
7+
"github.com/btcsuite/btcd/txscript"
8+
"github.com/btcsuite/btcd/wire"
9+
)
10+
11+
var (
12+
ANCHOR_PKSCRIPT = []byte{
13+
0x51, 0x02, 0x4e, 0x73,
14+
}
15+
ANCHOR_VALUE = int64(0)
16+
)
17+
18+
func AnchorOutput() *wire.TxOut {
19+
return &wire.TxOut{
20+
Value: ANCHOR_VALUE,
21+
PkScript: ANCHOR_PKSCRIPT,
22+
}
23+
}
24+
25+
// ExtractWithAnchors extracts the final witness and scriptSig from psbt fields and ignores anchor inputs without failing.
26+
func ExtractWithAnchors(p *psbt.Packet) (*wire.MsgTx, error) {
27+
finalTx := p.UnsignedTx.Copy()
28+
29+
for i, tin := range finalTx.TxIn {
30+
pInput := p.Inputs[i]
31+
32+
// ignore anchor outputs
33+
if pInput.WitnessUtxo != nil && bytes.Equal(pInput.WitnessUtxo.PkScript, ANCHOR_PKSCRIPT) {
34+
continue
35+
}
36+
37+
if pInput.FinalScriptSig != nil {
38+
tin.SignatureScript = pInput.FinalScriptSig
39+
}
40+
41+
if pInput.FinalScriptWitness != nil {
42+
witnessReader := bytes.NewReader(
43+
pInput.FinalScriptWitness,
44+
)
45+
46+
witCount, err := wire.ReadVarInt(witnessReader, 0)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
tin.Witness = make(wire.TxWitness, witCount)
52+
for j := uint64(0); j < witCount; j++ {
53+
wit, err := wire.ReadVarBytes(
54+
witnessReader, 0,
55+
txscript.MaxScriptSize, "witness",
56+
)
57+
if err != nil {
58+
return nil, err
59+
}
60+
tin.Witness[j] = wit
61+
}
62+
}
63+
}
64+
65+
return finalTx, nil
66+
}

common/tree/builder.go

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ const (
2222
// radix is hardcoded to 2
2323
func CraftSharedOutput(
2424
receivers []Leaf,
25-
feeSatsPerNode uint64,
2625
sweepTapTreeRoot []byte,
2726
) ([]byte, int64, error) {
28-
root, err := createTxTree(receivers, feeSatsPerNode, sweepTapTreeRoot, vtxoTreeRadix)
27+
root, err := createTxTree(receivers, sweepTapTreeRoot, vtxoTreeRadix)
2928
if err != nil {
3029
return nil, 0, err
3130
}
3231

33-
amount := root.getAmount() + int64(feeSatsPerNode)
32+
amount := root.getAmount() + ANCHOR_VALUE
3433

3534
aggregatedKey, err := AggregateKeys(root.getCosigners(), sweepTapTreeRoot)
3635
if err != nil {
@@ -50,11 +49,10 @@ func CraftSharedOutput(
5049
func BuildVtxoTree(
5150
initialInput *wire.OutPoint,
5251
receivers []Leaf,
53-
feeSatsPerNode uint64,
5452
sweepTapTreeRoot []byte,
5553
vtxoTreeExpiry common.RelativeLocktime,
5654
) (TxTree, error) {
57-
root, err := createTxTree(receivers, feeSatsPerNode, sweepTapTreeRoot, vtxoTreeRadix)
55+
root, err := createTxTree(receivers, sweepTapTreeRoot, vtxoTreeRadix)
5856
if err != nil {
5957
return nil, err
6058
}
@@ -66,14 +64,13 @@ func BuildVtxoTree(
6664
// radix is hardcoded to 4
6765
func CraftConnectorsOutput(
6866
receivers []Leaf,
69-
feeSatsPerNode uint64,
7067
) ([]byte, int64, error) {
71-
root, err := createTxTree(receivers, feeSatsPerNode, nil, connectorsTreeRadix)
68+
root, err := createTxTree(receivers, nil, connectorsTreeRadix)
7269
if err != nil {
7370
return nil, 0, err
7471
}
7572

76-
amount := root.getAmount() + int64(feeSatsPerNode)
73+
amount := root.getAmount() + ANCHOR_VALUE
7774

7875
aggregatedKey, err := AggregateKeys(root.getCosigners(), nil)
7976
if err != nil {
@@ -93,9 +90,8 @@ func CraftConnectorsOutput(
9390
func BuildConnectorsTree(
9491
initialInput *wire.OutPoint,
9592
receivers []Leaf,
96-
feeSatsPerNode uint64,
9793
) (TxTree, error) {
98-
root, err := createTxTree(receivers, feeSatsPerNode, nil, connectorsTreeRadix)
94+
root, err := createTxTree(receivers, nil, connectorsTreeRadix)
9995
if err != nil {
10096
return nil, err
10197
}
@@ -150,7 +146,7 @@ func toTxTree(root node, initialInput *wire.OutPoint, expiry *common.RelativeLoc
150146
}
151147

152148
type node interface {
153-
getAmount() int64 // returns the input amount of the node = sum of all receivers' amounts + fees
149+
getAmount() int64 // returns the input amount of the node = sum of all receivers' amounts
154150
getOutputs() ([]*wire.TxOut, error)
155151
getChildren() []node
156152
getCosigners() []*secp256k1.PublicKey
@@ -167,7 +163,6 @@ type branch struct {
167163
cosigners []*secp256k1.PublicKey
168164
pkScript []byte
169165
children []node
170-
feeAmount int64
171166
}
172167

173168
func (b *branch) getCosigners() []*secp256k1.PublicKey {
@@ -190,7 +185,7 @@ func (b *branch) getAmount() int64 {
190185
amount := int64(0)
191186
for _, child := range b.children {
192187
amount += child.getAmount()
193-
amount += b.feeAmount
188+
amount += ANCHOR_VALUE
194189
}
195190

196191
return amount
@@ -206,6 +201,7 @@ func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
206201
Value: l.amount,
207202
PkScript: l.pkScript,
208203
},
204+
AnchorOutput(),
209205
}, nil
210206
}
211207

@@ -214,12 +210,12 @@ func (b *branch) getOutputs() ([]*wire.TxOut, error) {
214210

215211
for _, child := range b.children {
216212
outputs = append(outputs, &wire.TxOut{
217-
Value: child.getAmount() + b.feeAmount,
213+
Value: child.getAmount(),
218214
PkScript: b.pkScript,
219215
})
220216
}
221217

222-
return outputs, nil
218+
return append(outputs, AnchorOutput()), nil
223219
}
224220

225221
func getTreeNode(
@@ -257,7 +253,7 @@ func getTx(
257253
return nil, err
258254
}
259255

260-
tx, err := psbt.New([]*wire.OutPoint{input}, outputs, 2, 0, []uint32{wire.MaxTxInSequenceNum})
256+
tx, err := psbt.New([]*wire.OutPoint{input}, outputs, 3, 0, []uint32{wire.MaxTxInSequenceNum})
261257
if err != nil {
262258
return nil, err
263259
}
@@ -290,7 +286,6 @@ func getTx(
290286
// from the leaves to the root.
291287
func createTxTree(
292288
receivers []Leaf,
293-
feeSatsPerNode uint64,
294289
tapTreeRoot []byte,
295290
radix int,
296291
) (root node, err error) {
@@ -372,7 +367,7 @@ func createTxTree(
372367
}
373368

374369
for len(nodes) > 1 {
375-
nodes, err = createUpperLevel(nodes, int64(feeSatsPerNode), tapTreeRoot, radix)
370+
nodes, err = createUpperLevel(nodes, tapTreeRoot, radix)
376371
if err != nil {
377372
return nil, fmt.Errorf("failed to create tx tree: %w", err)
378373
}
@@ -381,20 +376,20 @@ func createTxTree(
381376
return nodes[0], nil
382377
}
383378

384-
func createUpperLevel(nodes []node, feeAmount int64, tapTreeRoot []byte, radix int) ([]node, error) {
379+
func createUpperLevel(nodes []node, tapTreeRoot []byte, radix int) ([]node, error) {
385380
if len(nodes) <= 1 {
386381
return nodes, nil
387382
}
388383

389384
if len(nodes) < radix {
390-
return createUpperLevel(nodes, feeAmount, tapTreeRoot, len(nodes))
385+
return createUpperLevel(nodes, tapTreeRoot, len(nodes))
391386
}
392387

393388
remainder := len(nodes) % radix
394389
if remainder != 0 {
395390
// Handle nodes that don't form a complete group
396391
last := nodes[len(nodes)-remainder:]
397-
groups, err := createUpperLevel(nodes[:len(nodes)-remainder], feeAmount, tapTreeRoot, radix)
392+
groups, err := createUpperLevel(nodes[:len(nodes)-remainder], tapTreeRoot, radix)
398393
if err != nil {
399394
return nil, err
400395
}
@@ -425,7 +420,6 @@ func createUpperLevel(nodes []node, feeAmount int64, tapTreeRoot []byte, radix i
425420
branchNode := &branch{
426421
pkScript: pkScript,
427422
cosigners: cosigners,
428-
feeAmount: feeAmount,
429423
children: children,
430424
}
431425

common/tree/musig2_test.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ import (
1414
"github.com/stretchr/testify/require"
1515
)
1616

17-
const (
18-
minRelayFee = 1000
19-
exitDelay = 512
20-
)
21-
2217
var (
2318
vtxoTreeExpiry = common.RelativeLocktime{Type: common.LocktimeTypeBlock, Value: 144}
2419
rootInput, _ = wire.NewOutPointFromString("49f8664acc899be91902f8ade781b7eeb9cbe22bdd9efbc36e56195de21bcd12:0")
@@ -41,14 +36,14 @@ func TestBuildAndSignVtxoTree(t *testing.T) {
4136
for _, v := range testVectors {
4237
t.Run(v.name, func(t *testing.T) {
4338
sharedOutScript, sharedOutAmount, err := tree.CraftSharedOutput(
44-
v.receivers, minRelayFee, sweepRoot[:],
39+
v.receivers, sweepRoot[:],
4540
)
4641
require.NoError(t, err)
4742
require.NotNil(t, sharedOutScript)
4843
require.NotZero(t, sharedOutAmount)
4944

5045
vtxoTree, err := tree.BuildVtxoTree(
51-
rootInput, v.receivers, minRelayFee, sweepRoot[:], vtxoTreeExpiry,
46+
rootInput, v.receivers, sweepRoot[:], vtxoTreeExpiry,
5247
)
5348
require.NoError(t, err)
5449
require.NotNil(t, vtxoTree)

common/tree/pending.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const (
1414
cltvSequence = wire.MaxTxInSequenceNum - 1
1515
)
1616

17+
// BuildRedeemTx builds a redeem tx for the given vtxos and outputs.
18+
// The redeem tx is spending VTXOs using collaborative taproot path.
19+
// An anchor output is added to the transaction
1720
func BuildRedeemTx(
1821
vtxos []common.VtxoInput,
1922
outputs []*wire.TxOut,
@@ -95,7 +98,7 @@ func BuildRedeemTx(
9598
}
9699

97100
redeemPtx, err := psbt.New(
98-
ins, outputs, 2, uint32(txLocktime), sequences,
101+
ins, append(outputs, AnchorOutput()), 3, uint32(txLocktime), sequences,
99102
)
100103
if err != nil {
101104
return "", err

common/tree/validation.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func ValidateVtxoTree(
112112
sumRootValue += output.Value
113113
}
114114

115-
if sumRootValue >= roundTxAmount {
115+
if sumRootValue != roundTxAmount {
116116
return ErrInvalidAmount
117117
}
118118

@@ -220,7 +220,7 @@ func validateNodeTransaction(node Node, tree TxTree, tapTreeRoot []byte) error {
220220
sumChildAmount += output.Value
221221
}
222222

223-
if sumChildAmount >= parentOutput.Value {
223+
if sumChildAmount != parentOutput.Value {
224224
return ErrInvalidAmount
225225
}
226226
}

pkg/client-sdk/ark_sdk.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type ArkClient interface {
2020
Unlock(ctx context.Context, password string) error
2121
Lock(ctx context.Context) error
2222
Balance(ctx context.Context, computeExpiryDetails bool) (*Balance, error)
23-
Receive(ctx context.Context) (offchainAddr, boardingAddr string, err error)
23+
Receive(ctx context.Context) (onchainAddr, offchainAddr, boardingAddr string, err error)
2424
SendOffChain(
2525
ctx context.Context, withExpiryCoinselect bool, receivers []Receiver,
2626
withZeroFees bool,

0 commit comments

Comments
 (0)