-
Notifications
You must be signed in to change notification settings - Fork 76
COW in LeafNodes #314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
COW in LeafNodes #314
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ import ( | |
"bytes" | ||
"errors" | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/crate-crypto/go-ipa/banderwagon" | ||
) | ||
|
@@ -187,6 +188,7 @@ type ( | |
|
||
commitment *Point | ||
c1, c2 *Point | ||
cow map[byte][]byte | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, so this is the "main" change of this second version of the PR. If we've never calculated a But, if we already have a In summary, instead of doing diff-updating as soon as the user calls So if the client is updating multiple times the same leaf value, we avoid doing diff-updatings that are overwritten. We only do it once when Also, this centralized the logic of This changes the previous version of this PR pattern There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ^ is the complete picture of how COW works for leaf nodes. The idea of the comment is to give you the full picture. I'll dive into some extra details about where stuff happens. |
||
|
||
depth byte | ||
} | ||
|
@@ -219,23 +221,6 @@ func NewLeafNode(stem []byte, values [][]byte) *LeafNode { | |
c2: Generator(), | ||
} | ||
|
||
// Initialize the commitment with the extension tree | ||
|
||
// marker and the stem. | ||
cfg := GetConfig() | ||
count := 0 | ||
var poly, c1poly, c2poly [256]Fr | ||
poly[0].SetUint64(1) | ||
StemFromBytes(&poly[1], leaf.stem) | ||
|
||
count = fillSuffixTreePoly(c1poly[:], values[:128]) | ||
leaf.c1 = cfg.CommitToPoly(c1poly[:], 256-count) | ||
toFr(&poly[2], leaf.c1) | ||
count = fillSuffixTreePoly(c2poly[:], values[128:]) | ||
leaf.c2 = cfg.CommitToPoly(c2poly[:], 256-count) | ||
toFr(&poly[3], leaf.c2) | ||
|
||
leaf.commitment = cfg.CommitToPoly(poly[:], 252) | ||
|
||
jsign marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
return leaf | ||
} | ||
|
||
|
@@ -844,7 +829,7 @@ func (n *LeafNode) updateC(index byte, c *Point, oldc *Fr) { | |
n.commitment.Add(n.commitment, &diff) | ||
} | ||
|
||
func (n *LeafNode) updateCn(index byte, value []byte, c *Point) { | ||
func (n *LeafNode) updateCn(index byte, oldValue []byte, c *Point) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As part of the leaf COW change, It receives the old value (now from |
||
var ( | ||
old, newH [2]Fr | ||
diff Point | ||
|
@@ -857,8 +842,8 @@ func (n *LeafNode) updateCn(index byte, value []byte, c *Point) { | |
// do not include it. The result should be the same, | ||
// but the computation time should be faster as one doesn't need to | ||
// compute 1 - 1 mod N. | ||
leafToComms(old[:], n.values[index]) | ||
leafToComms(newH[:], value) | ||
leafToComms(old[:], oldValue) | ||
leafToComms(newH[:], n.values[index]) | ||
|
||
newH[0].Sub(&newH[0], &old[0]) | ||
poly[2*(index%128)] = newH[0] | ||
|
@@ -873,42 +858,36 @@ func (n *LeafNode) updateCn(index byte, value []byte, c *Point) { | |
} | ||
|
||
func (n *LeafNode) updateLeaf(index byte, value []byte) { | ||
c, oldc := n.getOldCn(index) | ||
|
||
n.updateCn(index, value, c) | ||
// If we haven't calculated a commitment for this node, we don't need to create the cow map since all the | ||
// previous values are empty. If we already have a calculated commitment, then we track new values in | ||
// cow so we can do diff-updating in the next Commit(). | ||
if n.commitment != nil { | ||
// If cow was never setup, then initialize the map. | ||
if n.cow == nil { | ||
n.cow = make(map[byte][]byte) | ||
} | ||
|
||
n.updateC(index, c, oldc) | ||
// If we are touching an value in an index for the first time, | ||
// we save the original value for future use to update commitments. | ||
if _, ok := n.cow[index]; !ok { | ||
if n.values[index] == nil { | ||
n.cow[index] = nil | ||
} else { | ||
n.cow[index] = make([]byte, 32) | ||
copy(n.cow[index], n.values[index]) | ||
} | ||
} | ||
} | ||
|
||
n.values[index] = value | ||
} | ||
Comment on lines
860
to
883
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, here's one half of the meat of leaf COWing. We remove the current diff-updating work, so no heavy stuff is done when VKT APIs are called (e.g: insert, delete, etc). We only will do work in What we do now here, is what was explained in the TL;DR before:
|
||
|
||
func (n *LeafNode) updateMultipleLeaves(values [][]byte) { | ||
var c1, c2 *Point | ||
var old1, old2 *Fr | ||
for i, v := range values { | ||
if len(v) != 0 && !bytes.Equal(v, n.values[i]) { | ||
if i < 128 { | ||
if c1 == nil { | ||
c1, old1 = n.getOldCn(byte(i)) | ||
} | ||
n.updateCn(byte(i), v, c1) | ||
} else { | ||
if c2 == nil { | ||
c2, old2 = n.getOldCn(byte(i)) | ||
} | ||
n.updateCn(byte(i), v, c2) | ||
} | ||
|
||
n.values[i] = v | ||
for i := range values { | ||
if values[i] != nil { | ||
n.updateLeaf(byte(i), values[i]) | ||
} | ||
} | ||
|
||
if c1 != nil { | ||
n.updateC(0, c1, old1) | ||
} | ||
if c2 != nil { | ||
n.updateC(128, c2, old2) | ||
} | ||
} | ||
Comment on lines
-886
to
891
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
|
||
func (n *LeafNode) InsertOrdered(key []byte, value []byte, _ NodeFlushFn) error { | ||
|
@@ -957,8 +936,77 @@ func (n *LeafNode) Commitment() *Point { | |
return n.commitment | ||
} | ||
|
||
func (n *LeafNode) Commit() *Point { | ||
return n.commitment | ||
var frPool = sync.Pool{ | ||
New: func() any { | ||
ret := make([]Fr, NodeWidth) | ||
return &ret | ||
}, | ||
} | ||
|
||
func (leaf *LeafNode) Commit() *Point { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, so here we have the new version of |
||
// If we've never calculated a commitment for this leaf node, we calculate the commitment | ||
// in a single shot considering all the values. | ||
if leaf.commitment == nil { | ||
Comment on lines
+947
to
+949
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As explained in the comment I added, if this leaf node never was We could in theory only do diff-updating starting from a "empty" commitment and do diff-updates on top. But that's quite slow... if we have a bunch of values that are all new (since we have never Commited before), we do all the calculation in a single poly commitment. The code of this "if" block is the same as the previous version of this PR, so nothing interesting is needed for reviewing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could that not be simplified by simply setting the commitment to zero and then applying the CoW logic below to it? This should be equivalent, unless I missed something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I went with that approach initially since it would avoid an "if" case here. Unfortunately, that was slower. If you have a calculated commitment and 10 values to update with diffs-commits, you have to do 10 diff commitment which is slower than doing a single 10-values polynomial commitment. In fact, at some point if your cow is showing that you changed 256 values, probably we shouldn't be doing diff-updating too. (Under the same logic) Might be useful to try to discover when diff-updating is slower than doing the full calculation again. |
||
// Initialize the commitment with the extension tree | ||
// marker and the stem. | ||
count := 0 | ||
c1polyp := frPool.Get().(*[]Fr) | ||
c1poly := *c1polyp | ||
defer func() { | ||
for i := 0; i < 256; i++ { | ||
c1poly[i] = Fr{} | ||
} | ||
frPool.Put(c1polyp) | ||
}() | ||
|
||
count = fillSuffixTreePoly(c1poly, leaf.values[:128]) | ||
leaf.c1 = cfg.CommitToPoly(c1poly, 256-count) | ||
|
||
for i := 0; i < 256; i++ { | ||
c1poly[i] = Fr{} | ||
} | ||
count = fillSuffixTreePoly(c1poly, leaf.values[128:]) | ||
leaf.c2 = cfg.CommitToPoly(c1poly, 256-count) | ||
|
||
for i := 0; i < 256; i++ { | ||
c1poly[i] = Fr{} | ||
} | ||
c1poly[0].SetUint64(1) | ||
StemFromBytes(&c1poly[1], leaf.stem) | ||
|
||
toFrMultiple([]*Fr{&c1poly[2], &c1poly[3]}, []*Point{leaf.c1, leaf.c2}) | ||
leaf.commitment = cfg.CommitToPoly(c1poly, 252) | ||
|
||
} else if len(leaf.cow) != 0 { | ||
// If we've already have a calculated commitment, and there're touched leaf values, we do a diff update. | ||
var c1, c2 *Point | ||
var old1, old2 *Fr | ||
for i, oldValue := range leaf.cow { | ||
if !bytes.Equal(oldValue, leaf.values[i]) { | ||
if i < 128 { | ||
if c1 == nil { | ||
c1, old1 = leaf.getOldCn(i) | ||
} | ||
leaf.updateCn(i, oldValue, c1) | ||
} else { | ||
if c2 == nil { | ||
c2, old2 = leaf.getOldCn(i) | ||
} | ||
leaf.updateCn(i, oldValue, c2) | ||
} | ||
} | ||
} | ||
|
||
if c1 != nil { | ||
leaf.updateC(0, c1, old1) | ||
} | ||
if c2 != nil { | ||
leaf.updateC(128, c2, old2) | ||
} | ||
leaf.cow = nil | ||
Comment on lines
+980
to
+1006
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now we have the new case. In this case we know that What we do here is something similar to what we did before for diff updating. The only "meaningful" change is that we send In L1143 we cleanup the |
||
} | ||
|
||
return leaf.commitment | ||
} | ||
|
||
// fillSuffixTreePoly takes one of the two suffix tree and | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,11 +33,17 @@ import ( | |
"errors" | ||
"fmt" | ||
mRand "math/rand" | ||
"os" | ||
"sort" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestMain(m *testing.M) { | ||
_ = GetConfig() | ||
os.Exit(m.Run()) | ||
} | ||
Comment on lines
+42
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that is necessary: once the first test is called, it will initialize the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoiding adding You can't rely on only doing that in the first test of the file, because if you later do Makes sense to you? |
||
|
||
// a 32 byte value, as expected in the tree structure | ||
var testValue = []byte("0123456789abcdef0123456789abcdef") | ||
|
||
|
@@ -106,7 +112,6 @@ func TestInsertTwoLeavesLastLevel(t *testing.T) { | |
if !bytes.Equal(leaf.values[0], testValue) { | ||
t.Fatalf("did not find correct value in trie %x != %x", testValue, leaf.values[0]) | ||
} | ||
|
||
} | ||
|
||
func TestGetTwoLeaves(t *testing.T) { | ||
|
@@ -413,6 +418,7 @@ func TestDeleteUnequalPath(t *testing.T) { | |
t.Fatalf("didn't catch the deletion of non-existing key, err =%v", err) | ||
} | ||
} | ||
|
||
func TestDeleteResolve(t *testing.T) { | ||
key1, _ := hex.DecodeString("0105000000000000000000000000000000000000000000000000000000000000") | ||
key2, _ := hex.DecodeString("0107000000000000000000000000000000000000000000000000000000000000") | ||
|
@@ -719,7 +725,7 @@ func isLeafEqual(a, b *LeafNode) bool { | |
|
||
func TestGetResolveFromHash(t *testing.T) { | ||
var count uint | ||
var dummyError = errors.New("dummy") | ||
dummyError := errors.New("dummy") | ||
var serialized []byte | ||
getter := func([]byte) ([]byte, error) { | ||
count++ | ||
|
@@ -813,7 +819,7 @@ func TestInsertIntoHashedNode(t *testing.T) { | |
t.Fatalf("error detecting a decoding error after resolution: %v", err) | ||
} | ||
|
||
var randomResolverError = errors.New("'clef' was mispronounced") | ||
randomResolverError := errors.New("'clef' was mispronounced") | ||
// Check that the proper error is raised if the resolver returns an error | ||
erroringResolver := func(h []byte) ([]byte, error) { | ||
return nil, randomResolverError | ||
|
@@ -879,9 +885,9 @@ func TestLeafToCommsLessThan16(*testing.T) { | |
} | ||
|
||
func TestGetProofItemsNoPoaIfStemPresent(t *testing.T) { | ||
|
||
root := New() | ||
root.Insert(ffx32KeyTest, zeroKeyTest, nil) | ||
root.Commit() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's quite important now to always do |
||
|
||
// insert two keys that differ from the inserted stem | ||
// by one byte. | ||
|
Uh oh!
There was an error while loading. Please reload this page.