prysm-pulse/beacon-chain/slasher/process_slashings_test.go
Manu NALEPA af203efa0c
Slasher: Refactor and add tests (#13589)
* `helpers.go`: Improve naming consistency.

* `detect_attestations.go`: Improve readability.

* `receive.go`: Add `attsQueueSize` in log message.

* `checkSlashableAttestations`: Improve logging.

`avgBatchProcessingTime` is not displayed any more if not batch is
processed.

* `loadChunks`: Use explicit `chunkKind` and `chunkIndices`.

* `getChunk`: Use specific `chunkIndex` and `chunkKind`.

* `validatorIndicesInChunk` -> `validatorIndexesInChunk`.

* `epochUpdateForValidator`: Use explicit arguments.

* `getChunk`: Change order of arguments.

* `latestEpochWrittenForValidator`: Use `ok` parameter.

So the default value is not any more considered as the absence of
value.

* `applyAttestationForValidator`: Use explicit arguments.

* `updateSpans`: Use explicit arguments.

* `saveUpdatedChunks`: Use explicit arguments.

* `checkSurrounds`: Use explicit arguments.

We see here that, previously, in `checkSlashableAttestations`,
`checkSurrounds` was called with the default value of `slashertypes`: `MinSpan`.

Now, we set it expliciterly at `MinSpan`, which may explicit a bug.

* `epochUpdateForValidator`: Set modified by the function argument first.

* `applyAttestationForValidator`: Set mutated argument `chunksByChunkIdx`first.

* `applyAttestationForValidator`: Rename variables.

* `Test_processQueuedAttestations`: Fix test.

Two tests were actually exactly the same.

* `updateSpans`: Keep happy path in the outer scope.

Even if in this case the "happy" path means slashing.

* `checkSurrounds`: Rename variable.

* `getChunk`: Avoid side effects.

It adds a few lines for callers, but it does not modify any more
arguments and it does what it says: getting a chunk.

* `CheckSlashable`: Flatten.

* `detect_attestations_test.go`: Simplify.

* `CheckSlashable`: Add error log in case of missing attestation.

* `processQueuedAttestations`: Extract a sub function.

So testing will be easier.

* `processAttesterSlashings` and `processProposerSlashings`: Improve.

* `processAttesterSlashings`: Return processed slashings.

* `createAttestationWrapper`: Rename variables.

* `signingRoot` ==> `headerRoot` or `dataRoot`.

Before this commit, there is two typse of `signing root`s floating around.
- The first one is a real signing root, aka a hash tree root computed from an object root and
a domain. This real signing root is the object ready to be signed.
- The second one is a "false" signing root, which is actually just the hash tree root of an object. This object is either the `Data` field of an attestation, or the `Header` field of a block.

Having 2 differents objects with the same name `signing root` is quite confusing.
This commit renames wrongly named `signing root` objects.

* `createAttestationWrapper` => `createAttestationWrapperEmptySig`.

So it's clear for the user that the created attestation wrapper has an empty signature.

* Implement `createAttestationWrapper`.

* `processAttestations`: Return processed attester slashings.

* Test `processAttestations` instead of `processQueuedAttestations`.

By testing `processAttestations` instead of `processQueuedAttestations`, we get rid of a lot of tests fixtures, including the 200 ms sleep.

The whole testing duration is shorter.

* `Test_processAttestations`: Allow multiple steps.

* `Test_processAttestations`: Add double steps tests.

Some new failing tests are commented with a corresponding github issue.

* `NextChunkStartEpoch`: Fix function comment.

Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com>

* `chunks.go`: Avoid templating log messages.

* `checkSlashableAttestations`: Simplify duration computation.

---------

Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com>
2024-02-09 21:02:18 +00:00

254 lines
7.4 KiB
Go

package slasher
import (
"context"
"testing"
mock "github.com/prysmaticlabs/prysm/v4/beacon-chain/blockchain/testing"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/signing"
dbtest "github.com/prysmaticlabs/prysm/v4/beacon-chain/db/testing"
doublylinkedtree "github.com/prysmaticlabs/prysm/v4/beacon-chain/forkchoice/doubly-linked-tree"
slashingsmock "github.com/prysmaticlabs/prysm/v4/beacon-chain/operations/slashings/mock"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/state/stategen"
"github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/crypto/bls"
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/testing/require"
"github.com/prysmaticlabs/prysm/v4/testing/util"
logTest "github.com/sirupsen/logrus/hooks/test"
)
func TestService_processAttesterSlashings(t *testing.T) {
ctx := context.Background()
slasherDB := dbtest.SetupSlasherDB(t)
beaconDB := dbtest.SetupDB(t)
beaconState, err := util.NewBeaconState()
require.NoError(t, err)
privKey, err := bls.RandKey()
require.NoError(t, err)
validators := make([]*ethpb.Validator, 1)
validators[0] = &ethpb.Validator{
PublicKey: privKey.PublicKey().Marshal(),
WithdrawalCredentials: make([]byte, 32),
EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance,
}
err = beaconState.SetValidators(validators)
require.NoError(t, err)
mockChain := &mock.ChainService{
State: beaconState,
}
s := &Service{
serviceCfg: &ServiceConfig{
Database: slasherDB,
AttestationStateFetcher: mockChain,
StateGen: stategen.New(beaconDB, doublylinkedtree.New()),
SlashingPoolInserter: &slashingsmock.PoolMock{},
HeadStateFetcher: mockChain,
},
}
firstAtt := util.HydrateIndexedAttestation(&ethpb.IndexedAttestation{
AttestingIndices: []uint64{0},
})
secondAtt := util.HydrateIndexedAttestation(&ethpb.IndexedAttestation{
AttestingIndices: []uint64{0},
})
domain, err := signing.Domain(
beaconState.Fork(),
0,
params.BeaconConfig().DomainBeaconAttester,
beaconState.GenesisValidatorsRoot(),
)
require.NoError(t, err)
signingRoot, err := signing.ComputeSigningRoot(firstAtt.Data, domain)
require.NoError(t, err)
t.Run("first_att_valid_sig_second_invalid", func(tt *testing.T) {
hook := logTest.NewGlobal()
// Use valid signature for the first att, but bad one for the second.
signature := privKey.Sign(signingRoot[:])
firstAtt.Signature = signature.Marshal()
secondAtt.Signature = make([]byte, 96)
slashings := []*ethpb.AttesterSlashing{
{
Attestation_1: firstAtt,
Attestation_2: secondAtt,
},
}
_, err = s.processAttesterSlashings(ctx, slashings)
require.NoError(tt, err)
require.LogsContain(tt, hook, "Invalid signature")
})
t.Run("first_att_invalid_sig_second_valid", func(tt *testing.T) {
hook := logTest.NewGlobal()
// Use invalid signature for the first att, but valid for the second.
signature := privKey.Sign(signingRoot[:])
firstAtt.Signature = make([]byte, 96)
secondAtt.Signature = signature.Marshal()
slashings := []*ethpb.AttesterSlashing{
{
Attestation_1: firstAtt,
Attestation_2: secondAtt,
},
}
_, err = s.processAttesterSlashings(ctx, slashings)
require.NoError(tt, err)
require.LogsContain(tt, hook, "Invalid signature")
})
t.Run("both_valid_att_signatures", func(tt *testing.T) {
hook := logTest.NewGlobal()
// Use valid signatures.
signature := privKey.Sign(signingRoot[:])
firstAtt.Signature = signature.Marshal()
secondAtt.Signature = signature.Marshal()
slashings := []*ethpb.AttesterSlashing{
{
Attestation_1: firstAtt,
Attestation_2: secondAtt,
},
}
_, err = s.processAttesterSlashings(ctx, slashings)
require.NoError(tt, err)
require.LogsDoNotContain(tt, hook, "Invalid signature")
})
}
func TestService_processProposerSlashings(t *testing.T) {
ctx := context.Background()
slasherDB := dbtest.SetupSlasherDB(t)
beaconDB := dbtest.SetupDB(t)
beaconState, err := util.NewBeaconState()
require.NoError(t, err)
privKey, err := bls.RandKey()
require.NoError(t, err)
validators := make([]*ethpb.Validator, 1)
validators[0] = &ethpb.Validator{
PublicKey: privKey.PublicKey().Marshal(),
WithdrawalCredentials: make([]byte, 32),
EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance,
}
err = beaconState.SetValidators(validators)
require.NoError(t, err)
mockChain := &mock.ChainService{
State: beaconState,
}
s := &Service{
serviceCfg: &ServiceConfig{
Database: slasherDB,
AttestationStateFetcher: mockChain,
StateGen: stategen.New(beaconDB, doublylinkedtree.New()),
SlashingPoolInserter: &slashingsmock.PoolMock{},
HeadStateFetcher: mockChain,
},
}
parentRoot := bytesutil.ToBytes32([]byte("parent"))
err = s.serviceCfg.StateGen.SaveState(ctx, parentRoot, beaconState)
require.NoError(t, err)
firstBlockHeader := util.HydrateSignedBeaconHeader(&ethpb.SignedBeaconBlockHeader{
Header: &ethpb.BeaconBlockHeader{
Slot: 0,
ProposerIndex: 0,
ParentRoot: parentRoot[:],
},
})
secondBlockHeader := util.HydrateSignedBeaconHeader(&ethpb.SignedBeaconBlockHeader{
Header: &ethpb.BeaconBlockHeader{
Slot: 0,
ProposerIndex: 0,
ParentRoot: parentRoot[:],
},
})
domain, err := signing.Domain(
beaconState.Fork(),
0,
params.BeaconConfig().DomainBeaconProposer,
beaconState.GenesisValidatorsRoot(),
)
require.NoError(t, err)
htr, err := firstBlockHeader.Header.HashTreeRoot()
require.NoError(t, err)
container := &ethpb.SigningData{
ObjectRoot: htr[:],
Domain: domain,
}
require.NoError(t, err)
signingRoot, err := container.HashTreeRoot()
require.NoError(t, err)
t.Run("first_header_valid_sig_second_invalid", func(tt *testing.T) {
hook := logTest.NewGlobal()
// Use valid signature for the first header, but bad one for the second.
signature := privKey.Sign(signingRoot[:])
firstBlockHeader.Signature = signature.Marshal()
secondBlockHeader.Signature = make([]byte, 96)
slashings := []*ethpb.ProposerSlashing{
{
Header_1: firstBlockHeader,
Header_2: secondBlockHeader,
},
}
err = s.processProposerSlashings(ctx, slashings)
require.NoError(tt, err)
require.LogsContain(tt, hook, "Invalid signature")
})
t.Run("first_header_invalid_sig_second_valid", func(tt *testing.T) {
hook := logTest.NewGlobal()
// Use invalid signature for the first header, but valid for the second.
signature := privKey.Sign(signingRoot[:])
firstBlockHeader.Signature = make([]byte, 96)
secondBlockHeader.Signature = signature.Marshal()
slashings := []*ethpb.ProposerSlashing{
{
Header_1: firstBlockHeader,
Header_2: secondBlockHeader,
},
}
err = s.processProposerSlashings(ctx, slashings)
require.NoError(tt, err)
require.LogsContain(tt, hook, "Invalid signature")
})
t.Run("both_valid_header_signatures", func(tt *testing.T) {
hook := logTest.NewGlobal()
// Use valid signatures.
signature := privKey.Sign(signingRoot[:])
firstBlockHeader.Signature = signature.Marshal()
secondBlockHeader.Signature = signature.Marshal()
slashings := []*ethpb.ProposerSlashing{
{
Header_1: firstBlockHeader,
Header_2: secondBlockHeader,
},
}
err = s.processProposerSlashings(ctx, slashings)
require.NoError(tt, err)
require.LogsDoNotContain(tt, hook, "Invalid signature")
})
}