Skip to content

ProposerVM Epochs POC #3746

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

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
34 changes: 18 additions & 16 deletions upgrade/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,30 @@ var (
EtnaTime: InitiallyActiveTime,
FortunaTime: InitiallyActiveTime,
GraniteTime: UnscheduledActivationTime,
GraniteEpochDuration: 30 * time.Second,
}

ErrInvalidUpgradeTimes = errors.New("invalid upgrade configuration")
)

type Config struct {
ApricotPhase1Time time.Time `json:"apricotPhase1Time"`
ApricotPhase2Time time.Time `json:"apricotPhase2Time"`
ApricotPhase3Time time.Time `json:"apricotPhase3Time"`
ApricotPhase4Time time.Time `json:"apricotPhase4Time"`
ApricotPhase4MinPChainHeight uint64 `json:"apricotPhase4MinPChainHeight"`
ApricotPhase5Time time.Time `json:"apricotPhase5Time"`
ApricotPhasePre6Time time.Time `json:"apricotPhasePre6Time"`
ApricotPhase6Time time.Time `json:"apricotPhase6Time"`
ApricotPhasePost6Time time.Time `json:"apricotPhasePost6Time"`
BanffTime time.Time `json:"banffTime"`
CortinaTime time.Time `json:"cortinaTime"`
CortinaXChainStopVertexID ids.ID `json:"cortinaXChainStopVertexID"`
DurangoTime time.Time `json:"durangoTime"`
EtnaTime time.Time `json:"etnaTime"`
FortunaTime time.Time `json:"fortunaTime"`
GraniteTime time.Time `json:"graniteTime"`
ApricotPhase1Time time.Time `json:"apricotPhase1Time"`
ApricotPhase2Time time.Time `json:"apricotPhase2Time"`
ApricotPhase3Time time.Time `json:"apricotPhase3Time"`
ApricotPhase4Time time.Time `json:"apricotPhase4Time"`
ApricotPhase4MinPChainHeight uint64 `json:"apricotPhase4MinPChainHeight"`
ApricotPhase5Time time.Time `json:"apricotPhase5Time"`
ApricotPhasePre6Time time.Time `json:"apricotPhasePre6Time"`
ApricotPhase6Time time.Time `json:"apricotPhase6Time"`
ApricotPhasePost6Time time.Time `json:"apricotPhasePost6Time"`
BanffTime time.Time `json:"banffTime"`
CortinaTime time.Time `json:"cortinaTime"`
CortinaXChainStopVertexID ids.ID `json:"cortinaXChainStopVertexID"`
DurangoTime time.Time `json:"durangoTime"`
EtnaTime time.Time `json:"etnaTime"`
FortunaTime time.Time `json:"fortunaTime"`
GraniteTime time.Time `json:"graniteTime"`
GraniteEpochDuration time.Duration `json:"graniteEpochDuration"`
}

