Add FindSubtrees - #250
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #250 +/- ##
==========================================
+ Coverage 47.16% 48.01% +0.84%
==========================================
Files 8 8
Lines 918 933 +15
==========================================
+ Hits 433 448 +15
Misses 476 476
Partials 9 9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| }{ | ||
| {start: 5, end: 5, wantErr: true}, | ||
| {start: 6, end: 5, wantErr: true}, | ||
| {start: 0, end: 1, want: []Subtree{{Start: 0, End: 1}}}, |
There was a problem hiding this comment.
The FindSubtrees implementation matches 1:1 the reference implementation, so I've got not doubt that it works.
For completeness though, maybe add:
- {start: 3, end: 4}: i.e one entry that doesn't start at 0 or at a power of 2
- {start:4, end: 6}: i.e a perfect subtree, that still returns two subtrees
| t.Fatalf("FindSubtrees(%d, %d): %v", start, end, err) | ||
| } | ||
| switch l := len(subtrees); l { | ||
| case 1: |
There was a problem hiding this comment.
Will this branch ever be executed?
There was a problem hiding this comment.
Good spot, removed.
| // FindSubtrees returns one or two subtrees that efficiently cover [start, end). | ||
| // It corresponds to the "Selecting Two Subtrees" procedure described in Section 4.5.1 | ||
| // of draft-ietf-plants-merkle-tree-certs. | ||
| func FindSubtrees(start, end uint64) ([]Subtree, error) { |
There was a problem hiding this comment.
This matches the reference implementation 1:1, and so it makes sense.
However, I fail to understand why the specs are written like this, do you?
- §4.5.1
The procedure either returns [start, end) as a subtree, or two subtrees, left and right, that satisfy the following properties, so if[start, end)is a perfect subtree, then the proof will only include one subtree. - The algorithm below already special cases
len([start, end)) == 1and already supports returning a single subtree. Why not replaceIf end - start is one, return a single subtree, [start, end)withif [start, end) is a valid subtree, return a single subtree [start, end)?
I presume (maybe I'm missing on something?) that all callers of FindSubtrees will call isSubtreeValid first. Should we provide function that does that? (Alternatively, I'm happy to ask on Slack / push to change the specs... but I'm looking forward to hearing what you think first!)
There was a problem hiding this comment.
Drive-by. Looks like it does something related to Decompose:
Line 248 in 600c703
but rounds up left and right to a power of 2.
Something like:
left, right := Decompose(begin, end)
mid := begin + left
left, right = roundPow2(left), roundPow2(right)
return mid - left, mid, mid + rightThe Decompose function splits the range in a way that left contains all the subtrees whose left sibling is out-of-bounds from the left side. Nicely, all these subtrees are in a prefix of [begin, end). Same, right has all subtrees whose right sibling is out-of-bounds from the right side; and it's a suffix. One of them can be empty/zero (when we are a perfect subtree, or leaning one way).
Because of the siblings property, we can round up and be sure that the left and right subtree still ends/begins at the mid point.
There was a problem hiding this comment.
Well, it doesn't round up end, so I guess it's something slightly different.
There was a problem hiding this comment.
When you/they say "subtree", is it a perfect subtree? If so, I struggle to see how it's always possible to cover a range with 2 perfect subtrees if the right one is not allowed to cross end. As an example, [0, 7) or [8,15).
So it looks like the 2 subtrees can be non-perfect. Then "efficiently cover [start, end)" means something very specific to this spec. Maybe mention what it means for clarity?
There was a problem hiding this comment.
I presume (maybe I'm missing on something?) that all callers of FindSubtrees will call isSubtreeValid first.
I don't think so - there's no requirement that start, end passed into FindSubtrees already describes a valid subtree, or did I miss something?
There was a problem hiding this comment.
Yeah, the spec behaves weirdly. For example, it decides to decompose [0,2) even though it's not needed
There was a problem hiding this comment.
I think that all callers who want to decompose a range will have to wrap the call in something along the lines of
Ah, I see what you mean now 👍.
Then it seems like this function should just do that too, inline, and fall-back to the two subtree algo? Otherwise this is a bit of a sharp edge.
I've added a commit which does that and adjusted the test + comments, wdyt?
There was a problem hiding this comment.
The thing is: it's not only the power-of-2 that's the corner case. If [begin, end) is already a "right wing" tree, the algorithm will decompose it, but it's possible not to.
For example, for all trees like [0,n), or all trees in [8,n) /* n <= 16 */ it returns 2 subtress, while one fits the definition.
https://go.dev/play/p/JtFPP6oUVsV (same code, commented out the panics)
There was a problem hiding this comment.
Hm, I see, that's what isSubtreeValid checks.
There was a problem hiding this comment.
I think the specs needs to be disambiguated, I'll ask folks and see what we can do.
| // - the returned subtree(s) fully cover the [start, end) range. | ||
| // - there are no "extra" entries covered past end, but there may be covered entries prior to start. | ||
| // - the number of entries covered before start is always less than half the size of the first returned subtree. | ||
| func FindSubtrees(start, end uint64) ([]Subtree, error) { |
There was a problem hiding this comment.
I think the struct is better - otherwise the client would need some extra logic to understand what to do when mid=end.
There was a problem hiding this comment.
Returning end seems redundant.
Also, anything breaks with start == end? Empty range -> empty decomposition start == mid == end.
Maybe in some extreme, the error could also be omitted. I imagine almost any func in this stack is checking the bounds. A common way to replace such checks/errors is a type that carries some invariant:
// Span is blah.
// Invariant: Begin <= End
type Span struct {
Begin uint64
End uint64
}
func (s Span) check() error {
if s.Begin > s.End {
// bark
}
return nil
}Then anyone who accepts this param as an input has been promised the invariant and doesn't need to check it again. Only the topmost caller will check it once.
There was a problem hiding this comment.
@pav-kv Sorry, was on autopilot and merged when I got back from lunch - didn't mean to ignore your comments!
There was a problem hiding this comment.
Yeah, it would be nice to drop the error and it's a good point about end, and the two do kinda go hand in hand.
@phbnf let's see how you end up using this in the MTC log, perhaps we can circle back and change this if it ends up fitting well there. Maybe the equality checks vs the err check end up being a wash, but worth a look.
| // FindSubtrees returns one or two subtrees that efficiently cover [start, end). | ||
| // It corresponds to the "Selecting Two Subtrees" procedure described in Section 4.5.1 | ||
| // of draft-ietf-plants-merkle-tree-certs. | ||
| func FindSubtrees(start, end uint64) ([]Subtree, error) { |
There was a problem hiding this comment.
I think that all callers who want to decompose a range will have to wrap the call in something along the lines of
Ah, I see what you mean now 👍.
Then it seems like this function should just do that too, inline, and fall-back to the two subtree algo? Otherwise this is a bit of a sharp edge.
I've added a commit which does that and adjusted the test + comments, wdyt?
| // - the returned subtree(s) fully cover the [start, end) range. | ||
| // - there are no "extra" entries covered past end, but there may be covered entries prior to start. | ||
| // - the number of entries covered before start is always less than half the size of the first returned subtree. | ||
| func FindSubtrees(start, end uint64) ([]Subtree, error) { |
There was a problem hiding this comment.
I think the struct is better - otherwise the client would need some extra logic to understand what to do when mid=end.
| // FindSubtrees returns one or two subtrees that efficiently cover [start, end). | ||
| // It corresponds to the "Selecting Two Subtrees" procedure described in Section 4.5.1 | ||
| // of draft-ietf-plants-merkle-tree-certs. | ||
| func FindSubtrees(start, end uint64) ([]Subtree, error) { |
There was a problem hiding this comment.
I think the specs needs to be disambiguated, I'll ask folks and see what we can do.
This PR adds a
FindSubtreesfunc which implements the MTC algorithm for determining the one or two subtrees which efficiently cover a range of entries.