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
6 changes: 4 additions & 2 deletions adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ func (c *Communication) Validators() common.Nodes {
// Upon an epoch change, it will ignore blocks from previous epochs
// and will call the onEpochChange callback when a new epoch is detected.
type EpochAwareStorage struct {
msm *metadata.StateMachine
onEpochChange func(seq uint64, validators common.Nodes) error
lastNonSimplexHeight uint64
msm *metadata.StateMachine
onEpochChange func(seq uint64, validators common.Nodes) error
Storage
epoch uint64
}
Expand All @@ -49,6 +50,7 @@ func (e *EpochAwareStorage) Retrieve(seq uint64) (common.VerifiedBlock, common.F
parsedBlock := &ParsedBlock{
msm: e.msm,
StateMachineBlock: block,
legacyBlock: seq <= e.lastNonSimplexHeight,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't it be easier to just wrap any non-simplex blocks in the default protocol metadata? this way we wouldn't need the legacy block fields and wouldn't need to use inner.Digest

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the protocol metadata doesn't contain the hash though, and the hash is computed differently for pre-simplex blocks.

}
return parsedBlock, *finalization, nil
}
Expand Down
12 changes: 12 additions & 0 deletions external.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type ParsedBlock struct {
metadata.StateMachineBlock
msm *metadata.StateMachine

legacyBlock bool // true if this is not a simplex block, but a block pre-dating simplex.

// lock guards size, so Size() can be invoked concurrently
lock sync.Mutex
// size caches the length of the Bytes encoding, computed on first use
Expand All @@ -27,6 +29,11 @@ func (p *ParsedBlock) Bytes() []byte {
rawInnerBlock := p.InnerBlock.Bytes()
innerBlockBytes = rawInnerBlock
}

if p.legacyBlock {
return innerBlockBytes
}

rawBlock := &metadata.RawBlock{
Metadata: p.Metadata.Clone(),
InnerBlockBytes: innerBlockBytes,
Expand All @@ -37,6 +44,11 @@ func (p *ParsedBlock) Bytes() []byte {
func (p *ParsedBlock) BlockHeader() common.BlockHeader {
md := p.Metadata.SimplexProtocolMetadata.Clone()
digest := p.Digest()

if p.legacyBlock {
digest = p.InnerBlock.Digest()
}

return common.BlockHeader{
ProtocolMetadata: md,
Digest: digest,
Expand Down
12 changes: 7 additions & 5 deletions instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@ func (i *Instance) createNonValidatorConfig(epochNum uint64, validators common.N
comm.SetValidators(validators)

epochAwareStorage := &EpochAwareStorage{
epoch: epochNum,
Storage: i.Config.Storage,
lastNonSimplexHeight: i.Config.LastNonSimplexInnerBlock.Height(),
epoch: epochNum,
Storage: i.Config.Storage,
onEpochChange: func(epoch uint64, validators common.Nodes) error {
height := i.Config.PlatformChain.GetCurrentHeight()
vdrs, err := i.Config.PlatformChain.GetValidatorSet(height)
Expand Down Expand Up @@ -482,9 +483,10 @@ func (i *Instance) createEpochConfig() (simplex.EpochConfig, error) {
comm.SetValidators(nodes)

epochAwareStorage := &EpochAwareStorage{
msm: msm,
epoch: epochNum,
Storage: i.cs,
lastNonSimplexHeight: i.Config.LastNonSimplexInnerBlock.Height(),
msm: msm,
epoch: epochNum,
Storage: i.cs,
onEpochChange: func(epoch uint64, validators common.Nodes) error {
blockBuilder.stop()
comm.SetValidators(validators)
Expand Down
105 changes: 103 additions & 2 deletions instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,83 @@ func TestInstanceDoubleStartFails(t *testing.T) {
require.ErrorContains(t, inst.Start(t.Context()), "instance already started")
}

// TestInstanceZeroBlockAfterPreSimplexBlocks brings up a network whose ledger already holds
// pre-Simplex blocks (not just the genesis block), and asserts that the zero block the network
// commits chains to the last non-Simplex block through the inner block's digest.
func TestInstanceZeroBlockAfterPreSimplexBlocks(t *testing.T) {
const (
// The ledger holds pre-Simplex blocks from height 0 (genesis) to lastNonSimplexHeight.
lastNonSimplexHeight = uint64(3)
zeroBlockSeq = lastNonSimplexHeight + 1
basePChainHeight = 100
)

var firstID, secondID [20]byte
rand.Read(firstID[:])
rand.Read(secondID[:])
firstNodeID := common.NodeID(firstID[:])
secondNodeID := common.NodeID(secondID[:])

// Both nodes are validators from the start, so neither can commit a block alone: the zero
// block proposed by the leader only gets committed if the other node verifies and votes for it.
validatorSetsAtHeight := map[uint64]metadata.NodeBLSMappings{
basePChainHeight: {
{NodeID: firstID, BLSKey: []byte{0xaa}, Weight: 1},
{NodeID: secondID, BLSKey: []byte{0xbb}, Weight: 1},
},
}

pChain := newTestPlatformChain(basePChainHeight, validatorSetsAtHeight)
cops := &testCryptoOps{}

// The pre-Simplex chain. Timestamps are in the past because the zero block carries over the
// last non-Simplex block's timestamp, which may not lie in the future.
preSimplexBlocks := make([]*testInnerBlock, 0, lastNonSimplexHeight+1)
for h := uint64(0); h <= lastNonSimplexHeight; h++ {
preSimplexBlocks = append(preSimplexBlocks, &testInnerBlock{
Height_: h,
TS: time.Now().Add(-time.Duration(lastNonSimplexHeight-h+1) * time.Second),
Payload: []byte(fmt.Sprintf("pre-simplex block %d", h)),
})
}
lastNonSimplexBlock := preSimplexBlocks[len(preSimplexBlocks)-1]

net := newInMemNetwork(t)
t.Cleanup(net.stop)

storage := newStorageWithBlocks(t, preSimplexBlocks...)
storage2 := newStorageWithBlocks(t, preSimplexBlocks...)

// The VMs continue the pre-Simplex chain, so the first inner block they build sits right on
// top of the last non-Simplex block.
firstInstance := newInstanceWithVM(t, firstNodeID, storage, net, pChain, cops, lastNonSimplexBlock, newTestVMAtHeight(lastNonSimplexHeight+1))
secondInstance := newInstanceWithVM(t, secondNodeID, storage2, net, pChain, cops, lastNonSimplexBlock, newTestVMAtHeight(lastNonSimplexHeight+1))
net.register(firstNodeID, firstInstance)
net.register(secondNodeID, secondInstance)

require.NoError(t, firstInstance.Start(t.Context()))
require.NoError(t, secondInstance.Start(t.Context()))
t.Cleanup(firstInstance.Stop)
t.Cleanup(secondInstance.Stop)

// Both nodes commit the zero block and a few ordinary Simplex blocks on top of it.
const simplexBlocks = uint64(3) // zero block + 2 ordinary blocks
waitForNumBlocks(t, storage, zeroBlockSeq+simplexBlocks)
waitForNumBlocks(t, storage2, zeroBlockSeq+simplexBlocks)

// Both nodes agree on a zero block that points to the last non-Simplex inner block.
for _, s := range []*MockStorage{storage, storage2} {
zeroBlock, ok := s.blockAt(zeroBlockSeq)
require.True(t, ok)
require.Equal(t, metadata.BlockTypeZero, zeroBlock.Type())
require.Nil(t, zeroBlock.InnerBlock)

md := zeroBlock.Metadata.SimplexProtocolMetadata
require.Equal(t, common.Digest(lastNonSimplexBlock.Digest()), md.Prev)
require.Equal(t, zeroBlockSeq, md.Seq)
}
}

// requireTipIsSealing asserts whether the last block in storage is a sealing block.
func requireTipIsSealing(t *testing.T, storage *MockStorage, want bool) {
t.Helper()
Expand Down Expand Up @@ -588,10 +665,26 @@ func waitForSealingBlockCount(t *testing.T, storage *MockStorage, target int) {
// newStorageWithGenesis returns storage holding only the genesis block, the ledger every node
// here starts from.
func newStorageWithGenesis(t *testing.T, genesisBlock *testInnerBlock) *MockStorage {
t.Helper()
return newStorageWithBlocks(t, genesisBlock)
}

// newStorageWithBlocks returns storage holding the given non-Simplex blocks (a genesis block, and
// possibly further pre-Simplex blocks on top of it), indexed in order.
// A non-Simplex block carries no Simplex metadata, save for the sequence number the test storage
// indexes it by, which for these blocks equals its height.
func newStorageWithBlocks(t *testing.T, blocks ...*testInnerBlock) *MockStorage {
t.Helper()
storage := NewMockStorage(t)
genesis := &ParsedBlock{StateMachineBlock: metadata.StateMachineBlock{InnerBlock: genesisBlock}}
require.NoError(t, storage.Index(context.Background(), genesis, common.Finalization{}))
for _, inner := range blocks {
block := &ParsedBlock{StateMachineBlock: metadata.StateMachineBlock{
InnerBlock: inner,
Metadata: metadata.StateMachineMetadata{
SimplexProtocolMetadata: common.ProtocolMetadata{Seq: inner.Height()},
},
}}
require.NoError(t, storage.Index(context.Background(), block, common.Finalization{}))
}
return storage
}

Expand Down Expand Up @@ -729,6 +822,14 @@ func newTestVM() *testVM {
return vm
}

// newTestVMAtHeight returns a VM whose first built block has the given height, for a ledger that
// already holds blocks above the genesis block.
func newTestVMAtHeight(height uint64) *testVM {
vm := &testVM{}
vm.nextHeight.Store(height)
return vm
}

func (vm *testVM) pause() { vm.paused.Store(true) }
func (vm *testVM) resume() { vm.paused.Store(false) }

Expand Down
13 changes: 7 additions & 6 deletions msm/msm.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ var (
errNilBlock = errors.New("block is nil")
errInvalidPChainHeight = errors.New("invalid P-chain height")
errZeroBlockHasInnerBlock = errors.New("zero block must not have an inner block")
errZeroBlockInnerDigestMismatch = errors.New("zero block inner block digest does not match last non-Simplex inner block digest")
errZeroBlockPrevDigestMismatch = errors.New("zero block previous digest does not match last non-Simplex inner block digest")
errZeroBlockTimestampMismatch = errors.New("zero block timestamp does not match last non-Simplex inner block timestamp")
errPrevSealingBlockNotFinalized = errors.New("previous sealing block is not finalized")
errBlockDigestMismatch = errors.New("does not match proposed block digest")
Expand Down Expand Up @@ -772,9 +772,8 @@ func (sm *StateMachine) buildBlockZero(parentBlock StateMachineBlock, simplexMet
timestamp := sm.LastNonSimplexInnerBlock.Timestamp().UnixMilli()
simplexEpochInfo := constructSimplexZeroBlockSimplexEpochInfo(pChainHeight, validatorSet, prevVMBlockSeq)

md := simplexMetadata
md.Prev = sm.LastNonSimplexInnerBlock.Digest()
md.Seq = sm.LastNonSimplexInnerBlock.Height()
simplexMetadata.Prev = sm.LastNonSimplexInnerBlock.Digest()
simplexMetadata.Seq = sm.LastNonSimplexInnerBlock.Height() + 1

// The zero block carries over the parent's ICM epoch unchanged, just as it carries over the
// timestamp. If the parent is a genesis block that predates ICM, the carried-over epoch is empty,
Expand Down Expand Up @@ -849,8 +848,10 @@ func (sm *StateMachine) verifyBlockZero(block *StateMachineBlock, prevBlock Stat
if block.InnerBlock != nil {
return errZeroBlockHasInnerBlock
}
if prevBlock.InnerBlock.Digest() != sm.LastNonSimplexInnerBlock.Digest() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how was this passing before? prevBlock.InnerBlock is supposed to be nil after the if

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think because prevBlock.InnerBlock is sm.LastNonSimplexInnerBlock

return errZeroBlockInnerDigestMismatch

// The zero block must build upon the last non-Simplex block
if block.Metadata.SimplexProtocolMetadata.Prev != sm.LastNonSimplexInnerBlock.Digest() {
return errZeroBlockPrevDigestMismatch
}

// The timestamp must equal the last non-Simplex inner block's timestamp.
Expand Down
Loading
Loading