Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
42 changes: 42 additions & 0 deletions core/beacon_slash_validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package core

import (
"errors"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/crypto/hash"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/staking/slash"
)

var errInvalidBeaconSlashPayload = errors.New("invalid beacon slash payload in header")

// checkBeaconSlashEvidenceUniqueness enforces canonical uniqueness for decoded
// beacon slash records when the chain schedule enables the rule. Caller runs this
// from StateProcessor.Process before the processor result cache and before
// transaction execution so all execution paths apply the same checks.
func checkBeaconSlashEvidenceUniqueness(cfg *params.ChainConfig, epoch *big.Int, records slash.Records) error {
if cfg == nil || !cfg.IsRejectDuplicateSlashEvidence(epoch) || len(records) < 2 {
return nil
}
seen := make(map[common.Hash]struct{}, len(records)*2)
for i := range records {
ev := &records[i].Evidence
h := hash.FromRLPNew256(ev)
if _, ok := seen[h]; ok {
return errInvalidBeaconSlashPayload
}
swapEv := *ev
tmp := swapEv.ConflictingVotes.FirstVote
swapEv.ConflictingVotes.FirstVote = swapEv.ConflictingVotes.SecondVote
swapEv.ConflictingVotes.SecondVote = tmp
sh := hash.FromRLPNew256(&swapEv)
if _, ok := seen[sh]; ok {
return errInvalidBeaconSlashPayload
}
seen[h] = struct{}{}
seen[sh] = struct{}{}
}
return nil
}
97 changes: 97 additions & 0 deletions core/beacon_slash_validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package core

import (
"math/big"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/crypto/hash"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/shard"
"github.com/harmony-one/harmony/staking/slash"
"github.com/stretchr/testify/require"
)

// reporterVariantSlashRecords returns two slash records that share identical
// signed evidence but use different reporter addresses (bug07 reporter-variant clone).
func reporterVariantSlashRecords(t *testing.T) (slash.Records, common.Hash) {
t.Helper()

evidence := slash.Evidence{
Moment: slash.Moment{
Epoch: big.NewInt(4),
ShardID: shard.BeaconChainShardID,
Height: 37,
ViewID: 38,
},
ConflictingVotes: slash.ConflictingVotes{
FirstVote: slash.Vote{
BlockHeaderHash: common.HexToHash("0x01"),
},
SecondVote: slash.Vote{
BlockHeaderHash: common.HexToHash("0x02"),
},
},
Offender: common.HexToAddress("0x0000000000000000000000000000000000000b22"),
}
evidenceHash := hash.FromRLPNew256(evidence)

return slash.Records{
{Evidence: evidence, Reporter: common.HexToAddress("0x0000000000000000000000000000000000000c33")},
{Evidence: evidence, Reporter: common.HexToAddress("0x0000000000000000000000000000000000000d44")},
}, evidenceHash
}

func TestCheckBeaconSlashEvidenceUniqueness_RejectsReporterVariants(t *testing.T) {
records, _ := reporterVariantSlashRecords(t)
cfg := params.TestChainConfig

err := checkBeaconSlashEvidenceUniqueness(cfg, big.NewInt(0), records)
require.ErrorIs(t, err, errInvalidBeaconSlashPayload)
}

func TestCheckBeaconSlashEvidenceUniqueness_SkippedBeforeFork(t *testing.T) {
records, _ := reporterVariantSlashRecords(t)
cfg := params.MainnetChainConfig

err := checkBeaconSlashEvidenceUniqueness(cfg, big.NewInt(1_000_000), records)
require.NoError(t, err)
}

func TestCheckBeaconSlashEvidenceUniqueness_AllowsSingleRecord(t *testing.T) {
records, _ := reporterVariantSlashRecords(t)
cfg := params.TestChainConfig

err := checkBeaconSlashEvidenceUniqueness(cfg, big.NewInt(0), slash.Records{records[0]})
require.NoError(t, err)
}

func TestCheckBeaconSlashEvidenceUniqueness_AllowsDistinctEvidence(t *testing.T) {
base, _ := reporterVariantSlashRecords(t)
other := base[0]
other.Evidence.ConflictingVotes.SecondVote.BlockHeaderHash = common.HexToHash("0x03")

records := slash.Records{base[0], other}
cfg := params.TestChainConfig

err := checkBeaconSlashEvidenceUniqueness(cfg, big.NewInt(0), records)
require.NoError(t, err)
}

func TestCheckBeaconSlashEvidenceUniqueness_RejectsSwappedVoteEvidence(t *testing.T) {
base, evidenceHash := reporterVariantSlashRecords(t)
swapped := base[0].Evidence
tmp := swapped.ConflictingVotes.FirstVote
swapped.ConflictingVotes.FirstVote = swapped.ConflictingVotes.SecondVote
swapped.ConflictingVotes.SecondVote = tmp

records := slash.Records{
base[0],
{Evidence: swapped, Reporter: common.HexToAddress("0x0000000000000000000000000000000000000e55")},
}
cfg := params.TestChainConfig

err := checkBeaconSlashEvidenceUniqueness(cfg, big.NewInt(0), records)
require.ErrorIs(t, err, errInvalidBeaconSlashPayload)
require.Equal(t, evidenceHash, hash.FromRLPNew256(records[0].Evidence))
}
28 changes: 19 additions & 9 deletions core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,25 @@ func (p *StateProcessor) Process(
}
utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("Process Staking Txns")
}

