-
Notifications
You must be signed in to change notification settings - Fork 14
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
feat(challenger): added stacked preconfs in the same slot #239
feat(challenger): added stacked preconfs in the same slot #239
Conversation
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 is very clear and that's great. Left some observations.
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 add a test also for stacked preconfirmations?
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.
done!
@@ -31,7 +31,7 @@ contract BoltChallenger is IBoltChallenger { | |||
// ========= CONSTANTS ========= | |||
|
|||
/// @notice The challenge bond required to open a challenge. | |||
uint256 public constant CHALLENGE_BOND = 1 ether; | |||
uint256 public constant COMMITMENT_BOND = 1 ether; |
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 was this renamed? I think CHALLENGE_BOND
is more clear since it's the bond required to open a challenge
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.
see above comment by Lore: #239 (comment)
challenges[commitmentID] = Challenge({ | ||
id: commitmentID, | ||
challengeIDs.add(challengeID); | ||
challenges[challengeID] = Challenge({ |
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 we clear expired challenges somewhere?
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.
No, they live in the storage forever, as we rely on this as an oracle for understanding if a slashing request is legit.
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.
Just wondering: after a challenge has been resolved can we keep its ID in the challengeIDs
storage but remove it from challenges
? We don't need that data anymore and can also give some gas refunds if I understood correctly (https://www.evm.codes/?fork=cancun#55).
With this, then we can perform this check on _resolve
:
function _resolve(bytes32 challengeID, bytes32 trustedPreviousBlockHash, Proof calldata proof) internal {
if (!challengeIDs.contains(challengeID)) {
revert ChallengeDoesNotExist();
}
// This means the challenge has been resolved and deleted from storage successfully.
if ( challenges[challengeID].id == bytes32(0)) {
revert ChallengeAlreadyResolved();
}
// ...
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.
Let me make a test to see how much gas this would save
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.
Before: BoltChallengerTest:testResolveChallengeFullDefenseSingleTx() (gas: 697302)
After: BoltChallengerTest:testResolveChallengeFullDefenseSingleTx() (gas: 554812)
Savings: 142.490 gas which at ~30 gwei would cost like $10 at current ETH prices.
This will be even more for N constraints. I think overall it makes sense and I'll think of a better solution
to keep track of the basic necessary data and get rid of everything else!
// Build the challenge ID: `keccak( keccak(signed tx 1) || keccak(signed tx 2) || ... || le_bytes(slot) )` | ||
bytes32 challengeID = _computeChallengeID(commitments); |
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 100% sure this works well - TBD
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 leaves an edge case open:
- proposer commits to 10 transactions (same sender, same slot)
- proposer includes only the first 3, the last 7 are broken commitments
- problem: challengeID should be unique for that sender and slot, but the challenge could be opened with any amount of signed commitments as calldata
- we can't guarantee that the first time the challenge is opened it will contain the full list of broken commitments
- it would be currently possible to open the challenge with commitments (1, 2, 3, 4), or (1, 2, 3, 4, 5), etc until (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) and all of them would be considered lost because there is at least a broken commitment in each.
A granular solution would be to keep track of all the tx hashes in storage in a mapping of (bytes32 -> bool) and slashing will be done at most once per tx hash for which the mapping is true. Thinking of possible alternatives.
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've seen that now the signature is computing by concatenating signatures which is good but still leaves this edge case exposed. I remember the latest proposal on it is to create a challenge per commitment breached, but I don't see this logic in the contract otherwise the function _computeChallengeID
would return bytes32[] memory challengeIDs
. Is there any reason we're not doing it?
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.
As discussed privately, this will be addressed in a later iteration. tracked in #248
Overview
This PR adds the logic to handle an important missing piece of the challenger logic: stacked preconfirmations.
A couple useful definitions:
commitmentReceiver
: this is the EOA that requested the preconfirmation. it is the sender of the signed transaction that has been preconfirmed by a proposer.commitmentSigner
: the party that signed the commitment. This is anyone running a Bolt sidecar, aka proposers.The problem
The issue, in short, can be summarized by the following example:
The solution
To address this, we now require the challenge to be opened with an array of committed transactions.
These transactions cannot be random though, they must follow these criteria:
Then, when it comes to verifying their inclusion, we check each transaction's nonce one after the other to validate them, before performing the usual merkle inclusion proof.
Currently the challenge is considered BREACHED if any of the commitments inside the bundle is breached.
A further improvement could keep track of the number of breached commitments easily, but that's out of scope here.
How it works
The main change this PR introduces is a new structure for the
Challenge
andProofs
structs:commitmentReceiver
explained abovetargetSlot
refers to the slot contained in all the commitments of this challengecommittedTxs
contains data about each individual committed transaction, like its nonce and gas limit.Meta