Skip to content

Commit

Permalink
Use subnet public key diffs after Etna is activated (#3502)
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenButtolph authored Oct 29, 2024
1 parent 9f0ff01 commit f500dee
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 17 deletions.
15 changes: 15 additions & 0 deletions vms/platformvm/state/mock_state.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions vms/platformvm/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ var (
ExpiryReplayProtectionPrefix = []byte("expiryReplayProtection")
SingletonPrefix = []byte("singleton")

EtnaHeightKey = []byte("etna height")
TimestampKey = []byte("timestamp")
FeeStateKey = []byte("fee state")
SoVExcessKey = []byte("sov excess")
Expand Down Expand Up @@ -145,6 +146,9 @@ type State interface {
uptime.State
avax.UTXOReader

// TODO: Remove after Etna is activated
GetEtnaHeight() (uint64, error)

GetLastAccepted() ids.ID
SetLastAccepted(blkID ids.ID)

Expand Down Expand Up @@ -293,6 +297,7 @@ type stateBlk struct {
* '-. singletons
* |-- initializedKey -> nil
* |-- blocksReindexedKey -> nil
* |-- etnaHeightKey -> height
* |-- timestampKey -> timestamp
* |-- feeStateKey -> feeState
* |-- sovExcessKey -> sovExcess
Expand Down Expand Up @@ -1083,6 +1088,10 @@ func (s *state) GetStartTime(nodeID ids.NodeID) (time.Time, error) {
return staker.StartTime, nil
}

func (s *state) GetEtnaHeight() (uint64, error) {
return database.GetUInt64(s.singletonDB, EtnaHeightKey)
}

func (s *state) GetTimestamp() time.Time {
return s.timestamp
}
Expand Down Expand Up @@ -1804,7 +1813,7 @@ func (s *state) write(updateValidators bool, height uint64) error {
s.writeTransformedSubnets(),
s.writeSubnetSupplies(),
s.writeChains(),
s.writeMetadata(),
s.writeMetadata(height),
)
}

Expand Down Expand Up @@ -2516,7 +2525,13 @@ func (s *state) writeChains() error {
return nil
}

func (s *state) writeMetadata() error {
func (s *state) writeMetadata(height uint64) error {
if !s.upgrades.IsEtnaActivated(s.persistedTimestamp) && s.upgrades.IsEtnaActivated(s.timestamp) {
if err := database.PutUInt64(s.singletonDB, EtnaHeightKey, height); err != nil {
return fmt.Errorf("failed to write etna height: %w", err)
}
}

if !s.persistedTimestamp.Equal(s.timestamp) {
if err := database.PutTimestamp(s.singletonDB, TimestampKey, s.timestamp); err != nil {
return fmt.Errorf("failed to write timestamp: %w", err)
Expand Down
68 changes: 68 additions & 0 deletions vms/platformvm/state/state_interface_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package state_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/upgrade/upgradetest"
"github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest"
"github.com/ava-labs/avalanchego/vms/platformvm/state/statetest"
)

func TestState_GetEtnaHeight_Activation(t *testing.T) {
require := require.New(t)

upgrades := upgradetest.GetConfig(upgradetest.Durango)
upgrades.EtnaTime = genesistest.DefaultValidatorStartTime.Add(2 * time.Second)
state := statetest.New(t, statetest.Config{
Upgrades: upgrades,
})

// Etna isn't initially active
_, err := state.GetEtnaHeight()
require.ErrorIs(err, database.ErrNotFound)

// Etna still isn't active after advancing the time
state.SetHeight(1)
state.SetTimestamp(genesistest.DefaultValidatorStartTime.Add(time.Second))
require.NoError(state.Commit())

_, err = state.GetEtnaHeight()
require.ErrorIs(err, database.ErrNotFound)

// Etna was just activated
const expectedEtnaHeight uint64 = 2
state.SetHeight(expectedEtnaHeight)
state.SetTimestamp(genesistest.DefaultValidatorStartTime.Add(2 * time.Second))
require.NoError(state.Commit())

etnaHeight, err := state.GetEtnaHeight()
require.NoError(err)
require.Equal(expectedEtnaHeight, etnaHeight)

// Etna was previously activated
state.SetHeight(3)
state.SetTimestamp(genesistest.DefaultValidatorStartTime.Add(3 * time.Second))
require.NoError(state.Commit())

etnaHeight, err = state.GetEtnaHeight()
require.NoError(err)
require.Equal(expectedEtnaHeight, etnaHeight)
}

func TestState_GetEtnaHeight_InitiallyActive(t *testing.T) {
require := require.New(t)

state := statetest.New(t, statetest.Config{})

// Etna is initially active
etnaHeight, err := state.GetEtnaHeight()
require.NoError(err)
require.Zero(etnaHeight)
}
39 changes: 24 additions & 15 deletions vms/platformvm/validators/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/ava-labs/avalanchego/cache"
"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/snow/validators"
"github.com/ava-labs/avalanchego/utils/constants"
Expand Down Expand Up @@ -49,6 +50,9 @@ type Manager interface {
type State interface {
GetTx(txID ids.ID) (*txs.Tx, status.Status, error)

// TODO: Remove after Etna is activated
GetEtnaHeight() (uint64, error)

GetLastAccepted() ids.ID
GetStatelessBlock(blockID ids.ID) (block.Block, error)

Expand Down Expand Up @@ -198,16 +202,20 @@ func (m *manager) GetValidatorSet(
return validatorSet, nil
}

etnaHeight, err := m.state.GetEtnaHeight()
if err != nil && err != database.ErrNotFound {
return nil, err
}

// get the start time to track metrics
startTime := m.clk.Time()

var (
validatorSet map[ids.NodeID]*validators.GetValidatorOutput
currentHeight uint64
err error
)
if subnetID == constants.PrimaryNetworkID {
validatorSet, currentHeight, err = m.makePrimaryNetworkValidatorSet(ctx, targetHeight)
if subnetID == constants.PrimaryNetworkID || (err == nil && targetHeight >= etnaHeight) {
validatorSet, currentHeight, err = m.makeValidatorSet(ctx, targetHeight, subnetID)
} else {
validatorSet, currentHeight, err = m.makeSubnetValidatorSet(ctx, targetHeight, subnetID)
}
Expand Down Expand Up @@ -243,24 +251,25 @@ func (m *manager) getValidatorSetCache(subnetID ids.ID) cache.Cacher[uint64, map
return validatorSetsCache
}

func (m *manager) makePrimaryNetworkValidatorSet(
func (m *manager) makeValidatorSet(
ctx context.Context,
targetHeight uint64,
subnetID ids.ID,
) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) {
validatorSet, currentHeight, err := m.getCurrentPrimaryValidatorSet(ctx)
validatorSet, currentHeight, err := m.getCurrentValidatorSet(ctx, subnetID)
if err != nil {
return nil, 0, err
}
if currentHeight < targetHeight {
return nil, 0, fmt.Errorf("%w with SubnetID = %s: current P-chain height (%d) < requested P-Chain height (%d)",
errUnfinalizedHeight,
constants.PrimaryNetworkID,
subnetID,
currentHeight,
targetHeight,
)
}

// Rebuild primary network validators at [targetHeight]
// Rebuild subnet validators at [targetHeight]
//
// Note: Since we are attempting to generate the validator set at
// [targetHeight], we want to apply the diffs from
Expand All @@ -272,7 +281,7 @@ func (m *manager) makePrimaryNetworkValidatorSet(
validatorSet,
currentHeight,
lastDiffHeight,
constants.PrimaryNetworkID,
subnetID,
)
if err != nil {
return nil, 0, err
Expand All @@ -283,19 +292,22 @@ func (m *manager) makePrimaryNetworkValidatorSet(
validatorSet,
currentHeight,
lastDiffHeight,
constants.PrimaryNetworkID,
subnetID,
)
return validatorSet, currentHeight, err
}

func (m *manager) getCurrentPrimaryValidatorSet(
func (m *manager) getCurrentValidatorSet(
ctx context.Context,
subnetID ids.ID,
) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) {
primaryMap := m.cfg.Validators.GetMap(constants.PrimaryNetworkID)
subnetMap := m.cfg.Validators.GetMap(subnetID)
currentHeight, err := m.getCurrentHeight(ctx)
return primaryMap, currentHeight, err
return subnetMap, currentHeight, err
}

// TODO: Once Etna has been activated, remove this function and use
// makeValidatorSet for all validator set lookups.
func (m *manager) makeSubnetValidatorSet(
ctx context.Context,
targetHeight uint64,
Expand Down Expand Up @@ -350,9 +362,6 @@ func (m *manager) makeSubnetValidatorSet(
subnetValidatorSet,
currentHeight,
lastDiffHeight,
// TODO: Etna introduces L1s whose validators specify their own public
// keys, rather than inheriting them from the primary network.
// Therefore, this will need to use the subnetID after Etna.
constants.PrimaryNetworkID,
)
return subnetValidatorSet, currentHeight, err
Expand Down
127 changes: 127 additions & 0 deletions vms/platformvm/validators/manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package validators_test

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/snow/validators"
"github.com/ava-labs/avalanchego/upgrade/upgradetest"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/avalanchego/utils/timer/mockable"
"github.com/ava-labs/avalanchego/vms/platformvm/block"
"github.com/ava-labs/avalanchego/vms/platformvm/config"
"github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest"
"github.com/ava-labs/avalanchego/vms/platformvm/metrics"
"github.com/ava-labs/avalanchego/vms/platformvm/state"
"github.com/ava-labs/avalanchego/vms/platformvm/state/statetest"

. "github.com/ava-labs/avalanchego/vms/platformvm/validators"
)

func TestGetValidatorSet_AfterEtna(t *testing.T) {
require := require.New(t)

vdrs := validators.NewManager()
upgrades := upgradetest.GetConfig(upgradetest.Durango)
upgradeTime := genesistest.DefaultValidatorStartTime.Add(2 * time.Second)
upgrades.EtnaTime = upgradeTime
s := statetest.New(t, statetest.Config{
Validators: vdrs,
Upgrades: upgrades,
})

sk, err := bls.NewSecretKey()
require.NoError(err)
var (
subnetID = ids.GenerateTestID()
startTime = genesistest.DefaultValidatorStartTime
endTime = startTime.Add(24 * time.Hour)
pk = bls.PublicFromSecretKey(sk)
primaryStaker = &state.Staker{
TxID: ids.GenerateTestID(),
NodeID: ids.GenerateTestNodeID(),
PublicKey: pk,
SubnetID: constants.PrimaryNetworkID,
Weight: 1,
StartTime: startTime,
EndTime: endTime,
PotentialReward: 1,
}
subnetStaker = &state.Staker{
TxID: ids.GenerateTestID(),
NodeID: primaryStaker.NodeID,
PublicKey: nil, // inherited from primaryStaker
SubnetID: subnetID,
Weight: 1,
StartTime: upgradeTime,
EndTime: endTime,
}
)

// Add a subnet staker during the Etna upgrade
{
blk, err := block.NewBanffStandardBlock(upgradeTime, s.GetLastAccepted(), 1, nil)
require.NoError(err)

s.SetHeight(blk.Height())
s.SetTimestamp(blk.Timestamp())
s.AddStatelessBlock(blk)
s.SetLastAccepted(blk.ID())

require.NoError(s.PutCurrentValidator(primaryStaker))
require.NoError(s.PutCurrentValidator(subnetStaker))

require.NoError(s.Commit())
}

// Remove a subnet staker
{
blk, err := block.NewBanffStandardBlock(s.GetTimestamp(), s.GetLastAccepted(), 2, nil)
require.NoError(err)

s.SetHeight(blk.Height())
s.SetTimestamp(blk.Timestamp())
s.AddStatelessBlock(blk)
s.SetLastAccepted(blk.ID())

s.DeleteCurrentValidator(subnetStaker)

require.NoError(s.Commit())
}

m := NewManager(
logging.NoLog{},
config.Config{
Validators: vdrs,
},
s,
metrics.Noop,
new(mockable.Clock),
)

expectedValidators := []map[ids.NodeID]*validators.GetValidatorOutput{
{}, // Subnet staker didn't exist at genesis
{
subnetStaker.NodeID: {
NodeID: subnetStaker.NodeID,
PublicKey: pk,
Weight: subnetStaker.Weight,
},
}, // Subnet staker was added at height 1
{}, // Subnet staker was removed at height 2
}
for height, expected := range expectedValidators {
actual, err := m.GetValidatorSet(context.Background(), uint64(height), subnetID)
require.NoError(err)
require.Equal(expected, actual)
}
}

0 comments on commit f500dee

Please sign in to comment.