Skip to content
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

Merged
merged 8 commits into from
Aug 18, 2022
Merged

Attestations implementation #3791

merged 8 commits into from
Aug 18, 2022

Conversation

nzpr
Copy link
Collaborator

@nzpr nzpr commented Jul 28, 2022

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.

Sorry, something went wrong.

@nzpr nzpr force-pushed the attest branch 2 times, most recently from 78c04a9 to 8b4f482 Compare July 28, 2022 12:06
@nzpr nzpr marked this pull request as ready for review July 28, 2022 15:28
@nzpr nzpr requested review from tgrospic and stanislavlyalin July 28, 2022 15:28
@nzpr nzpr added this to the Block merge (Hard Fork 2) milestone Jul 28, 2022
@nzpr nzpr requested a review from hilltracer July 28, 2022 17:48
): F[BlockCreatorResult] = {
val creatorsPk = id.publicKey
val blockData = BlockData(blockNum, creatorsPk, seqNum)
val shouldPropose = deploys.nonEmpty || toSlash.nonEmpty || changeEpoch
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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
Copy link
Collaborator

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.

Copy link
Collaborator Author

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?

Copy link
Collaborator

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)
Copy link
Collaborator

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)
Copy link
Collaborator

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.

Copy link
Collaborator Author

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?

Copy link
Collaborator

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

@nzpr nzpr force-pushed the attest branch 2 times, most recently from 5752e82 to bdda594 Compare August 2, 2022 17:38

def createBlock(validatorIdentity: ValidatorIdentity): F[BlockCreatorResult] =
def createBlock(
Copy link
Collaborator

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
Copy link
Collaborator

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.

@nzpr nzpr force-pushed the attest branch 4 times, most recently from b50e06d to be81bfd Compare August 8, 2022 09:05
Copy link
Collaborator

@tgrospic tgrospic left a 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
Copy link
Collaborator

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
Copy link
Collaborator

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.

@tgrospic
Copy link
Collaborator

tgrospic commented Aug 9, 2022

bors try

bors bot added a commit that referenced this pull request Aug 9, 2022
@bors
Copy link
Contributor

bors bot commented Aug 9, 2022

try

Build failed:

@tgrospic tgrospic removed their assignment Aug 11, 2022
Copy link
Collaborator

@stanislavlyalin stanislavlyalin left a 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.

Comment on lines +172 to +188
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
Copy link
Collaborator

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.

Comment on lines +184 to +197
// // 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)
Copy link
Collaborator

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
Copy link
Collaborator

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)
Copy link
Collaborator

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)
Copy link
Collaborator

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.

Comment on lines +221 to +222
OptionT
.fromOption(pooledOk.nonEmpty.guard[Option].as(pooledOk))
Copy link
Collaborator

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?

Copy link
Collaborator

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toPune - looks like typo.

Copy link
Collaborator

@stanislavlyalin stanislavlyalin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@nzpr
Copy link
Collaborator Author

nzpr commented Aug 18, 2022

bors merge

@bors
Copy link
Contributor

bors bot commented Aug 18, 2022

Build succeeded:

@bors bors bot merged commit 641ad00 into rchain:dev Aug 18, 2022
@stanislavlyalin stanislavlyalin mentioned this pull request Aug 19, 2022
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enable confirmation/attestation blocks
3 participants