-
Notifications
You must be signed in to change notification settings - Fork 46
feat(node): verifying cometbft precommit signatures #1402
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: main
Are you sure you want to change the base?
Conversation
Co-authored-by: cryptoAtwill <[email protected]>
Co-authored-by: cryptoAtwill <[email protected]>
Co-authored-by: cryptoAtwill <[email protected]>
return s.networkName; | ||
} | ||
|
||
/// @notice Returns a specific bottom-up checkpoint based on an epoch number. |
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.
Is there any reason to keep all code related to the checkpoints quorum?
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 missed them in deleting code, will delete one more round.
/// @param checkpoint The executed bottom-up checkpoint. | ||
/// @param signatories The addresses of validators signing the checkpoint. | ||
/// @param signatures The signatures of validators on the checkpoint. | ||
function submitCheckpoint( |
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.
Can you explain why this was removed?
Conceptually we should still have the notion of checkpoint submission to the parent, as it's a larger process than merely submitting the signing data.
Any reason why to decouple the activity rollup & bottom-up messages commitment from that?
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 the second question, it's mainly for gas considerations. The verification of signed header and signatures consumes a lot of gas. Adding more processing to existing process will likely take longer to be mined.
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.
In that question I was referring to the commitment (hash) of the activity rollup & the bottom-up messages.
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 split them into separate functions as it provides max flexibility. If there is no activity required for a subnet, then activity rollup should never be used. If someone wants to merge them in one transaction, they can simply use multi-call pattern.
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 i'm convinced.. if there was no activity, then it can be marked with a zero-value commitment, or something similar. Otherwise, the state of the checkpoint in that regard is unknown.
In addition, I don't think the current implementation supports multiple calls to execBottomUpMsgBatch
for the same checkpoint.
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 understand where you are coming from, but I still think there are more flexibility if we split the calls and putting them in one call does not add much more.
I dont consider the state of the checkpoint in that regard is unknown.
to be true, because the checkpoint is the signed header submission, which is already handled. These are just executing side effects, merging them in one method does not change the face that the checkpoint is committed. And if no one cares about the activity, then no one needs to do anything about it. Putting them together forces these two to be handled simultaneously.
Eventually we will move to supporting single bottom up message batch execution, then we will have to break the methods into two.
// validate signatures and quorum threshold, revert if validation fails | ||
validateActiveQuorumSignatures({signatories: signatories, hash: checkpointHash, signatures: signatures}); | ||
// TODO: The parameter should be SignedHeader.Data, but ethers-rust failed to generate the bindings | ||
function submitSignedHeader(bytes calldata rawData) external whenNotPaused { |
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 a cometbft
-specific implementation. We need to introduce polymorphism, where the variant of the verifying client is declared for the given subnet.
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 thinking the polymorphism should be from the Facet. I think that might be a better way to keep things simpler. Currently we dont have other checkpoint mechanism, I dont really see a point of polymorphism at the moment to create something that does not exist.
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 IPC roadmap is to allow configurable consensus mechanisms, so we should cater for it, even if it doesn't exist yet.
Making the Facet as the configurable component is fine, but it currently carries other, non-related functionality.
I suggest to see the checkpoint mechanism as fixed, decoupled from a configurable verifying light client component.
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.
Updated, the naming has been reverted.
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 wasn't referring to the naming here.
} | ||
|
||
function ensureValidatorSubmission(uint256 i, bytes memory incomingValidator) internal view returns (uint256 power, address validator) { | ||
validator = LibPower.getActiveValidatorAddressByIndex(uint16(i)); |
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.
Can you explain how the signature index correlates with the validator priority position?
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 will add doc to this. TLDR, they must match.
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 added a comment that it should match, which is fine for the contract side. But you didn't explain what exactly is making sure it is matching, given that the list is fetched from CometBFT, without being re-ordered prior to calling submitBottomUpCheckpoint
.
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.
Simply put, the validator orders are checked before SignedHeader submission, then the validator signatures are sorted against that order, see
ipc/ipc/provider/src/checkpoint.rs
Line 204 in c1b5842
header.order_commit_against(pubkeys)?; |
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, I now see the code in the off-chain relayer. Please add some docs there explaining why this ordering is taking place.
string memory _chainID, | ||
uint256 idx | ||
) internal pure returns (bytes memory) { | ||
CanonicalVote.Data memory vote = CanonicalVote.Data({ |
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.
What prevents the signed header to contain an arbitrary app_hash
?
It doesn't seems to be used here.
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 cannot be arbitrary app_hash because checkCommitHash
checks it before reaching this step
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's not clear to me what exactly checkCommitHash
is checking, and how that relates specifically to app_hash
. Please provide more docs.
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.
updated!
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 still not clear what exactly checkCommitHash
is checking.
- What
header.hash()
is including? - You wrote "This also makes sure the AppHash is not fabricated" without explaining how it's done.
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.
function hash(SignedHeader.Data memory h) internal pure returns (bytes32) {
require(h.header.validators_hash.length > 0, "Tendermint: hash can't be empty");
bytes memory hbz = Consensus.encode(h.header.version);
bytes memory pbt = Timestamp.encode(h.header.time);
bytes memory bzbi = BlockID.encode(h.header.last_block_id);
bytes[14] memory all = [
hbz,
Encoder.cdcEncode(h.header.chain_id),
Encoder.cdcEncode(h.header.height),
pbt,
bzbi,
Encoder.cdcEncode(h.header.last_commit_hash),
Encoder.cdcEncode(h.header.data_hash),
Encoder.cdcEncode(h.header.validators_hash),
Encoder.cdcEncode(h.header.next_validators_hash),
Encoder.cdcEncode(h.header.consensus_hash),
Encoder.cdcEncode(h.header.app_hash),
Encoder.cdcEncode(h.header.last_results_hash),
Encoder.cdcEncode(h.header.evidence_hash),
Encoder.cdcEncode(h.header.proposer_address)
];
return MerkleTree.merkleRootHash(all, 0, all.length);
}
This is what the hash
function does. You can see the app_hash
is part of the checking mechanism.
…nsus-shipyard/ipc into cometbft-precommit-signatures
} | ||
|
||
function ensureValidatorSubmission(uint256 i, bytes memory incomingValidator) internal view returns (uint256 power, address validator) { | ||
validator = LibPower.getActiveValidatorAddressByIndex(uint16(i)); |
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, I now see the code in the off-chain relayer. Please add some docs there explaining why this ordering is taking place.
validator = LibPower.getActiveValidatorAddressByIndex(uint16(i)); | ||
ValidatorInfo memory info = LibPower.getActiveValidatorInfo(validator); | ||
|
||
bytes20 expectedCometbftAccountId = toCometBFTAddress(info.metadata); |
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's not clear how info.metadata
contains the public key. It is documented as data for off-chain retrieval.
bytes memory message = generateSignePayload(header.commit, LibSubnetActorStorage.appStorage().chainID, i); | ||
bytes32 messageHash = sha256(message); | ||
|
||
ensureValidSignature(messageHash, commitSig.signature, validator); |
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.
Signature is checked here with the onchain address validator
.
If it's actually passing, then CometBFT is using that key for signing.
If so, why ensureValidatorSubmission
needs to check CometBFT address? If it doesn't match, the signature check will fail.
In that case, i'm not sure if/why commitSig.validator_address
needs to be submitted.
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 removing them should work as well because public key has to match the validator. I will remove this.
commitSig = header.commit.signatures[i]; | ||
// no need to verify absent or nil votes. | ||
if (commitSig.block_id_flag != TENDERMINTLIGHT_PROTO_GLOBAL_ENUMS.BlockIDFlag.BLOCK_ID_FLAG_COMMIT) { | ||
continue; |
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.
Any reason why not to filter these off-chain?
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 even if offchain is filtered, contract should check it just ensure block id flag is correct. Sanity check, no need to revert, just ignore.
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 it's unsigned data, I don't see why it's need to be submitted. (both as a field, and as items who contain the wrong field value).
And on the same note, I don't think we should follow CometBFT data types when not needed.
// confirming the changes in membership in the child | ||
LibPower.confirmChange(checkpoint.nextConfigurationNumber); | ||
checkpointStorage.stateCommitments[height] = StateCommitment({ | ||
blockHeight: height, |
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.
Why blockHeight
is needed in this struct? it's already used as the key.
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.
updated
assembly { | ||
mstore(add(appHash, 32), derived) | ||
} | ||
return appHash; |
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're trying to produce bytes
instead of bytes32
to comply with having the commitment being bytes
(taken from header_app_hash
), which I don't think is needed.
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.
True, we can simplify the payload. Because behind the scene it's using Tendermint library to do the verification, I would prefer to use the library as it is for now. Unless there is strong gas concerns.
SubnetActorCheckpointingStorage storage checkpointStorage = LibCheckpointingStorage.getStorage(); | ||
|
||
uint256 threshold = (activeCollateral * s.majorityPercentage) / 100; | ||
validateAppHash(checkpointHeight, breakdown); |
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.
What prevents an arbitrary activity data, unrelated to the commitment breakdown?
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.
good catch, I missed this. The keccack should be equal. Updated.
|
||
uint256 threshold = (activeCollateral * s.majorityPercentage) / 100; | ||
validateAppHash(checkpointHeight, breakdown); | ||
ensureValidHeight(checkpointHeight, checkpointStorage.commitmentHeights.activity); |
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 seems here that this will need to be called for every checkpoint, otherwise will fail, which doesn't follow what you wrote in #1402 (comment).
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 mean if there is activity rollup, then we have to enforce it every checkpoint. If there is no activity rollup, then no one needs to call this method.
validateAppHash(checkpointHeight, breakdown); | ||
|
||
LibBottomUpBatch.recordBottomUpBatchCommitment(checkpointHeight, breakdown.msgBatchCommitment); | ||
_execBottomUpMsgBatch(checkpointHeight, inclusions); |
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 won’t allow multi-phase execution, i.e., execution without the full set of inclusion proofs.
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.
Is multi-phase execution possible before this 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.
Yes
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, in the relayer, it's packing all the BottomUpBatch.Inclusion[]
and submit. In the contract, it supports submission one by one. To replicate that, we can just add two more methods:
function recordBottomUpMsgBatch(
uint64 checkpointHeight,
StateCommitmentBreakDown calldata breakdown
) external whenNotPaused {
validateAppHash(checkpointHeight, breakdown);
LibBottomUpBatch.recordBottomUpBatchCommitment(checkpointHeight, breakdown.msgBatchCommitment);
}
function execBottomUpMsgBatch(
uint64 checkpointHeight,
BottomUpBatch.Inclusion[] calldata inclusions
) external whenNotPaused {
_execBottomUpMsgBatch(checkpointHeight, inclusions);
}
This PR adds light client commitment to fendermint app state.
At the end of each block , the old checkpoint process is replaced with generating light client commitments instead of old bottom up checkpoints. When commit from abci is called, it will collect the light client commitments (if any) to derive a app hash that includes the old state param with new light client commitments.
This new app hash will be forwards to solidity light client contract to validate the cometbft precommit signatures quorum cert.
Then replayer will then collect all the commitments to submit the cometbft header including the app hash.
Key changes:
Contracts:
SubnetActorCheckpointFacet
contract to validate cometbft signed headerSubnetActorCheckpointFacet
and side effects execution.BottomUpCheckpoint
and updated testsFendermint:
Relayer:
Current submission flow:
ipc/fendermint/vm/interpreter/src/fvm/interpreter.rs
Line 402 in 0e2a137
ipc/ipc/provider/src/checkpoint.rs
Line 131 in 0e2a137
ipc/ipc/provider/src/manager/cometbft/mod.rs
Line 21 in 0e2a137
ipc/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol
Line 32 in 0e2a137
ipc/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol
Line 49 in 0e2a137
ipc/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol
Line 61 in 0e2a137
ipc/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol
Line 77 in 0e2a137
Note
Replaces bottom-up checkpoints with CometBFT light client commitments across contracts, node, and relayer, updating app hash derivation and submission/exec flows.
SubnetActorCheckpointingFacet
to validate CometBFT signed headers and execute side effects; emitStateCommitmentCreated
and support batch exec via commitments.BottomUpCheckpoint
/quorum logic and related getters; update tests and selector gen.tendermint-sol
), addchainID
to Subnet params.LightClientCommitments
; include in snapshot and app hash (new derive path) and state payloads.EndBlockManager
and commitment plumbing.SignedHeader
(new RPC & types), submit headers to subnet, confirm validator changes, and execute bottom-up batches using commitments.eth_getCommitSignedHeader
; various ABI/type conversions updated; remove unused checkpoint APIs.chain-id
tosubnet create
; remove old checkpoint commands; make genesischain_id
mandatory and persisted.tendermint-sol
submodule and deps; update storage-layouts accordingly.Written by Cursor Bugbot for commit e6900d0. This will update automatically on new commits. Configure here.