package detect import ( "fmt" "testing" "github.com/OffchainLabs/prysm/v7/beacon-chain/state" "github.com/OffchainLabs/prysm/v7/config/params" "github.com/OffchainLabs/prysm/v7/consensus-types/blocks" "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/runtime/version" "github.com/OffchainLabs/prysm/v7/testing/require" "github.com/OffchainLabs/prysm/v7/testing/util" "github.com/OffchainLabs/prysm/v7/time/slots" ) func TestSlotFromBlock(t *testing.T) { b := util.NewBeaconBlock() var slot primitives.Slot = 3 b.Block.Slot = slot bb, err := b.MarshalSSZ() require.NoError(t, err) sfb, err := slotFromBlock(bb) require.NoError(t, err) require.Equal(t, slot, sfb) ba := util.NewBeaconBlockAltair() ba.Block.Slot = slot bab, err := ba.MarshalSSZ() require.NoError(t, err) sfba, err := slotFromBlock(bab) require.NoError(t, err) require.Equal(t, slot, sfba) bm := util.NewBeaconBlockBellatrix() bm.Block.Slot = slot bmb, err := ba.MarshalSSZ() require.NoError(t, err) sfbm, err := slotFromBlock(bmb) require.NoError(t, err) require.Equal(t, slot, sfbm) } func TestByState(t *testing.T) { defer util.HackForksMaxuint(t, []int{version.Electra, version.Fulu})() bc := params.BeaconConfig() altairSlot, err := slots.EpochStart(bc.AltairForkEpoch) require.NoError(t, err) bellaSlot, err := slots.EpochStart(bc.BellatrixForkEpoch) require.NoError(t, err) capellaSlot, err := slots.EpochStart(bc.CapellaForkEpoch) require.NoError(t, err) denebSlot, err := slots.EpochStart(bc.DenebForkEpoch) require.NoError(t, err) electraSlot, err := slots.EpochStart(bc.ElectraForkEpoch) require.NoError(t, err) fuluSlot, err := slots.EpochStart(bc.FuluForkEpoch) require.NoError(t, err) cases := []struct { name string version int slot primitives.Slot forkversion [4]byte }{ { name: "genesis", version: version.Phase0, slot: 0, forkversion: bytesutil.ToBytes4(bc.GenesisForkVersion), }, { name: "altair", version: version.Altair, slot: altairSlot, forkversion: bytesutil.ToBytes4(bc.AltairForkVersion), }, { name: "bellatrix", version: version.Bellatrix, slot: bellaSlot, forkversion: bytesutil.ToBytes4(bc.BellatrixForkVersion), }, { name: "capella", version: version.Capella, slot: capellaSlot, forkversion: bytesutil.ToBytes4(bc.CapellaForkVersion), }, { name: "deneb", version: version.Deneb, slot: denebSlot, forkversion: bytesutil.ToBytes4(bc.DenebForkVersion), }, { name: "electra", version: version.Electra, slot: electraSlot, forkversion: bytesutil.ToBytes4(bc.ElectraForkVersion), }, { name: "fulu", version: version.Fulu, slot: fuluSlot, forkversion: bytesutil.ToBytes4(bc.FuluForkVersion), }, } for _, c := range cases { st, err := stateForVersion(c.version) require.NoError(t, err) fork, err := params.Fork(slots.ToEpoch(c.slot)) require.NoError(t, err) require.NoError(t, st.SetFork(fork)) require.NoError(t, st.SetSlot(c.slot)) m, err := st.MarshalSSZ() require.NoError(t, err) cf, err := FromState(m) require.NoError(t, err) require.Equal(t, c.version, cf.Fork) require.Equal(t, c.forkversion, cf.Version) require.Equal(t, bc.ConfigName, cf.Config.ConfigName) } } func stateForVersion(v int) (state.BeaconState, error) { switch v { case version.Phase0: return util.NewBeaconState() case version.Altair: return util.NewBeaconStateAltair() case version.Bellatrix: return util.NewBeaconStateBellatrix() case version.Capella: return util.NewBeaconStateCapella() case version.Deneb: return util.NewBeaconStateDeneb() case version.Electra: return util.NewBeaconStateElectra() case version.Fulu: return util.NewBeaconStateFulu() default: return nil, fmt.Errorf("unrecognized version %d", v) } } func TestUnmarshalState(t *testing.T) { ctx := t.Context() defer util.HackForksMaxuint(t, []int{version.Electra, version.Fulu})() bc := params.BeaconConfig() altairSlot, err := slots.EpochStart(bc.AltairForkEpoch) require.NoError(t, err) bellaSlot, err := slots.EpochStart(bc.BellatrixForkEpoch) require.NoError(t, err) capellaSlot, err := slots.EpochStart(bc.CapellaForkEpoch) require.NoError(t, err) denebSlot, err := slots.EpochStart(bc.DenebForkEpoch) require.NoError(t, err) electraSlot, err := slots.EpochStart(bc.ElectraForkEpoch) require.NoError(t, err) fuluSlot, err := slots.EpochStart(bc.FuluForkEpoch) require.NoError(t, err) cases := []struct { name string version int slot primitives.Slot forkversion [4]byte }{ { name: "genesis", version: version.Phase0, slot: 0, forkversion: bytesutil.ToBytes4(bc.GenesisForkVersion), }, { name: "altair", version: version.Altair, slot: altairSlot, forkversion: bytesutil.ToBytes4(bc.AltairForkVersion), }, { name: "bellatrix", version: version.Bellatrix, slot: bellaSlot, forkversion: bytesutil.ToBytes4(bc.BellatrixForkVersion), }, { name: "capella", version: version.Capella, slot: capellaSlot, forkversion: bytesutil.ToBytes4(bc.CapellaForkVersion), }, { name: "deneb", version: version.Deneb, slot: denebSlot, forkversion: bytesutil.ToBytes4(bc.DenebForkVersion), }, { name: "electra", version: version.Electra, slot: electraSlot, forkversion: bytesutil.ToBytes4(bc.ElectraForkVersion), }, { name: "fulu", version: version.Fulu, slot: fuluSlot, forkversion: bytesutil.ToBytes4(bc.FuluForkVersion), }, } for _, c := range cases { st, err := stateForVersion(c.version) require.NoError(t, err) require.NoError(t, st.SetFork(ðpb.Fork{ PreviousVersion: make([]byte, 4), CurrentVersion: c.forkversion[:], Epoch: 0, })) require.NoError(t, st.SetSlot(c.slot)) m, err := st.MarshalSSZ() require.NoError(t, err) cf, err := FromState(m) require.NoError(t, err) s, err := cf.UnmarshalBeaconState(m) require.NoError(t, err) expected, err := st.HashTreeRoot(ctx) require.NoError(t, err) actual, err := s.HashTreeRoot(ctx) require.NoError(t, err) require.DeepEqual(t, expected, actual) } } func TestDetectAndUnmarshalBlock(t *testing.T) { defer util.HackForksMaxuint(t, []int{version.Electra, version.Fulu})() altairS, err := slots.EpochStart(params.BeaconConfig().AltairForkEpoch) require.NoError(t, err) bellaS, err := slots.EpochStart(params.BeaconConfig().BellatrixForkEpoch) require.NoError(t, err) capellaS, err := slots.EpochStart(params.BeaconConfig().CapellaForkEpoch) require.NoError(t, err) denebS, err := slots.EpochStart(params.BeaconConfig().DenebForkEpoch) require.NoError(t, err) electraS, err := slots.EpochStart(params.BeaconConfig().ElectraForkEpoch) require.NoError(t, err) fuluS, err := slots.EpochStart(params.BeaconConfig().FuluForkEpoch) require.NoError(t, err) cases := []struct { b func(*testing.T, primitives.Slot) interfaces.ReadOnlySignedBeaconBlock name string slot primitives.Slot errExists bool }{ { name: "genesis - slot 0", b: signedTestBlockGenesis, }, { name: "last slot of phase 0", b: signedTestBlockGenesis, slot: altairS - 1, }, { name: "first slot of altair", b: signedTestBlockAltair, slot: altairS, }, { name: "last slot of altair", b: signedTestBlockAltair, slot: bellaS - 1, }, { name: "first slot of bellatrix", b: signedTestBlockBellatrix, slot: bellaS, }, { name: "first slot of capella", b: signedTestBlockCapella, slot: capellaS, }, { name: "last slot of capella", b: signedTestBlockCapella, slot: denebS - 1, }, { name: "first slot of deneb", b: signedTestBlockDeneb, slot: denebS, }, { name: "first slot of electra", b: signedTestBlockElectra, slot: electraS, }, { name: "first slot of fulu", b: signedTestBlockFulu, slot: fuluS, }, { name: "bellatrix block in altair slot", b: signedTestBlockBellatrix, slot: bellaS - 1, errExists: true, }, { name: "genesis block in altair slot", b: signedTestBlockGenesis, slot: bellaS - 1, errExists: true, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { b := c.b(t, c.slot) marshaled, err := b.MarshalSSZ() require.NoError(t, err) cf, err := FromBlock(marshaled) require.NoError(t, err) bcf, err := cf.UnmarshalBeaconBlock(marshaled) if c.errExists { require.NotNil(t, err) return } require.NoError(t, err) expected, err := b.Block().HashTreeRoot() require.NoError(t, err) actual, err := bcf.Block().HashTreeRoot() require.NoError(t, err) require.Equal(t, expected, actual) }) } } func TestUnmarshalBlock(t *testing.T) { defer util.HackForksMaxuint(t, []int{version.Electra, version.Fulu})() genv := bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion) altairv := bytesutil.ToBytes4(params.BeaconConfig().AltairForkVersion) bellav := bytesutil.ToBytes4(params.BeaconConfig().BellatrixForkVersion) capellaV := bytesutil.ToBytes4(params.BeaconConfig().CapellaForkVersion) denebV := bytesutil.ToBytes4(params.BeaconConfig().DenebForkVersion) electraV := bytesutil.ToBytes4(params.BeaconConfig().ElectraForkVersion) fuluV := bytesutil.ToBytes4(params.BeaconConfig().FuluForkVersion) altairS, err := slots.EpochStart(params.BeaconConfig().AltairForkEpoch) require.NoError(t, err) bellaS, err := slots.EpochStart(params.BeaconConfig().BellatrixForkEpoch) require.NoError(t, err) capellaS, err := slots.EpochStart(params.BeaconConfig().CapellaForkEpoch) require.NoError(t, err) denebS, err := slots.EpochStart(params.BeaconConfig().DenebForkEpoch) require.NoError(t, err) electraS, err := slots.EpochStart(params.BeaconConfig().ElectraForkEpoch) require.NoError(t, err) fuluS, err := slots.EpochStart(params.BeaconConfig().FuluForkEpoch) require.NoError(t, err) cases := []struct { b func(*testing.T, primitives.Slot) interfaces.ReadOnlySignedBeaconBlock name string version [4]byte slot primitives.Slot err error }{ { name: "genesis - slot 0", b: signedTestBlockGenesis, version: genv, }, { name: "last slot of phase 0", b: signedTestBlockGenesis, version: genv, slot: altairS - 1, }, { name: "first slot of altair", b: signedTestBlockAltair, version: altairv, slot: altairS, }, { name: "last slot of altair", b: signedTestBlockAltair, version: altairv, slot: bellaS - 1, }, { name: "first slot of bellatrix", b: signedTestBlockBellatrix, version: bellav, slot: bellaS, }, { name: "first slot of capella", b: signedTestBlockCapella, version: capellaV, slot: capellaS, }, { name: "last slot of capella", b: signedTestBlockCapella, version: capellaV, slot: denebS - 1, }, { name: "first slot of deneb", b: signedTestBlockDeneb, version: denebV, slot: denebS, }, { name: "first slot of electra", b: signedTestBlockElectra, version: electraV, slot: electraS, }, { name: "first slot of fulu", b: signedTestBlockFulu, version: fuluV, slot: fuluS, }, { name: "bellatrix block in altair slot", b: signedTestBlockBellatrix, version: bellav, slot: bellaS - 1, err: errBlockForkMismatch, }, { name: "genesis block in altair slot", b: signedTestBlockGenesis, version: genv, slot: bellaS - 1, err: errBlockForkMismatch, }, { name: "altair block in genesis slot", b: signedTestBlockAltair, version: altairv, err: errBlockForkMismatch, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { b := c.b(t, c.slot) marshaled, err := b.MarshalSSZ() require.NoError(t, err) cf, err := FromForkVersion(c.version) require.NoError(t, err) bcf, err := cf.UnmarshalBeaconBlock(marshaled) if c.err != nil { require.ErrorIs(t, err, c.err) return } require.NoError(t, err) expected, err := b.Block().HashTreeRoot() require.NoError(t, err) actual, err := bcf.Block().HashTreeRoot() require.NoError(t, err) require.Equal(t, expected, actual) }) } } func TestUnmarshalBlindedBlock(t *testing.T) { defer util.HackForksMaxuint(t, []int{version.Electra, version.Fulu})() genv := bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion) altairv := bytesutil.ToBytes4(params.BeaconConfig().AltairForkVersion) bellav := bytesutil.ToBytes4(params.BeaconConfig().BellatrixForkVersion) capellaV := bytesutil.ToBytes4(params.BeaconConfig().CapellaForkVersion) denebV := bytesutil.ToBytes4(params.BeaconConfig().DenebForkVersion) electraV := bytesutil.ToBytes4(params.BeaconConfig().ElectraForkVersion) fuluV := bytesutil.ToBytes4(params.BeaconConfig().FuluForkVersion) altairS, err := slots.EpochStart(params.BeaconConfig().AltairForkEpoch) require.NoError(t, err) bellaS, err := slots.EpochStart(params.BeaconConfig().BellatrixForkEpoch) require.NoError(t, err) capellaS, err := slots.EpochStart(params.BeaconConfig().CapellaForkEpoch) require.NoError(t, err) denebS, err := slots.EpochStart(params.BeaconConfig().DenebForkEpoch) require.NoError(t, err) electraS, err := slots.EpochStart(params.BeaconConfig().ElectraForkEpoch) require.NoError(t, err) fuluS, err := slots.EpochStart(params.BeaconConfig().FuluForkEpoch) require.NoError(t, err) cases := []struct { b func(*testing.T, primitives.Slot) interfaces.ReadOnlySignedBeaconBlock name string version [4]byte slot primitives.Slot err error }{ { name: "genesis - slot 0", b: signedTestBlockGenesis, version: genv, }, { name: "last slot of phase 0", b: signedTestBlockGenesis, version: genv, slot: altairS - 1, }, { name: "first slot of altair", b: signedTestBlockAltair, version: altairv, slot: altairS, }, { name: "last slot of altair", b: signedTestBlockAltair, version: altairv, slot: bellaS - 1, }, { name: "first slot of bellatrix", b: signedTestBlindedBlockBellatrix, version: bellav, slot: bellaS, }, { name: "bellatrix block in altair slot", b: signedTestBlindedBlockBellatrix, version: bellav, slot: bellaS - 1, err: errBlockForkMismatch, }, { name: "first slot of capella", b: signedTestBlindedBlockCapella, version: capellaV, slot: capellaS, }, { name: "last slot of capella", b: signedTestBlindedBlockCapella, version: capellaV, slot: denebS - 1, }, { name: "first slot of deneb", b: signedTestBlindedBlockDeneb, version: denebV, slot: denebS, }, { name: "first slot of electra", b: signedTestBlindedBlockElectra, version: electraV, slot: electraS, }, { name: "first slot of fulu", b: signedTestBlindedBlockFulu, version: fuluV, slot: fuluS, }, { name: "genesis block in altair slot", b: signedTestBlockGenesis, version: genv, slot: bellaS - 1, err: errBlockForkMismatch, }, { name: "altair block in genesis slot", b: signedTestBlockAltair, version: altairv, err: errBlockForkMismatch, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { b := c.b(t, c.slot) marshaled, err := b.MarshalSSZ() require.NoError(t, err) cf, err := FromForkVersion(c.version) require.NoError(t, err) bcf, err := cf.UnmarshalBlindedBeaconBlock(marshaled) if c.err != nil { require.ErrorIs(t, err, c.err) return } require.NoError(t, err) expected, err := b.Block().HashTreeRoot() require.NoError(t, err) actual, err := bcf.Block().HashTreeRoot() require.NoError(t, err) require.Equal(t, expected, actual) }) } } // ---------------------------------------------------------------------------- // Phase 0 // ---------------------------------------------------------------------------- func signedTestBlockGenesis(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBeaconBlock() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } // ---------------------------------------------------------------------------- // Altair // ---------------------------------------------------------------------------- func signedTestBlockAltair(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBeaconBlockAltair() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } // ---------------------------------------------------------------------------- // Bellatrix // ---------------------------------------------------------------------------- func signedTestBlockBellatrix(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBeaconBlockBellatrix() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } func signedTestBlindedBlockBellatrix(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBlindedBeaconBlockBellatrix() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } // ---------------------------------------------------------------------------- // Capella // ---------------------------------------------------------------------------- func signedTestBlockCapella(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBeaconBlockCapella() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } func signedTestBlindedBlockCapella(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBlindedBeaconBlockCapella() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } // ---------------------------------------------------------------------------- // Deneb // ---------------------------------------------------------------------------- func signedTestBlockDeneb(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBeaconBlockDeneb() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } func signedTestBlindedBlockDeneb(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBlindedBeaconBlockDeneb() b.Message.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } // ---------------------------------------------------------------------------- // Electra // ---------------------------------------------------------------------------- func signedTestBlockElectra(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBeaconBlockElectra() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } func signedTestBlindedBlockElectra(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBlindedBeaconBlockElectra() b.Message.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } // ---------------------------------------------------------------------------- // Fulu // ---------------------------------------------------------------------------- func signedTestBlockFulu(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBeaconBlockFulu() b.Block.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } func signedTestBlindedBlockFulu(t *testing.T, slot primitives.Slot) interfaces.ReadOnlySignedBeaconBlock { b := util.NewBlindedBeaconBlockFulu() b.Message.Slot = slot s, err := blocks.NewSignedBeaconBlock(b) require.NoError(t, err) return s } func TestUnmarshalStateStandalone(t *testing.T) { ctx := t.Context() defer util.HackForksMaxuint(t, []int{version.Electra, version.Fulu})() bc := params.BeaconConfig() altairSlot, err := slots.EpochStart(bc.AltairForkEpoch) require.NoError(t, err) bellaSlot, err := slots.EpochStart(bc.BellatrixForkEpoch) require.NoError(t, err) capellaSlot, err := slots.EpochStart(bc.CapellaForkEpoch) require.NoError(t, err) denebSlot, err := slots.EpochStart(bc.DenebForkEpoch) require.NoError(t, err) electraSlot, err := slots.EpochStart(bc.ElectraForkEpoch) require.NoError(t, err) fuluSlot, err := slots.EpochStart(bc.FuluForkEpoch) require.NoError(t, err) cases := []struct { name string version int slot primitives.Slot forkversion [4]byte }{ { name: "phase0", version: version.Phase0, slot: 0, forkversion: bytesutil.ToBytes4(bc.GenesisForkVersion), }, { name: "altair", version: version.Altair, slot: altairSlot, forkversion: bytesutil.ToBytes4(bc.AltairForkVersion), }, { name: "bellatrix", version: version.Bellatrix, slot: bellaSlot, forkversion: bytesutil.ToBytes4(bc.BellatrixForkVersion), }, { name: "capella", version: version.Capella, slot: capellaSlot, forkversion: bytesutil.ToBytes4(bc.CapellaForkVersion), }, { name: "deneb", version: version.Deneb, slot: denebSlot, forkversion: bytesutil.ToBytes4(bc.DenebForkVersion), }, { name: "electra", version: version.Electra, slot: electraSlot, forkversion: bytesutil.ToBytes4(bc.ElectraForkVersion), }, { name: "fulu", version: version.Fulu, slot: fuluSlot, forkversion: bytesutil.ToBytes4(bc.FuluForkVersion), }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { // Create a state for the specific version originalState, err := stateForVersion(c.version) require.NoError(t, err) require.NoError(t, originalState.SetFork(ðpb.Fork{ PreviousVersion: make([]byte, 4), CurrentVersion: c.forkversion[:], Epoch: 0, })) require.NoError(t, originalState.SetSlot(c.slot)) marshaled, err := originalState.MarshalSSZ() require.NoError(t, err) unmarshaledState, err := UnmarshalState(marshaled) require.NoError(t, err) require.NotNil(t, unmarshaledState) // Verify the unmarshaled state matches the original expectedRoot, err := originalState.HashTreeRoot(ctx) require.NoError(t, err) actualRoot, err := unmarshaledState.HashTreeRoot(ctx) require.NoError(t, err) require.DeepEqual(t, expectedRoot, actualRoot) // Verify basic state properties require.Equal(t, c.slot, unmarshaledState.Slot()) fork := unmarshaledState.Fork() require.DeepEqual(t, c.forkversion[:], fork.CurrentVersion) }) } t.Run("invalid state data", func(t *testing.T) { invalidData := []byte("bad") _, err := UnmarshalState(invalidData) require.ErrorContains(t, "failed to detect version from state", err) }) }