func (c *Config) Validate() error {
Expand Down
12 changes: 9 additions & 3 deletions vms/proposervm/batched_vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,9 @@ func TestBatchedParseBlockParallel(t *testing.T) {
parentID := ids.ID{1}
timestamp := time.Unix(123, 0)
pChainHeight := uint64(2)
pChainEpochHeight := uint64(2)
epochNumber := uint64(0)
epochStartTime := time.Unix(123, 0)
chainID := ids.GenerateTestID()

vm := VM{
Expand All @@ -616,10 +619,10 @@ func TestBatchedParseBlockParallel(t *testing.T) {

blockThatCantBeParsed := snowmantest.BuildChild(snowmantest.Genesis)

blocksWithUnparsable := makeParseableBlocks(t, parentID, timestamp, pChainHeight, cert, chainID, key)
blocksWithUnparsable := makeParseableBlocks(t, parentID, timestamp, pChainHeight, pChainEpochHeight, epochNumber, epochStartTime, cert, chainID, key)
blocksWithUnparsable[50] = blockThatCantBeParsed.Bytes()

parsableBlocks := makeParseableBlocks(t, parentID, timestamp, pChainHeight, cert, chainID, key)
parsableBlocks := makeParseableBlocks(t, parentID, timestamp, pChainHeight, pChainEpochHeight, epochNumber, epochStartTime, cert, chainID, key)

for _, testCase := range []struct {
name string
Expand Down Expand Up @@ -663,14 +666,17 @@ func TestBatchedParseBlockParallel(t *testing.T) {
}
}

func makeParseableBlocks(t *testing.T, parentID ids.ID, timestamp time.Time, pChainHeight uint64, cert *staking.Certificate, chainID ids.ID, key crypto.Signer) [][]byte {
func makeParseableBlocks(t *testing.T, parentID ids.ID, timestamp time.Time, pChainHeight uint64, pChainEpochHeight uint64, epochNumber uint64, epochStartTime time.Time, cert *staking.Certificate, chainID ids.ID, key crypto.Signer) [][]byte {
makeSignedBlock := func(i int) []byte {
buff := binary.AppendVarint(nil, int64(i))

signedBlock, err := blockbuilder.Build(
parentID,
timestamp,
pChainHeight,
pChainEpochHeight,
epochNumber,
epochStartTime,
cert,
buff,
chainID,
Expand Down
100 changes: 95 additions & 5 deletions vms/proposervm/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type Block interface {
buildChild(context.Context) (Block, error)

pChainHeight(context.Context) (uint64, error)
pChainEpochHeight(context.Context) (uint64, error)
epochNumber(context.Context) (uint64, error)
epochStartTime(context.Context) (time.Time, error)
}

type PostForkBlock interface {
Expand All @@ -79,6 +82,51 @@ func (p *postForkCommonComponents) Height() uint64 {
return p.innerBlk.Height()
}

// Calculates a block's P-Chain epoch height based on its ancestor's epoch membership
func (p *postForkCommonComponents) getPChainEpoch(ctx context.Context, parentID ids.ID) (uint64, uint64, time.Time, error) {
parent, err := p.vm.getBlock(ctx, parentID)
if err != nil {
return 0, 0, time.Time{}, err
}
parentTimestamp := parent.Timestamp()
epoch, err := parent.epochNumber(ctx)
if err != nil {
return 0, 0, time.Time{}, fmt.Errorf("failed to get epoch number: %w", err)
}
epochStartTime, err := parent.epochStartTime(ctx)
if err != nil {
return 0, 0, time.Time{}, fmt.Errorf("failed to get epoch start time: %w", err)
}
if epochStartTime.IsZero() {
// If the parent was not assigned an epoch, then the child is the first block of
// the initial epoch.
height, err := parent.pChainHeight(ctx)
if err != nil {
return 0, 0, time.Time{}, fmt.Errorf("failed to get P-Chain height: %w", err)
}
return height, 0, parentTimestamp, nil
}

if parentTimestamp.After(epochStartTime.Add(p.vm.Upgrades.GraniteEpochDuration)) {
// If the parent crossed the epoch boundary, then it sealed the previous epoch. The child
// is the first block of the new epoch, so should use the parent's P-Chain height, increment
// the epoch number, and set the epoch start time to the parent's timestamp.
height, err := parent.pChainHeight(ctx)
if err != nil {
return 0, 0, time.Time{}, fmt.Errorf("failed to get P-Chain height: %w", err)
}
return height, epoch + 1, parentTimestamp, nil
}
// Otherwise, the parent did not seal the previous epoch, so the child should use the parent's
// epoch information. This is true even if the child crosses the epoch boundary, since sealing
// blocks are considered to be part of the epoch they seal.
height, err := parent.pChainEpochHeight(ctx)
if err != nil {
return 0, 0, time.Time{}, fmt.Errorf("failed to get P-Chain height: %w", err)
}
return height, epoch, epochStartTime, nil
}

// Verify returns nil if:
// 1) [p]'s inner block is not an oracle block
// 2) [child]'s P-Chain height >= [parentPChainHeight]
Expand All @@ -89,6 +137,7 @@ func (p *postForkCommonComponents) Height() uint64 {
// 7) [child]'s timestamp is within its proposer's window
// 8) [child] has a valid signature from its proposer
// 9) [child]'s inner block is valid
// 10) [child] has the expected P-Chain epoch height
func (p *postForkCommonComponents) Verify(
ctx context.Context,
parentTimestamp time.Time,
Expand Down Expand Up @@ -163,9 +212,23 @@ func (p *postForkCommonComponents) Verify(
}

var contextPChainHeight uint64
if p.vm.Upgrades.IsEtnaActivated(childTimestamp) {
switch {
case p.vm.Upgrades.IsGraniteActivated(childTimestamp):
pChainEpochHeight, _, _, err := p.getPChainEpoch(ctx, child.Parent())
if err != nil {
p.vm.ctx.Log.Error("unexpected build verification failure",
zap.String("reason", "failed to get P-Chain epoch height"),
zap.Stringer("parentID", child.Parent()),
zap.Error(err),
)
}
if childHeight := child.PChainEpochHeight(); pChainEpochHeight != childHeight {
return fmt.Errorf("epoch height mismatch: expectedEpochHeight %d != epochHeight %d", pChainEpochHeight, childHeight)
}
contextPChainHeight = pChainEpochHeight
case p.vm.Upgrades.IsEtnaActivated(childTimestamp):
contextPChainHeight = childPChainHeight
} else {
default:
contextPChainHeight = parentPChainHeight
}

Expand Down Expand Up @@ -225,10 +288,31 @@ func (p *postForkCommonComponents) buildChild(
return nil, err
}

var contextPChainHeight uint64
if p.vm.Upgrades.IsEtnaActivated(newTimestamp) {
var (
contextPChainHeight, pChainEpochHeight, epochNumber uint64
epochStartTime time.Time
)
switch {
case p.vm.Upgrades.IsGraniteActivated(newTimestamp):
pChainEpochHeight, epochNumber, epochStartTime, err = p.getPChainEpoch(ctx, parentID)
if err != nil {
p.vm.ctx.Log.Error("unexpected build block failure",
zap.String("reason", "failed to get P-Chain epoch height"),
zap.Stringer("parentID", parentID),
zap.Error(err),
)
}
p.vm.ctx.Log.Debug(
"epoch",
zap.Uint64("pChainHeight", pChainHeight),
zap.Uint64("pChainEpochHeight", pChainEpochHeight),
zap.Uint64("epochNumber", epochNumber),
zap.Time("epochStartTime", epochStartTime),
)
contextPChainHeight = pChainEpochHeight
case p.vm.Upgrades.IsEtnaActivated(newTimestamp):
contextPChainHeight = pChainHeight
} else {
default:
contextPChainHeight = parentPChainHeight
}

Expand All @@ -251,6 +335,9 @@ func (p *postForkCommonComponents) buildChild(
parentID,
newTimestamp,
pChainHeight,
pChainEpochHeight,
epochNumber,
epochStartTime,
p.vm.StakingCertLeaf,
innerBlock.Bytes(),
p.vm.ctx.ChainID,
Expand All @@ -261,6 +348,9 @@ func (p *postForkCommonComponents) buildChild(
parentID,
newTimestamp,
pChainHeight,
pChainEpochHeight,
epochNumber,
epochStartTime,
innerBlock.Bytes(),
)
}
Expand Down
28 changes: 23 additions & 5 deletions vms/proposervm/block/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type SignedBlock interface {
Block

PChainHeight() uint64
PChainEpochHeight() uint64
EpochNumber() uint64
EpochStartTime() time.Time
Timestamp() time.Time

// Proposer returns the ID of the node that proposed this block. If no node
Expand All @@ -43,11 +46,14 @@ type SignedBlock interface {
}

type statelessUnsignedBlock struct {
ParentID ids.ID `serialize:"true"`
Timestamp int64 `serialize:"true"`
PChainHeight uint64 `serialize:"true"`
Certificate []byte `serialize:"true"`
Block []byte `serialize:"true"`
ParentID ids.ID `serialize:"true"`
Timestamp int64 `serialize:"true"`
PChainHeight uint64 `serialize:"true"`
PChainEpochHeight uint64 `serialize:"true"`
EpochNumber uint64 `serialize:"true"`
EpochStartTime int64 `serialize:"true"`
Certificate []byte `serialize:"true"`
Block []byte `serialize:"true"`
}

type statelessBlock struct {
Expand Down Expand Up @@ -127,6 +133,18 @@ func (b *statelessBlock) PChainHeight() uint64 {
return b.StatelessBlock.PChainHeight
}

func (b *statelessBlock) PChainEpochHeight() uint64 {
return b.StatelessBlock.PChainEpochHeight
}

func (b *statelessBlock) EpochNumber() uint64 {
return b.StatelessBlock.EpochNumber
}

func (b *statelessBlock) EpochStartTime() time.Time {
return time.Unix(b.StatelessBlock.EpochStartTime, 0)
}

func (b *statelessBlock) Timestamp() time.Time {
return b.timestamp
}
Expand Down
2 changes: 1 addition & 1 deletion vms/proposervm/block/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ func TestBlockSizeLimit(t *testing.T) {
innerBlockBytes := bytes.Repeat([]byte{0}, 270*units.KiB)

// with the large limit, it should be able to build large blocks
_, err := BuildUnsigned(parentID, timestamp, pChainHeight, innerBlockBytes)
_, err := BuildUnsigned(parentID, timestamp, pChainHeight, 0, 0, time.Time{}, innerBlockBytes)
require.NoError(err)
}
32 changes: 22 additions & 10 deletions vms/proposervm/block/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,21 @@ func BuildUnsigned(
parentID ids.ID,
timestamp time.Time,
pChainHeight uint64,
pChainEpochHeight uint64,
epochNumber uint64,
epochStartTime time.Time,
blockBytes []byte,
) (SignedBlock, error) {
var block SignedBlock = &statelessBlock{
StatelessBlock: statelessUnsignedBlock{
ParentID: parentID,
Timestamp: timestamp.Unix(),
PChainHeight: pChainHeight,
Certificate: nil,
Block: blockBytes,
ParentID: parentID,
Timestamp: timestamp.Unix(),
PChainHeight: pChainHeight,
PChainEpochHeight: pChainEpochHeight,
EpochNumber: epochNumber,
EpochStartTime: epochStartTime.Unix(),
Certificate: nil,
Block: blockBytes,
},
timestamp: timestamp,
}
Expand All @@ -43,18 +49,24 @@ func Build(
parentID ids.ID,
timestamp time.Time,
pChainHeight uint64,
pChainEpochHeight uint64,
epochNumber uint64,
epochStartTime time.Time,
cert *staking.Certificate,
blockBytes []byte,
chainID ids.ID,
key crypto.Signer,
) (SignedBlock, error) {
block := &statelessBlock{
StatelessBlock: statelessUnsignedBlock{
ParentID: parentID,
Timestamp: timestamp.Unix(),
PChainHeight: pChainHeight,
Certificate: cert.Raw,
Block: blockBytes,
ParentID: parentID,
Timestamp: timestamp.Unix(),
PChainHeight: pChainHeight,
PChainEpochHeight: pChainEpochHeight,
EpochNumber: epochNumber,
EpochStartTime: epochStartTime.Unix(),
Certificate: cert.Raw,
Block: blockBytes,
},
timestamp: timestamp,
cert: cert,
Expand Down
11 changes: 10 additions & 1 deletion vms/proposervm/block/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ func TestBuild(t *testing.T) {
parentID := ids.ID{1}
timestamp := time.Unix(123, 0)
pChainHeight := uint64(2)
pChainEpochHeight := uint64(2)
epochNumber := uint64(0)
epochStartTime := time.Unix(123, 0)
innerBlockBytes := []byte{3}
chainID := ids.ID{4}

Expand All @@ -35,6 +38,9 @@ func TestBuild(t *testing.T) {
parentID,
timestamp,
pChainHeight,
pChainEpochHeight,
epochNumber,
epochStartTime,
cert,
innerBlockBytes,
chainID,
Expand All @@ -53,11 +59,14 @@ func TestBuildUnsigned(t *testing.T) {
parentID := ids.ID{1}
timestamp := time.Unix(123, 0)
pChainHeight := uint64(2)
pChainEpochHeight := uint64(2)
epochNumber := uint64(0)
epochStartTime := time.Unix(123, 0)
innerBlockBytes := []byte{3}

require := require.New(t)

builtBlock, err := BuildUnsigned(parentID, timestamp, pChainHeight, innerBlockBytes)
builtBlock, err := BuildUnsigned(parentID, timestamp, pChainHeight, pChainEpochHeight, epochNumber, epochStartTime, innerBlockBytes)
require.NoError(err)

require.Equal(parentID, builtBlock.ParentID())
Expand Down
Loading
Loading