// Decode header slashes and apply scheduled slash-payload rules before the
// processor result cache. When the fork flag is off, extra uniqueness checks are skipped.
var slashes slash.Records
if s := header.Slashes(); len(s) > 0 {
if err := rlp.DecodeBytes(s, &slashes); err != nil {
return nil, nil, nil, nil, 0, nil, statedb, errors.New(
"[Process] Cannot finalize block",
)
}
}
if p.bc.ShardID() == shard.BeaconChainShardID {
if err := checkBeaconSlashEvidenceUniqueness(p.bc.Config(), block.Epoch(), slashes); err != nil {
return nil, nil, nil, nil, 0, nil, statedb, errors.WithMessage(err,
"[Process] invalid beacon slash payload",
)
}
}

// incomingReceipts should always be processed
// after transactions (to be consistent with the block proposal)
for _, cx := range block.IncomingReceipts() {
Expand All @@ -205,15 +224,6 @@ func (p *StateProcessor) Process(
}
}

slashes := slash.Records{}
if s := header.Slashes(); len(s) > 0 {
if err := rlp.DecodeBytes(s, &slashes); err != nil {
return nil, nil, nil, nil, 0, nil, statedb, errors.New(
"[Process] Cannot finalize block",
)
}
}

if err := MayShardReduction(p.bc, statedb, header); err != nil {
return nil, nil, nil, nil, 0, nil, statedb, err
}
Expand Down
19 changes: 19 additions & 0 deletions internal/params/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ var (
EIP6780Epoch: EpochTBD,
PragueEpoch: EpochTBD,
EIP8024Epoch: EpochTBD,
RejectDuplicateSlashEvidenceEpoch: EpochTBD,
}

// TestnetChainConfig contains the chain parameters to run a node on the harmony test network.
Expand Down Expand Up @@ -152,6 +153,7 @@ var (
EIP6780Epoch: EpochTBD,
PragueEpoch: EpochTBD,
EIP8024Epoch: EpochTBD,
RejectDuplicateSlashEvidenceEpoch: EpochTBD,
}
// PangaeaChainConfig contains the chain parameters for the Pangaea network.
// All features except for CrossLink are enabled at launch.
Expand Down Expand Up @@ -211,6 +213,7 @@ var (
EIP3860Epoch: EpochTBD,
PragueEpoch: EpochTBD,
EIP8024Epoch: EpochTBD,
RejectDuplicateSlashEvidenceEpoch: EpochTBD,
}

// PartnerChainConfig contains the chain parameters for the Partner network.
Expand Down Expand Up @@ -272,6 +275,7 @@ var (
TimestampValidationEpoch: big.NewInt(47190),
PragueEpoch: EpochTBD,
EIP8024Epoch: EpochTBD,
RejectDuplicateSlashEvidenceEpoch: EpochTBD,
}

// StressnetChainConfig contains the chain parameters for the Stress test network.
Expand Down Expand Up @@ -332,6 +336,7 @@ var (
EIP3860Epoch: EpochTBD,
PragueEpoch: EpochTBD,
EIP8024Epoch: EpochTBD,
RejectDuplicateSlashEvidenceEpoch: EpochTBD,
}

// LocalnetChainConfig contains the chain parameters to run for local development.
Expand Down Expand Up @@ -391,6 +396,7 @@ var (
EIP3860Epoch: EpochTBD,
PragueEpoch: EpochTBD,
EIP8024Epoch: EpochTBD,
RejectDuplicateSlashEvidenceEpoch: big.NewInt(0),
}

// AllProtocolChanges ...
Expand Down Expand Up @@ -453,6 +459,7 @@ var (
big.NewInt(0), // TimestampValidationEpoch
big.NewInt(0), // PragueEpoch
big.NewInt(0), // EIP8024Epoch
big.NewInt(0), // RejectDuplicateSlashEvidenceEpoch
}

// TestChainConfig ...
Expand Down Expand Up @@ -515,6 +522,7 @@ var (
big.NewInt(0), // TimestampValidationEpoch
big.NewInt(0), // PragueEpoch
big.NewInt(0), // EIP8024Epoch
big.NewInt(0), // RejectDuplicateSlashEvidenceEpoch
}

// TestRules ...
Expand Down Expand Up @@ -726,6 +734,11 @@ type ChainConfig struct {
PragueEpoch *big.Int `json:"prague-epoch,omitempty"`
// EIP8024Epoch is the first epoch to support EIP-8024 (DUPN, SWAPN, EXCHANGE opcodes)
EIP8024Epoch *big.Int `json:"eip8024-epoch,omitempty"`

// RejectDuplicateSlashEvidenceEpoch is the first epoch where beacon header slash
// payloads are validated with stricter canonical uniqueness rules. Until set to a
// concrete epoch on a network, EpochTBD leaves the rule inactive there.
RejectDuplicateSlashEvidenceEpoch *big.Int `json:"reject-duplicate-slash-evidence-epoch,omitempty"`
}

// String implements the fmt.Stringer interface.
Expand Down Expand Up @@ -1012,6 +1025,12 @@ func (c *ChainConfig) IsEIP8024(epoch *big.Int) bool {
return isForked(c.EIP8024Epoch, epoch)
}

// IsRejectDuplicateSlashEvidence returns whether stricter beacon header slash
// payload checks are active for the given epoch.
func (c *ChainConfig) IsRejectDuplicateSlashEvidence(epoch *big.Int) bool {
return isForked(c.RejectDuplicateSlashEvidenceEpoch, epoch)
}

// IsChainIdFix returns whether epoch is either equal to the ChainId Fix fork epoch or greater.
func (c *ChainConfig) IsChainIdFix(epoch *big.Int) bool {
return isForked(c.ChainIdFixEpoch, epoch)
Expand Down
Loading