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
refactor!: don't send public key with signature #4518
base: main
Are you sure you want to change the base?
Conversation
cee8760
to
df420c1
Compare
df420c1
to
120c0df
Compare
120c0df
to
f66a58f
Compare
dc4d7d7
to
3701fc5
Compare
bdb903e
to
fb852e9
Compare
050891b
to
51eff2c
Compare
d1c63f2
to
9edd012
Compare
core/src/sumeragi/view_change.rs
Outdated
self.signatures | ||
.iter() | ||
.try_fold(IndexSet::new(), |mut acc, elem| { | ||
if !acc.insert(elem) { | ||
return Err("Duplicate signature in 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.
I'm not sure whether proofs are duplicated if peer is repeatedly requesting view change?
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.
Since you've changed type from set to vector singnatures now can duplicate.
You can look at merge_signatures
function.
Signed-off-by: Marin Veršić <marin.versic101@gmail.com>
let topology = Topology::new(UniqueVec::new()); | ||
let (peer_public_key, _) = KeyPair::random().into_parts(); | ||
let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); | ||
let topology = Topology::new(vec![peer_id]); |
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've changed topology to have at least one element?
If so what happens when last peer is unregistered in Unregister<Peer>
?
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.
I didn't think about that though. I didn't strictly impose the limit that there must be 1 peer in the topology, rather it's just that new topology has to be created with 1 peer at least. Do you agree with this change? Do you think we should impose a limit to prevent topology from going to 0?
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.
Do you think we should impose a limit to prevent topology from going to 0?
Right now we would panic in Topology::new
if topology is empty.
But it's possible to get empty topology through block_committed
call afaik.
As well calls to leader
and proxy_tail
would panic in case topology is empty.
So i think we should bring consistency.
Essentially empty topology is possible but it's the end of the blockchain since no new blocks would be produced ever.
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.
Found ticket #3522 created by me exactly 1 year ago lol
if additional_leader_signatures.next().is_some() { | ||
return Err(SignatureVerificationError::DuplicateSignatures { | ||
signatory: leader_index, | ||
}); | ||
} |
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.
I have concern about this check, can't malicious peer insert wrong index and valid signature (for random key pair)?
So that it looks like leader submitted multiple signatures.
This way malicous peer can't prevent safety of blockchain, but can violate liveliness.
Maybe we can require that at least one signature should be from leader?
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.
malicious peer can send all sorts of invalid blocks. What you're saying is true, but is no less dangerous than sending other types of invalid blocks. We just reject a block sent from this peer, there is no harm in that IMO
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.
I think what @Erigara is saying is that the block validation will globally and repeatedly fail unless it has a mechanism to identify and ignore (or expel from the validator set) the peer that spoof index zero with its signature.
The problem here would be trusting the index without checking it against the signature
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.
I think what @Erigara is saying is that the block validation will globally and repeatedly fail unless it has a mechanism to identify and ignore (or expel from the validator set) the peer that spoof index zero with its signature.
spoofing 0 index is just one way in which malicious peer can affect consensus. Worst case, malicious peer can withhold sending any messages to other peers. It will only fail consensus repeatedly until the malicious peer becomes observing peer. 0 index spoofing is no special case and I don't think we should be more forgiving in this case
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.
Ok, at least this is not a problem specific to this PR. It used to trust public keys instead of indices
//let roles: &[Role] = if topology.view_change_index() >= 1 { | ||
// &[Role::ValidatingPeer, Role::ObservingPeer] | ||
//} else { | ||
// if topology | ||
// .filter_signatures_by_roles(&[Role::ObservingPeer], block.signatures()) | ||
// .next() | ||
// .is_some() | ||
// { | ||
// return Err(SignatureVerificationError::UnknownSignatory); | ||
// } | ||
|
||
// &[Role::ValidatingPeer] | ||
//}; |
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.
Commented code.
core/src/sumeragi/view_change.rs
Outdated
self.signatures | ||
.iter() | ||
.try_fold(IndexSet::new(), |mut acc, elem| { | ||
if !acc.insert(elem) { | ||
return Err("Duplicate signature in 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.
Since you've changed type from set to vector singnatures now can duplicate.
You can look at merge_signatures
function.
Serialize, | ||
IntoSchema, | ||
)] | ||
pub struct QuerySignature(pub PublicKey, pub SignatureOf<ClientQueryPayload>); |
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 Sato's work pub key can be extracted from AccoundId
.
211e87a
to
ad1aa08
Compare
core/src/sumeragi/main_loop.rs
Outdated
// FIX: Release writer before handling block sync | ||
let curr_block = voting_block.take(); |
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.
I have this issue in 4 places. It happens either on BlockSyncUpdate
or BlockCreated
. At the time of receiving a new block it's possible that the block is invalid and I would like to keep the old block while validating the new one. However, this creates deadlock as I have not released previous writer
@Erigara do you have some suggestions?
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.
Unfortunately it’s current limitation of our state layout.
That it’s single writer and locks state for duration of block.
When i’ve designed state i made some decisions for making optimisations for happy path (block committed) without problems.
We might reconsider some of this decisions if necessary.
Maybe we can be smarter about when to reject incoming block? Like checking signatures, height, hashes this checks doesn’t need ability to modify state.
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.
well, I'll just drop the current voting block in this PR as it was before and open an issue for this
ad1aa08
to
1479b74
Compare
(BlockMessage::BlockSigned(BlockSigned { signatures }), Role::ProxyTail) => { | ||
info!( | ||
peer_id=%self.peer_id, | ||
role=%self.role(), | ||
"Received block signatures" | ||
); | ||
|
||
let roles: &[Role] = if current_view_change_index >= 1 { | ||
let roles: &[Role] = if view_change_index >= 1 { | ||
&[Role::ValidatingPeer, Role::ObservingPeer] | ||
} else { | ||
&[Role::ValidatingPeer] | ||
}; | ||
let valid_signatures = | ||
current_topology.filter_signatures_by_roles(roles, &signatures); | ||
|
||
if let Some(voted_block) = voting_block.as_mut() { | ||
let voting_block_hash = voted_block.block.as_ref().hash_of_payload(); | ||
|
||
if hash == voting_block_hash { | ||
add_signatures::<true>(voted_block, valid_signatures); | ||
} else { | ||
debug!(%voting_block_hash, "Received signatures are not for the current block"); | ||
} | ||
let valid_signatures = self | ||
.topology | ||
.filter_signatures_by_roles(roles, &signatures) | ||
.cloned(); | ||
|
||
if let Some(mut voted_block) = voting_block.take() { | ||
add_signatures::<true>(&mut voted_block, valid_signatures, &self.topology); | ||
*voting_block = self.try_commit_block(voted_block, is_genesis_peer); | ||
} else { | ||
// NOTE: Due to the nature of distributed systems, signatures can sometimes be received before | ||
// the block (sent by the leader). Collect the signatures and wait for the block to be received | ||
voting_signatures.extend(valid_signatures); | ||
} | ||
} |
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.
By looking at this code and try_commit
it look like we don't check that signature indeed belong to the peer.
So any peer (including malicious leader itself) can send BlockSigned
message with random block matching signatures for enough validating/observing peers and block will be committed.
Another problem is that on this path signatures are not checked for duplication.
So as before peer can submit BlockSigned
message with the same index and signature and block will be committed.
.block | ||
.commit(&self.topology) | ||
.unpack(|e| self.send_event(e)) | ||
.expect("INTERNAL BUG: Proxy tail failed to commit block"); | ||
|
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.
Maybe instead of unwrapping we can try to commit the block in the if
statement?
I think it would be more reliable in long term if we decide that to commit block it's not enough to just receive enough signatures.
And proxy tail can add it's signature on BlockCreated
message.
if let Err(err) = Self::verify_proxy_tail_signature(self.as_ref(), topology) { | ||
return WithEvents::new(Err((self, err.into()))); | ||
} |
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 probably should check that whole set of sigantures is valid.
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.
ValidBlock
is guaranteed to have all signatures valid except ProxyTail
1479b74
to
4e1cf76
Compare
Signed-off-by: Marin Veršić <marin.versic101@gmail.com>
4e1cf76
to
5b5f2cb
Compare
Description
Linked issue
Closes #4393
Benefits
Checklist
CONTRIBUTING.md