-
Notifications
You must be signed in to change notification settings - Fork 5
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
[1.0.4] Finality violation tests 1.0.0 #610
base: release/1.0
Are you sure you want to change the base?
Conversation
…savanna contract for clarity
…_policy_generation in savanna smart contracts and tests for clarity
generation used in finality violation tests
… collected by the light client is used
…nction with full policies
…scenarios where the fake block's timestamp is lower vs higher than block with conflicting range
33c5429
to
f07308f
Compare
…to finality_violation_tests_1.0.0
…ntelopeIO/spring into finality_violation_tests_1.0.0
("bitset_vector", bitset_8) | ||
("finalizers_count", 4) | ||
); | ||
|
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 expected bitset_string
s seem very strange to me. I get what it is doing, but it does not seem intuitive at all if we were trying to mentally map it to which finalizer is present or not when just looking at the hex string.
My hope with this string representation was following:
If I want to figure out whether finalizer i
is present or not in the QC, I first find i/4
th nibble (using zero-indexing) in the hex string and convert that nibble into string of 4 bits. I then look at the i%4
th bit (using zero-indexing) in that string of 4 bits to determine whether the finalizer is present or not.
//Compute finality digests for both proofs | ||
checksum256 digest_1 = block_finality_data_internal(proof_1.qc_block).finality_digest(); | ||
checksum256 digest_2 = block_finality_data_internal(proof_2.qc_block).finality_digest(); | ||
|
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.
For all three rules, you will need to check that the two QCs are for distinct blocks. So just check that the two computed finality digests are different here rather than having to recompute the finality digest in the different rule checking implementations.
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.
Addressed in 68c2c1d
//representation of a bitset, where each bit represents the ordinal finalizer position according to canonical sorting rules of the finalizer policy | ||
std::vector<uint8_t> finalizers; | ||
//string representation of a BLS signature | ||
std::string signature; | ||
std::optional<bool> is_weak; |
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 not sufficient.
Look at the qc_sig_t
type in Spring. Two separate (optional) bitsets are required. One to track whether a finalizer voted strong, and another one to track whether a finalizer voted weak.
If the second bitset is not present, then you know that if this is a valid QC then it must be a strong QC and all of the present finalizers (which can be checked in the first bitset which must be present) voted strong.
If the second bitset is present, then you know that if this is a valid QC then it must be a weak QC. A finalizer present in the QC must have either voted strong (if so they are present in the first bitset which must be present) or voted weak (if so they are present in the second bitset). They cannot be present in both bitsets. If a finalizer is not present in either bitset, then it its vote (assuming it even voted) was not present as part of this QC.
This level of detail is necessary to properly validate the signature of a weak QC because some finalizers may have voted weak while others voted strong.
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.
Addressed in eca9352
else { | ||
std::array<uint8_t, 32> fd_data = finality_digest.extract_as_byte_array(); | ||
message = std::string(fd_data.begin(), fd_data.end()); | ||
} |
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.
You must create up to two messages. A strong_message
capturing the finality_digest
if a strong bitset is present. And a weak_digest
capturing the result of create_weak_digest(finality_digest)
if a weak bitset is present.
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.
You would have also needed to construct up to two different aggregated public keys. A strong_agg_pub_key
would aggregate the public keys of finalizers that voted strong (present in the strong bitset) and a weak_agg_pub_key
would aggregate the public keys of finalizers that voted weak (present in the weak bitset).
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.
Addressed in eca9352
else { | ||
std::array<uint8_t, 32> fd_data = finality_digest.extract_as_byte_array(); | ||
message = std::string(fd_data.begin(), fd_data.end()); | ||
} | ||
|
||
std::string s_agg_pub_key = encode_g1_to_bls_public_key(agg_pub_key); | ||
//verify signature validity | ||
check(_verify(s_agg_pub_key, qc.signature, message), "signature verification failed"); |
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 _verify
call needs to be updated to support verifying the signature with a vector of public keys and a vector of messages where the length of both vectors is the same (in this case either length of 1 or 2).
See:
spring/libraries/chain/finality/qc.cpp
Lines 118 to 152 in 53b2392
// no reason to use bls_public_key wrapper | |
std::vector<bls12_381::g1> pubkeys; | |
pubkeys.reserve(2); | |
std::vector<std::vector<uint8_t>> digests; | |
digests.reserve(2); | |
// utility to aggregate public keys for verification | |
auto aggregate_pubkeys = [&](const auto& votes_bitset) -> bls12_381::g1 { | |
const auto n = std::min(num_finalizers, votes_bitset.size()); | |
std::vector<bls12_381::g1> pubkeys_to_aggregate; | |
pubkeys_to_aggregate.reserve(n); | |
for(auto i = 0u; i < n; ++i) { | |
if (votes_bitset[i]) { // ith finalizer voted | |
pubkeys_to_aggregate.emplace_back(finalizers[i].public_key.jacobian_montgomery_le()); | |
} | |
} | |
return bls12_381::aggregate_public_keys(pubkeys_to_aggregate); | |
}; | |
// aggregate public keys and digests for strong and weak votes | |
if( strong_votes ) { | |
pubkeys.emplace_back(aggregate_pubkeys(*strong_votes)); | |
digests.emplace_back(std::vector<uint8_t>{strong_digest.data(), strong_digest.data() + strong_digest.data_size()}); | |
} | |
if( weak_votes ) { | |
pubkeys.emplace_back(aggregate_pubkeys(*weak_votes)); | |
digests.emplace_back(std::vector<uint8_t>{weak_digest.begin(), weak_digest.end()}); | |
} | |
// validate aggregated signature | |
EOS_ASSERT( bls12_381::aggregate_verify(pubkeys, digests, sig.jacobian_montgomery_le()), | |
invalid_qc_claim, "qc signature validation failed" ); | |
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.
Addressed in eca9352
savanna::bitset proof_1_bitset(finalizer_policy.finalizers.size(), high_proof.active_policy_qc.finalizers); | ||
savanna::bitset proof_2_bitset(finalizer_policy.finalizers.size(), low_proof.active_policy_qc.finalizers); | ||
|
||
auto result = bitset::compare(proof_1_bitset, proof_2_bitset); |
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.
For rule 2 violation, we only want to count a finalizer as a bad actor if it has voted in the low_proof
(weak or strong doesn't matter) and it has voted strong in the high_proof
.
savanna::bitset proof_1_bitset(finalizer_policy.finalizers.size(), high_proof.active_policy_qc.finalizers); | ||
savanna::bitset proof_2_bitset(finalizer_policy.finalizers.size(), low_proof.active_policy_qc.finalizers); | ||
|
||
auto result = bitset::compare(proof_1_bitset, proof_2_bitset); |
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.
For rule 3 violation, we only want to count a finalizer as a bad actor if it has voted in the high_proof
(weak or strong doesn't matter) and it has voted strong in the low_proof
.
//Verify that the computed digest for the low proof doesn't appear in the list of reversible block digests committed to by the high proof | ||
auto f_itr = std::find(reversible_blocks_digests.begin(), reversible_blocks_digests.end(), computed_digest); | ||
|
||
check(f_itr==reversible_blocks_digests.end(), "finality digest of low block exists in reversible_blocks_digests vector"); |
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 check is not correct.
The digests used to construct the Merkle tree of reversible blocks are not just the finality digests.
See:
spring/libraries/chain/finality/finality_core.cpp
Lines 166 to 173 in 53b2392
block_ref_digest_data data = { | |
.block_num = refs[i].block_num(), | |
.timestamp = refs[i].timestamp, | |
.finality_digest = refs[i].finality_digest, | |
.parent_timestamp = refs[i-1].timestamp | |
}; | |
block_ref_digests.emplace_back(fc::sha256::hash(data)); |
So the check within this action at this line should never fail. And related to that, you need test cases where the rule proof fails because the conditions are not met. We need to verify that false proofs are properly rejected.
The proper way to check this is as follows:
First, the user must submit the contents of the leaf node in the reversible block Merkle tree (along with the proof of inclusion up to the reversible blocks root) for the block that has a timestamp
greater than or equal to low_proof_timestamp
and a parent_timestamp
strictly less than low_proof_timestamp
. Both of those inequalities must be validated otherwise the submitted proof is not correct.
Then, if the timestamp
for the submitted reversible blocks leaf node is strictly greater than low_proof_timestamp
, then you know that the low proof block is not an ancestor of the high proof block and so a rule 2 violation has occurred.
Otherwise, in the case where the timestamp
for the submitted reversible blocks leaf node is exactly equal to low_proof_timestamp
, you then need to calculate computed_digest
(the finality digest of the low proof block) and compare it to the finality_digest
of the submitted reversible blocks leaf node to check that they are not the same. If they are the same, then again the submitted proof is not correct. But if they are not the same, then you know that the low proof block is not an ancestor of the high proof block and so a rule 2 violation has occurred.
Better yet, as an optimization, don't calculate computed_digest
again since you would have already calculated once before to validate the QC of low_proof
. So just cache that and reuse it if necessary.
block_timestamp high_proof_last_claim_timestamp = high_proof.qc_block.level_3_commitments.value().latest_qc_claim_timestamp; | ||
|
||
//If the high proof timestamp is higher than the low proof timestamp, but the high proof last QC claim timestamp is lower than the low proof last QC claim, the lock was violated | ||
bool lock_violation = high_proof_timestamp > low_proof_timestamp && high_proof_last_claim_timestamp < low_proof_timestamp; |
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 should be:
bool lock_violation =
(high_proof_last_claim_timestamp <= low_proof_last_claim_timestamp) &&
(low_proof_timestamp < high_proof_timestamp);
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.
Addressed in : bae1c27
//Verify that the computed digest for the low proof doesn't appear in the list of reversible block digests committed to by the high proof | ||
auto f_itr = std::find(reversible_blocks_digests.begin(), reversible_blocks_digests.end(), computed_digest); | ||
|
||
check(f_itr==reversible_blocks_digests.end(), "finality digest of low block exists in reversible_blocks_digests vector"); |
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.
Again, this check is not correct for the same reasons described earlier for the rule 2 proof action. But in addition to that it is attempting to show that the low proof block is not an ancestor of the high proof block, but rule 3 requires that the block claimed by the low proof block is not an ancestor of the high proof block.
A similar process as before needs to be carried out here:
The proper way to check this is as follows:
First, the user must submit the contents of the leaf node in the reversible block Merkle tree (along with the proof of inclusion up to the reversible blocks root) for the block that has a timestamp
greater than or equal to low_proof_last_claim_timestamp
and a parent_timestamp
strictly less than low_proof_last_claim_timestamp
. Both of those inequalities must be validated otherwise the submitted proof is not correct.
Then, if the timestamp
for the submitted reversible blocks leaf node is strictly greater than low_proof_last_claim_timestamp
, then you know that the block claimed by the low proof block is not an ancestor of the high proof block and so a rule 3 violation has occurred.
Otherwise, in the case where the timestamp
for the submitted reversible blocks leaf node is exactly equal to low_proof_last_claim_timestamp
, you then need to get finality digest of the block claimed by the low proof block (low_proof.qc_block.level_3_commitments->latest_qc_claim_finality_digest
) and compare it to the finality_digest
of the submitted reversible blocks leaf node to check that they are not the same. If they are the same, then again the submitted proof is not correct. But if they are not the same, then you know that the block claimed by the low proof block is not an ancestor of the high proof block and so a rule 3 violation has occurred.
…ty_violation contract
…uorum threshold has been met
Note:start |
This PR adds tests to cover violation of finality rules #1, #2 and #3 and addresses issue : #91