-
Notifications
You must be signed in to change notification settings - Fork 216
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
Attestations implementation #3791
Conversation
78c04a9
to
8b4f482
Compare
): F[BlockCreatorResult] = { | ||
val creatorsPk = id.publicKey | ||
val blockData = BlockData(blockNum, creatorsPk, seqNum) | ||
val shouldPropose = deploys.nonEmpty || toSlash.nonEmpty || changeEpoch |
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 need to split this logic in multiple functions. First to detect conditions and data for the next block and after that the block creation itself.
This is also important to correct handling of invalid blocks (consensus related and replay related) and also for easier testing.
Create function should ALWAYS create a 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.
It looks to me that you want this function to be just "packageBlock".
If we go to deep refactoring, I think that there should not be even create block
function which is special for propose. It should be the same thing for propose and replaying 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.
shouldPropose
can be calculated by the caller so that logic of checking for conditions is not mixed with the main logic of creation blocks.
We can leave it as is in this PR and fix it later.
signedBlock = validatorIdentity.signBlock(unsignedBlock) | ||
_ <- Span[F].mark("block-signed") | ||
postState.map { | ||
case None => BlockCreatorResult.noNewDeploys |
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.
Block creator status should be removed, see comments above.
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.
How do you want to signal about the result of a propose? What should be the result? Are you thinking to store it in Proposer 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.
Checking the condition to propose or not should not be inside of the function doing the main logic. So if conditions are not satisfied propose should not be called.
We can leave it as is in this PR and fix it later.
nextBlockNum = preState.justifications.map(_.blockNum).max + 1 | ||
parentHashes = preState.justifications.map(_.blockHash) | ||
finalBonds = preState.fringeBondsMap | ||
offenders = preState.justifications.filter(_.validationFailed).map(_.sender) |
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 data should be extracted and used in block validation. From parents state expected data should be created and be used as a basis for validation. Block metadata will store expected data and invalid parts like invalid block number will be store separately. This is to make DB consistent even with invalid blocks.
finalBonds = preState.fringeBondsMap | ||
offenders = preState.justifications.filter(_.validationFailed).map(_.sender) | ||
// slashing | ||
preStateBonds <- RuntimeManager[F].computeBonds(preStateHash.toByteString) |
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 cannot use bonds from PoS here, previous code is wrong in doing this. PoS bonds should only be used in epoch change to add new validators to final bonds and to remove rejected from final bonds.
Bonds on final state + bonds from consensus (parents) together must be used to validate and generate new blocks. Bonds from final state defines all active validators and bonds from consensus defines who is rejected in non-finalized blocks until the new bonds is finalized.
For now just use bonds from pre-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.
I'm not sure wdy mean - For now just use bonds from pre-state.
I'm doing exactly that, don't 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.
It's fine for now until finalizer have support for invalid blocks.
Situation that I have in mind is just before epoch change is finalized and fringe bonds map contains old validators and pre-state bonds contains new validators. When old validator is slashed it may not be visible to pre-state bonds, I'm not sure how this will be handled in PoS. Maybe I'm missing something now.
!newStateTransition && attestationStake * 3 < preStateBonds.values.toList.sum * 2 | ||
} | ||
} | ||
suppressAttestation <- nothingToFinalize ||^ waitingForSupermajorityToAttest | ||
// push dummy deploy if 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.
What is the purpose of dummy deploys?
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.
There is a dev
mode in the node when you can specify to put deploy in each block, signed with specific key. So you can create blocks without external orchestration, pretending that there is always deploy in the pool.
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 our standup discussion, we should later move this in a separate debugging code so it's not mixed with main block creation logic.
5752e82
to
bdda594
Compare
|
||
def createBlock(validatorIdentity: ValidatorIdentity): F[BlockCreatorResult] = | ||
def createBlock( |
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'd rather have BlockCreator with methods to create all necessary stuff to create blocks then Proposer.
checkActiveValidator: ValidatorIdentity => F[Boolean], | ||
createBlock: ValidatorIdentity => F[BlockCreatorResult], | ||
createBlock: (ParentsMergedState, ValidatorIdentity) => F[BlockCreatorResult], | ||
validateBlock: BlockMessage => F[ValidBlockProcessing], | ||
proposeEffect: BlockMessage => F[Unit], | ||
validator: ValidatorIdentity |
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 to have all these methods accepted separately? Proposer can just work with instance like BlockCreator to initiate block creator execution.
Take a look how easily can be tested code with dependencies with Mockito library.
We now have 3 different things about Proposer which is confusing. For example ProposerInstance
doesn't create Proposer instance, it just accepting it as an argument.
I'd like to see clear hierarchy of dependencies. BlockCreator can have methods with the logic to prepare each step in block creation process and Proposer can be the worker how is responsible to call the final method from BlockCreator in the right moment and/or from the right place.
b50e06d
to
be81bfd
Compare
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
signedBlock = validatorIdentity.signBlock(unsignedBlock) | ||
_ <- Span[F].mark("block-signed") | ||
postState.map { | ||
case None => BlockCreatorResult.noNewDeploys |
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.
Checking the condition to propose or not should not be inside of the function doing the main logic. So if conditions are not satisfied propose should not be called.
We can leave it as is in this PR and fix it later.
): F[BlockCreatorResult] = { | ||
val creatorsPk = id.publicKey | ||
val blockData = BlockData(blockNum, creatorsPk, seqNum) | ||
val shouldPropose = deploys.nonEmpty || toSlash.nonEmpty || changeEpoch |
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.
shouldPropose
can be calculated by the caller so that logic of checking for conditions is not mixed with the main logic of creation blocks.
We can leave it as is in this PR and fix it later.
bors try |
tryBuild 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.
What is Attestations? Without reading PR description and especially issue description is very hard to understand it is just messages. Maybe we should provide it in PR's title or add some information from issue in PR's description. I would like to see some sequential diagram how attestations works and why it is needed. Why we cannot close block without attestations and what is "closing block"?
In general, it would be very good to have description in Wiki how blockchain works in examples and with terms. I think now we have good understanding of LLBM and can document it. I want to do it because want to understand this things. And I can do it from sources. We can create issue for that. @tgrospic, what do you think?
Few last commits looks unrelated to this PR.
preState <- MultiParentCasper.getPreStateForNewBlock[F] | ||
creatorsPk = validatorIdOpt.get.publicKey | ||
creatorsId = ByteString.copyFrom(creatorsPk.bytes) | ||
creatorsLatestOpt = preState.justifications.find(_.sender == creatorsId) | ||
nextSeqNum = creatorsLatestOpt.map(_.seqNum + 1).getOrElse(0L) | ||
nextBlockNum = preState.justifications.map(_.blockNum).max + 1 | ||
createBlockResult <- BlockCreator(validatorIdOpt.get, shardName).create( | ||
preState.preStateHash, | ||
preState.justifications.map(_.blockHash), | ||
preState.fringeBondsMap, | ||
preState.rejectedDeploys, | ||
nextBlockNum, | ||
nextSeqNum, | ||
deployDatums.map(_.sig), | ||
Set(), | ||
false, | ||
true |
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.
Preparation of calling create
looks very similar in all cases. Consider to hide some preparation logic inside create
(e.g. extracting needed parameters from preState
) or introduce additional preparation function.
// // push dummy deploy if needed | ||
// p <- BlockDagStorage[F].pooledDeploys | ||
// _ <- dummyDeployOpt | ||
// .traverse { | ||
// case (privateKey, term) => | ||
// val deployData = ConstructDeploy.sourceDeployNow( | ||
// source = term, | ||
// sec = privateKey, | ||
// vabn = nextBlockNum - 1, | ||
// shardId = shardId | ||
// ) | ||
// BlockDagStorage[F].addDeploy(deployData) | ||
// } | ||
// .whenA(p.isEmpty) |
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 do you leave this commented code? If it is really needed it would be good to add TODO comment.
newBlocks.exists(b => b.state.systemDeploys.nonEmpty || b.state.deploys.nonEmpty) | ||
val attestationStake = | ||
preStateBonds.filterKeys(newBlocks.map(_.sender).toSet).values.toList.sum | ||
!newStateTransition && attestationStake * 3 < preStateBonds.values.toList.sum * 2 |
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 don't like that numbers 2 and 3 (stake threshold) hardcoded here and in other places (e.g. in Finalizer). Better to move it to utility function or to shared constant or to configuration.
val newlySeen = creatorsLatestOpt | ||
.map { prev => | ||
prev.justifications.flatMap(dag.dagMessageState.msgMap(_).seen) -- | ||
parentHashes.flatMap(dag.dagMessageState.msgMap(_).seen) |
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 block of code looks similar to forming conflictSet
. Maybe better to extract
val msgMap = dag.dagMessageState.msgMap
and use in both cases or to extract whole parentHashes.flatMap(dag.dagMessageState.msgMap(_).seen)
.
.getOrElse(Set()) | ||
newlySeen.toList.traverse(BlockStore[F].getUnsafe).map { newBlocks => | ||
val newStateTransition = | ||
newBlocks.exists(b => b.state.systemDeploys.nonEmpty || b.state.deploys.nonEmpty) |
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.
Consider to introduce function hasDeploys(block: BlockMessage)
and used here and in creation of nothingToFinalize
.
OptionT | ||
.fromOption(pooledOk.nonEmpty.guard[Option].as(pooledOk)) |
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.
OptionT.whenF(pooledOk.nonEmpty)(pooledOk.pure)
looks more natural for me. @tgrospic, it such replacement correct?
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, this also makes sense.
Another solution is to not even go to OptionT, but just use Option. In this case it's not necessary to mention pooledOk
twice.
Option(pooledOk).filter(_.nonEmpty)
): F[Unit] = { | ||
val newLPF = dbPruneFringe(newState, childMap) | ||
val curLPF = dbPruneFringe(curState, childMap) | ||
val toPune = (newState.msgMap.between(newLPF, curLPF) ++ curLPF).map(_.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.
toPune
- looks like typo.
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.
👍
bors merge |
Build succeeded: |
Overview
This PR adds implementation for attestation messages, which are the same block messages, but with pre state == post state and empty list of deploys.
The logic of block creation remains the same - there is an attempt to create block after each incoming block replayed.
But when there are no deploys to put in a block (no user deploys, no system deploys) - attestation is created.
I'm not sure yet how to enable testing for this. Thinking about more refactoring.
Closes #3249
Notes
Please make sure that this PR:
Bors cheat-sheet:
bors r+
runs integration tests and merges the PR (if it's approved),bors try
runs integration tests for the PR,bors delegate+
enables non-maintainer PR authors to run the above.