-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathtree_path_convert.go
More file actions
364 lines (309 loc) · 9.98 KB
/
Copy pathtree_path_convert.go
File metadata and controls
364 lines (309 loc) · 9.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
package arkrpc
import (
"fmt"
"sort"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/wire/v2"
"github.com/lightninglabs/wavelength/lib/tree"
)
// TreePathFromTree converts a tree.Tree (typically an extracted path) into its
// proto TreePath representation by flattening the recursive node structure
// into a pre-order indexed slice.
func TreePathFromTree(t *tree.Tree) (*TreePath, error) {
if t == nil {
return nil, nil
}
// Flatten nodes in pre-order.
var nodes []*TreePathNode
nodeIndex := make(map[*tree.Node]uint32)
if err := flattenTreePathNode(
t.Root, &nodes, nodeIndex,
); err != nil {
return nil, err
}
return &TreePath{
Nodes: nodes,
BatchOutpoint: outpointToProto(t.BatchOutpoint),
BatchOutput: txOutToProto(t.BatchOutput),
SweepTapscriptRoot: t.SweepTapscriptRoot,
}, nil
}
// DefaultMaxTreePathNodes is the upper bound on the number of nodes
// allowed in a TreePath decoded from an indexer response. A path is a
// slice through a commitment tree, so it is far smaller than a whole
// tree in practice; the bound is deliberately set to the same generous
// figure roundpb.DefaultMaxTreeNodes uses for a full tree, so the two
// decode boundaries agree and no shape that survives one is rejected by
// the other purely on size.
const DefaultMaxTreePathNodes = 50_000
// TreePathToTreeOption is a functional option for TreePathToTree that
// allows callers to override default validation parameters.
type TreePathToTreeOption func(*treePathToTreeConfig)
// treePathToTreeConfig holds configurable validation parameters for tree
// path deserialization.
type treePathToTreeConfig struct {
maxNodes int
}
// defaultTreePathToTreeConfig returns the default configuration.
func defaultTreePathToTreeConfig() treePathToTreeConfig {
return treePathToTreeConfig{
maxNodes: DefaultMaxTreePathNodes,
}
}
// WithMaxTreePathNodes sets the maximum number of nodes allowed in a
// deserialized TreePath. A value of 0 disables the limit.
func WithMaxTreePathNodes(maxNodes int) TreePathToTreeOption {
return func(cfg *treePathToTreeConfig) {
cfg.maxNodes = maxNodes
}
}
// TreePathToTree converts a proto TreePath back to a tree.Tree by
// reconstructing the recursive node structure from the flattened nodes.
//
// This sits on the indexer receive path, which treats responses as
// untrusted, so the reconstructed shape is validated to be exactly one
// connected tree rather than merely an acyclic graph.
func TreePathToTree(tp *TreePath,
opts ...TreePathToTreeOption) (*tree.Tree, error) {
if tp == nil {
return nil, nil
}
cfg := defaultTreePathToTreeConfig()
for _, o := range opts {
o(&cfg)
}
if len(tp.Nodes) == 0 {
return nil, fmt.Errorf("empty tree path nodes")
}
if cfg.maxNodes > 0 && len(tp.Nodes) > cfg.maxNodes {
return nil, fmt.Errorf("tree path has %d nodes, exceeds "+
"maximum %d", len(tp.Nodes), cfg.maxNodes)
}
// Convert all proto nodes to Go nodes.
goNodes := make([]*tree.Node, len(tp.Nodes))
for i, pn := range tp.Nodes {
node, err := treePathNodeFromProto(pn)
if err != nil {
return nil, fmt.Errorf("node[%d]: %w", i, err)
}
goNodes[i] = node
}
// Wire up children references. The pre-order invariant
// (childIdx > i) rules out cycles, but on its own it still
// admits a DAG: two parents at different indices can both name
// the same higher-indexed child and both satisfy it.
//
// That matters most here. Children is a map, so one node may
// point all of its outputs at the same next node, and every
// recursive walk over the result then re-visits the shared
// subtree once per path that reaches it. nodeMaxDepth, which the
// receive path runs on this very tree to validate the claimed
// ancestry depth, is exactly such a walk: it has no memoization,
// so a shared child turns a linear-sized message into
// exponentially many paths.
//
// parentOf records the parent that claimed each child, so a
// second claim can name both of them. The bounds check runs
// first so a wild index is reported as out of range rather than
// as a sharing violation.
parentOf := make(map[uint32]int, len(tp.Nodes))
for i, pn := range tp.Nodes {
for outIdx, childIdx := range pn.Children {
if childIdx <= uint32(i) {
return nil, fmt.Errorf("node[%d] child index "+
"%d must be > parent index (cycle or "+
"back-reference)", i, childIdx)
}
if int(childIdx) >= len(goNodes) {
return nil, fmt.Errorf("node[%d] child index "+
"%d out of range", i, childIdx)
}
if prev, dup := parentOf[childIdx]; dup {
return nil, fmt.Errorf("node[%d] child index "+
"%d is already a child of node[%d]; "+
"tree must not share children", i,
childIdx, prev)
}
parentOf[childIdx] = i
if int(outIdx) >= len(goNodes[i].Outputs) {
return nil, fmt.Errorf("node[%d] child output "+
"index %d out of range (node has %d "+
"outputs)", i, outIdx,
len(goNodes[i].Outputs))
}
goNodes[i].Children[outIdx] = goNodes[childIdx]
}
}
// Single-parent plus forward-edges describes a forest, not a
// tree: nothing above requires a node to be reachable from index
// 0. Every node other than the root must be claimed exactly
// once, so counting the claims pins the shape to one connected
// tree and rejects both padding with unreachable nodes and a
// second detached root carrying its own subtree.
// flattenTreePathNode always emits exactly this shape, so no
// well-formed producer is affected.
if len(parentOf) != len(tp.Nodes)-1 {
return nil, fmt.Errorf("tree path has %d nodes but only %d "+
"are claimed as children; unreachable nodes or "+
"multiple roots", len(tp.Nodes), len(parentOf))
}
batchOP, err := outpointFromProto(tp.BatchOutpoint)
if err != nil {
return nil, fmt.Errorf("batch outpoint: %w", err)
}
batchOut, err := txOutFromProto(tp.BatchOutput)
if err != nil {
return nil, fmt.Errorf("batch output: %w", err)
}
return &tree.Tree{
Root: goNodes[0],
BatchOutpoint: batchOP,
BatchOutput: batchOut,
SweepTapscriptRoot: tp.SweepTapscriptRoot,
}, nil
}
// flattenTreePathNode recursively flattens a tree node into the nodes slice
// in pre-order.
func flattenTreePathNode(n *tree.Node, nodes *[]*TreePathNode,
index map[*tree.Node]uint32) error {
if n == nil {
return nil
}
myIdx := uint32(len(*nodes))
index[n] = myIdx
// Convert outputs.
outputs := make([]*TxOut, len(n.Outputs))
for i, out := range n.Outputs {
outputs[i] = txOutToProto(out)
}
// Convert co-signers.
coSigners := make([][]byte, len(n.CoSigners))
for i, pk := range n.CoSigners {
coSigners[i] = pk.SerializeCompressed()
}
protoNode := &TreePathNode{
Input: outpointToProto(n.Input),
Outputs: outputs,
CoSigners: coSigners,
Children: make(map[uint32]uint32),
Amount: int64(n.Amount),
Signature: schnorrSigToBytes(n.Signature),
}
*nodes = append(*nodes, protoNode)
// Recurse into children in deterministic order so the
// flattened output is stable across runs.
childIndices := make([]uint32, 0, len(n.Children))
for outIdx := range n.Children {
childIndices = append(childIndices, outIdx)
}
sort.Slice(childIndices, func(i, j int) bool {
return childIndices[i] < childIndices[j]
})
for _, outIdx := range childIndices {
child := n.Children[outIdx]
if err := flattenTreePathNode(
child, nodes, index,
); err != nil {
return err
}
protoNode.Children[outIdx] = index[child]
}
return nil
}
// treePathNodeFromProto converts a single proto TreePathNode to a tree.Node.
func treePathNodeFromProto(pn *TreePathNode) (*tree.Node, error) {
input, err := outpointFromProto(pn.Input)
if err != nil {
return nil, fmt.Errorf("input: %w", err)
}
// Convert outputs.
outputs := make([]*wire.TxOut, len(pn.Outputs))
for i, out := range pn.Outputs {
txOut, err := txOutFromProto(out)
if err != nil {
return nil, fmt.Errorf("output[%d]: %w", i, err)
}
outputs[i] = txOut
}
// Convert co-signers.
coSigners := make([]*btcec.PublicKey, len(pn.CoSigners))
for i, pkBytes := range pn.CoSigners {
pk, err := btcec.ParsePubKey(pkBytes)
if err != nil {
return nil, fmt.Errorf("co_signer[%d]: %w", i, err)
}
coSigners[i] = pk
}
var sig *schnorr.Signature
if len(pn.Signature) > 0 {
sig, err = schnorr.ParseSignature(pn.Signature)
if err != nil {
return nil, fmt.Errorf("signature: %w", err)
}
}
return &tree.Node{
Input: input,
Outputs: outputs,
CoSigners: coSigners,
Children: make(map[uint32]*tree.Node),
Amount: btcutil.Amount(pn.Amount),
Signature: sig,
}, nil
}
// outpointToProto converts a wire.OutPoint to a proto OutPoint.
func outpointToProto(op wire.OutPoint) *OutPoint {
hash := op.Hash[:]
return &OutPoint{
Txid: append([]byte(nil), hash...),
Vout: op.Index,
}
}
// outpointFromProto converts a proto OutPoint to a wire.OutPoint.
func outpointFromProto(op *OutPoint) (wire.OutPoint, error) {
if op == nil {
return wire.OutPoint{}, fmt.Errorf("nil outpoint")
}
if len(op.Txid) != chainhash.HashSize {
return wire.OutPoint{}, fmt.Errorf("invalid txid length %d",
len(op.Txid))
}
var hash chainhash.Hash
copy(hash[:], op.Txid)
return wire.OutPoint{
Hash: hash,
Index: op.Vout,
}, nil
}
// txOutToProto converts a wire.TxOut to a proto TxOut.
func txOutToProto(out *wire.TxOut) *TxOut {
if out == nil {
return nil
}
return &TxOut{
Value: out.Value,
PkScript: append([]byte(nil), out.PkScript...),
}
}
// txOutFromProto converts a proto TxOut to a wire.TxOut.
func txOutFromProto(out *TxOut) (*wire.TxOut, error) {
if out == nil {
return nil, nil
}
if out.Value < 0 {
return nil, fmt.Errorf("negative output value %d", out.Value)
}
return &wire.TxOut{
Value: out.Value,
PkScript: append([]byte(nil), out.PkScript...),
}, nil
}
// schnorrSigToBytes serializes a schnorr signature to bytes.
func schnorrSigToBytes(sig *schnorr.Signature) []byte {
if sig == nil {
return nil
}
return sig.Serialize()
}