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

[BFT] ReceiptValidator ensures k receipts committing to the execution result #5050

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion engine/testutil/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,13 @@ func ConsensusNode(t *testing.T, hub *stub.Hub, identity *flow.Identity, identit
assigner, err := chunks.NewChunkAssigner(flow.DefaultChunkAssignmentAlpha, node.State)
require.Nil(t, err)

receiptValidator := validation.NewReceiptValidator(node.State, node.Headers, node.Index, resultsDB, node.Seals)
receiptValidator := validation.NewReceiptValidator(
node.State,
node.Headers,
node.Index,
resultsDB,
node.Seals,
)

sealingEngine, err := sealing.NewEngine(
node.Log,
Expand Down
23 changes: 18 additions & 5 deletions module/validation/receipt_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/onflow/flow-go/crypto/hash"
"github.com/onflow/flow-go/engine"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/module"
"github.com/onflow/flow-go/module/signature"
"github.com/onflow/flow-go/state"
"github.com/onflow/flow-go/state/fork"
Expand All @@ -15,7 +16,7 @@ import (
)

// receiptValidator holds all needed context for checking
// receipt validity against current protocol state.
// receipt validity against the current protocol state.
type receiptValidator struct {
headers storage.Headers
seals storage.Seals
Expand All @@ -25,12 +26,14 @@ type receiptValidator struct {
signatureHasher hash.Hasher
}

var _ module.ReceiptValidator = (*receiptValidator)(nil)

func NewReceiptValidator(state protocol.State,
headers storage.Headers,
index storage.Index,
results storage.ExecutionResults,
seals storage.Seals,
) *receiptValidator {
) module.ReceiptValidator {
rv := &receiptValidator{
state: state,
headers: headers,
Expand All @@ -39,7 +42,6 @@ func NewReceiptValidator(state protocol.State,
signatureHasher: signature.NewBLSHasher(signature.ExecutionReceiptTag),
seals: seals,
}

return rv
}

Expand Down Expand Up @@ -272,12 +274,23 @@ func (v *receiptValidator) ValidatePayload(candidate *flow.Block) error {
return fmt.Errorf("internal error while traversing the ancestor fork of unsealed blocks: %w", err)
}

// tracks the number of receipts committing to each result.
// it's ok to only index receipts at this point, because we will perform
// all needed checks after we have validated all results.
receiptsByResult := payload.Receipts.GroupByResultID()

// first validate all results that were included into payload
// if one of results is invalid we fail the whole check because it could be violating
// parent-children relationship
// if one of results is invalid we fail the whole check because it could be violating parent-children relationship
// each execution
Copy link
Member

Choose a reason for hiding this comment

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

In this function ValidatePayload, we are checking the protocol compliance of all the results in the payload. Anything that violates protocol rules makes the entire block invalid. I think arguing at that level is totally fine. I don't think we need to go into the details of parent-children relationship (tbh, I didn't follow that argument about parent-children relationship ... it might have well been me adding or suggesting this comment).

Suggested change
// first validate all results that were included into payload
// if one of results is invalid we fail the whole check because it could be violating
// parent-children relationship
// if one of results is invalid we fail the whole check because it could be violating parent-children relationship
// each execution
// Validate all results that are incorporated into the payload. If one is malformed, the entire block is invalid.

for i, result := range payload.Results {
resultID := result.ID()

// check if there are enough execution receipts included in the payload corresponding to the execution result
durkmurder marked this conversation as resolved.
Show resolved Hide resolved
receiptsForResult := uint(len(receiptsByResult.GetGroup(resultID)))
if receiptsForResult == 0 {
return engine.NewInvalidInputErrorf("no receipts for result %v at index %d", resultID, i)
}

// check for duplicated results
if _, isDuplicate := executionTree[resultID]; isDuplicate {
return engine.NewInvalidInputErrorf("duplicate result %v at index %d", resultID, i)
Expand Down
95 changes: 94 additions & 1 deletion module/validation/receipt_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ func (s *ReceiptValidationSuite) SetupTest() {
s.SetupChain()
s.publicKey = &fmock.PublicKey{}
s.Identities[s.ExeID].StakingPubKey = s.publicKey
s.receiptValidator = NewReceiptValidator(s.State, s.HeadersDB, s.IndexDB, s.ResultsDB, s.SealsDB)
s.receiptValidator = NewReceiptValidator(
s.State,
s.HeadersDB,
s.IndexDB,
s.ResultsDB,
s.SealsDB,
)
}

// TestReceiptValid try submitting valid receipt
Expand Down Expand Up @@ -745,3 +751,90 @@ func (s *ReceiptValidationSuite) TestValidateReceiptAfterBootstrap() {
err := s.receiptValidator.ValidatePayload(candidate)
s.Require().NoError(err)
}

// TestValidateReceiptResultWithoutReceipt tests a case when a malicious leader incorporates a made-up execution result
// into their proposal. ReceiptValidator must ensure that for each result included in the block, there must be
// at least one receipt included in that block as well.
func (s *ReceiptValidationSuite) TestValidateReceiptResultWithoutReceipt() {
// assuming signatures are all good
s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil)

// G <- A <- B
blocks, result0, seal := unittest.ChainFixture(2)
s.SealsIndex[blocks[0].ID()] = seal

receipts := unittest.ReceiptChainFor(blocks, result0)
blockA, blockB := blocks[1], blocks[2]
receiptA, receiptB := receipts[1], receipts[2]

blockA.Payload.Receipts = []*flow.ExecutionReceiptMeta{}
blockB.Payload.Receipts = []*flow.ExecutionReceiptMeta{receiptA.Meta()}
blockB.Payload.Results = []*flow.ExecutionResult{&receiptA.ExecutionResult}
// update block header so that blocks are chained together
unittest.ReconnectBlocksAndReceipts(blocks, receipts)
// assuming all receipts are executed by the correct executor
for _, r := range receipts {
r.ExecutorID = s.ExeID
}

for _, b := range blocks {
s.Extend(b)
}
s.PersistedResults[result0.ID()] = result0

candidate := unittest.BlockWithParentFixture(blockB.Header)
candidate.Payload = &flow.Payload{
Receipts: []*flow.ExecutionReceiptMeta{},
Results: []*flow.ExecutionResult{&receiptB.ExecutionResult},
}

err := s.receiptValidator.ValidatePayload(candidate)
s.Require().Error(err)
s.Require().True(engine.IsInvalidInputError(err))
}

// TestValidateReceiptResultHasEnoughReceipts tests a case where a leader incorporates an execution result
// into their proposal, which has multiple receipts and ReceiptValidator.
func (s *ReceiptValidationSuite) TestValidateReceiptResultHasEnoughReceipts() {
s.receiptValidator = NewReceiptValidator(
s.State,
s.HeadersDB,
s.IndexDB,
s.ResultsDB,
s.SealsDB,
)
// assuming signatures are all good
s.publicKey.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(true, nil)

// G <- A <- B
blocks, result0, seal := unittest.ChainFixture(2)
s.SealsIndex[blocks[0].ID()] = seal

receipts := unittest.ReceiptChainFor(blocks, result0)
blockA, blockB := blocks[1], blocks[2]
receiptA, receiptB := receipts[1], receipts[2]

blockA.Payload.Receipts = []*flow.ExecutionReceiptMeta{}
blockB.Payload.Receipts = []*flow.ExecutionReceiptMeta{receiptA.Meta()}
blockB.Payload.Results = []*flow.ExecutionResult{&receiptA.ExecutionResult}
// update block header so that blocks are chained together
unittest.ReconnectBlocksAndReceipts(blocks, receipts)
// assuming all receipts are executed by the correct executor
for _, r := range receipts {
r.ExecutorID = s.ExeID
}

for _, b := range blocks {
s.Extend(b)
}
s.PersistedResults[result0.ID()] = result0

candidate := unittest.BlockWithParentFixture(blockB.Header)
candidate.Payload = &flow.Payload{
Receipts: []*flow.ExecutionReceiptMeta{receiptB.Meta()},
Results: []*flow.ExecutionResult{&receiptB.ExecutionResult},
}

err := s.receiptValidator.ValidatePayload(candidate)
s.Require().NoError(err)
}
Loading