-
Notifications
You must be signed in to change notification settings - Fork 10
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
[wip] Adds a Merkle Tree library and "snapshotting" of tables' rows #455
Conversation
e416356
to
18d5269
Compare
mEventExecutionCounter syncint64.Counter | ||
mTxnExecutionLatency syncint64.Histogram | ||
mHashCalculationElapsedTime atomic.Int64 | ||
mBaseLabels []attribute.KeyValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adds mTreeLeavesCalculationElapsedTime
to measure how long it took to snapshot the leaves
@@ -220,6 +221,11 @@ func (ep *EventProcessor) executeBlock(ctx context.Context, block eventfeed.Bloc | |||
if err := ep.calculateHash(ctx, bs); err != nil { | |||
return fmt.Errorf("calculate hash: %s", err) | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the same spot we calculate the hash we calculate the tree leaves. I'm keeping both implementations running so I can compare, but the calculateHash
would be removed before merging
@@ -215,6 +216,84 @@ func (bs *blockScope) StateHash(ctx context.Context, chainID tableland.ChainID) | |||
return executor.NewStateHash(chainID, bs.scopeVars.BlockNumber, hash), nil | |||
} | |||
|
|||
func (bs *blockScope) CalculateTreeLeaves(ctx context.Context, chainID tableland.ChainID) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where the snapshotting of the rows of each table happen. Here's how it works:
- We fetch all tables for a given chain
- For each table, we fetch all rows raw bytes. Each row is hashed.
- The hashed rows for each table are concanated together into a single BLOB (
[]byte
) - An entry for that table is added in a new table called
system_tree_leaves
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic here has some implications. Every time we call CalculateTreeLeaves
we scan all tables. Ideally we would scan only the tables that have been changed since last call of CalculateTreeLeaves
. @jsign proposed an optimization back ago to add the block a table was last touched in the registry
table.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this implementation excludes system tables. That means we are losing the capability to compare a full database state. Maybe only for those tables, we can use the current state hash solution.
return fmt.Errorf("table row scan: %s", err) | ||
} | ||
|
||
rowHash := sha3.New256() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're hashing the entire row here using. There's not a real need for that at this point. I did that so all rows have the same size, and the column leaves
of the table system_tree_leaves
would have a predictable length. If we don't hash it, we would have to add a byte to indicate where one row ends and another starts, which is fine. Also, the hashing of rows has some consequences on the storage size. For small rows (less than the size of the hash) we would be increasing storage more than necessary, but for big rows that would be a gain.
Note: All leaves are hashed again using a different hash when building the Merkle Tree. This means that the final encoding of a row really is keccak256(sha3(row))
, in other words, keccak256(sha3(row))
is the real leaf of the tree, and sha3(row)
is what's stored.
The sha3
was quite arbitrary. Can change to another hash if it makes more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. As a comment, since the leaf is later keccak-ed, we don't strictly need a cryptographic hash function like sha3, so if that is changed in the future it can be switched to something faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh right, I always go for sha
and forget about if the cryptographic properties are needed. good callout. any suggestions on standard go implementations? maybe crc32?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fnv
is pretty fast and supported in stdlib. It can be an option.
}) | ||
} | ||
|
||
func TestProperties(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Experimenting with the testing/quick
library to test properties of the tree and catch problems. It was quite good actually, found 2 bugs in my first implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
}, | ||
} | ||
|
||
t.Run("tree holds merkle tree property", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this tests if the tree holds the merkle tree property: the hashing of the leaves leads to the merkle root
} | ||
}) | ||
|
||
t.Run("leaves are always sorted", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test that we did not messed up with the order along the way
} | ||
}) | ||
|
||
t.Run("height of the tree is correct", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tests that the height property of the tree
} | ||
}) | ||
|
||
t.Run("every leaf proof is correctly verifiable", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tests that every leaf can be correctly verified by getting the proof of each one and verifying
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
I left some comments that I think you might want to take a look at.
pkg/merkletree/tree.go
Outdated
} | ||
leaf, parent = parent, parent.parent | ||
} | ||
return proof |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: may worth adding to the method comment that the provided leaf in the parameter, isn't part of the proof. That's fine and normal, since technically speaking that is known to the client.
Just to say that the proof isn't "self-contained" (i.e: you need something extra apart from the proof to verify)
}) | ||
} | ||
|
||
func TestProperties(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
rowHash.Write(col) | ||
} | ||
|
||
leaves = append(leaves, rowHash.Sum(nil)...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It maybe worth deduping here? The hash will be the same if two rows have the same column values. That would make the underlying tree have duplicate leaves next to each other and make the tree bigger unnecessarily.
|
||
rowHash := sha3.New256() | ||
for _, col := range rawBuffer { | ||
rowHash.Write(col) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After the chat in the code walk through in regard to column ordering, I found this forum post. I'm not sure of when or how it would be possible, but one of the answering posts suggests that select * from
does not guarantee column order. If that is the case, would we need to amend the above query, or sort the columns in the query results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I understood there it's that it does not follow any standard behavior, but it has a deterministic behavior:
it is the current habit of choice of SQLite specifically to return the columns in the order they are created in the internal structure, I assume which is simply because it happens to be the order in which they are parsed, which happens to be the order in which they appear in the CREATE statement
The problem is that they can change that behavior in a future version (because it's not standard) and also ALTER tables can mess up things (we don't support ALTER TABLE
, at least yet).
We could sort it but that implies that the client when producing the leaf for verification, would have to sort it too. Maybe that's ok? I think so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could sort it but that implies that the client when producing the leaf for verification, would have to sort it too. Maybe that's ok? I think so.
The client sorting seems ok to me too. Either way as you mention this isn't a problem at the moment, but maybe something to consider in the future.
ccb0412
to
1950c27
Compare
aaf4df1
to
347f5bb
Compare
Signed-off-by: Bruno Calza <[email protected]>
Signed-off-by: Bruno Calza <[email protected]>
Signed-off-by: Bruno Calza <[email protected]>
Signed-off-by: Bruno Calza <[email protected]>
Signed-off-by: Bruno Calza <[email protected]> Signed-off-by: Bruno Calza <[email protected]>
Signed-off-by: Bruno Calza <[email protected]>
6f4990d
to
d2c4818
Compare
Signed-off-by: Bruno Calza <[email protected]>
Summary
This PR implements the architecture necessary to provide Inclusion Proofs in Tableland.
Context
This can be potentially a very big PR. Not really sure how it will fully unfold. But as is, we have fully working parts. And others to come next, as described in the next section.
Implementation overview
Currently, we have the following parts implemented and ready for review:
This is an independent package that implements a simple in-memory Merkle tree. It's currently not used by any code. But it will be used by Merkle Root publisher
For every X block, we snapshot all rows for all tables into a stream of bytes row in a new table. This is described here.
Parts to come:
This part will fetch rows from '"Snapshotting" of tables' rows', build the Merkle Tree and publish the root to a smart contract
Todos
Notes
Implementation details and review orientation
Checklist