Skip to content
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

State serialization to 0.5.0 #147

Merged
merged 6 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 13 additions & 0 deletions internal/service/service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package service

import (
"encoding/binary"
"github.com/eigerco/strawberry/internal/block"
"github.com/eigerco/strawberry/internal/crypto"
"github.com/eigerco/strawberry/internal/jamtime"
Expand Down Expand Up @@ -82,6 +83,18 @@ type PreImageMetaKey struct {
}
type PreimageHistoricalTimeslots []jamtime.Timeslot // Metadata for preimages (l) - TODO: There is a MaxHistoricalTimeslotsForPreimageMeta.

func (p PreimageHistoricalTimeslots) ToByteSlice() []byte {
// Allocate a byte slice large enough to hold all Timeslot values
output := make([]byte, len(p)*4)

// Convert each Timeslot to bytes and copy into the output slice
for i, ts := range p {
binary.BigEndian.PutUint32(output[i*4:], uint32(ts))
}

return output
}

type Memo [TransferMemoSizeBytes]byte

// DeferredTransfer Equation 161: T ≡ (s ∈ Ns, d ∈ Ns, a ∈ Nb, m ∈ Ym, g ∈ Ng)
Expand Down
12 changes: 8 additions & 4 deletions internal/state/merkle/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package state

import (
"crypto/ed25519"
"encoding/binary"
"errors"
"fmt"
"github.com/eigerco/strawberry/internal/state"
Expand Down Expand Up @@ -282,7 +281,8 @@ func RandomSafroleStateWithEpochKeys(t *testing.T) safrole.State {
func RandomState(t *testing.T) state.State {
services := make(service.ServiceState)
for i := 0; i < 10; i++ {
services[block.ServiceId(789)] = RandomServiceAccount(t)
// Use different service IDs for each iteration
services[block.ServiceId(uint32(i+789))] = RandomServiceAccount(t)
}

return state.State{
Expand Down Expand Up @@ -451,6 +451,10 @@ func isServiceAccountKey(stateKey crypto.Hash) bool {
}

func extractServiceIdFromKey(stateKey crypto.Hash) block.ServiceId {
// Assuming that the service ID is embedded in bytes 1-4 of the key
return block.ServiceId(binary.BigEndian.Uint32(stateKey[1:5]))
// Since we're using zero-interleaved pattern, we need to reconstruct the service ID
// from bytes at positions 1,3,5,7
return block.ServiceId(uint32(stateKey[1])<<24 |
uint32(stateKey[3])<<16 |
uint32(stateKey[5])<<8 |
uint32(stateKey[7]))
}
27 changes: 22 additions & 5 deletions internal/state/merkle/serialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/eigerco/strawberry/internal/service"
"github.com/eigerco/strawberry/internal/state"
"github.com/eigerco/strawberry/pkg/serialization/codec/jam"
"math"
)

// SerializeState serializes the given state into a map of crypto.Hash to byte arrays, for merklization.
Expand Down Expand Up @@ -148,7 +149,7 @@ func serializeServiceAccount(serviceId block.ServiceId, serviceAccount service.S
encodedFootprintSize,
encodedFootprintItems,
)
stateKey := generateStateKey(255, serviceId)
stateKey := generateStateKeyInterleavedBasic(255, serviceId)
serializedState[stateKey] = combined

// Serialize storage and preimage items
Expand All @@ -160,21 +161,37 @@ func serializeServiceAccount(serviceId block.ServiceId, serviceAccount service.S
}

func serializeStorageAndPreimage(serviceId block.ServiceId, serviceAccount service.ServiceAccount, serializedState map[crypto.Hash][]byte) error {
encodedMaxUint32, err := jam.Marshal(math.MaxUint32)
if err != nil {
return err
}
for hash, value := range serviceAccount.Storage {
encodedValue, err := jam.Marshal(value)
if err != nil {
return err
}
stateKey := generateStateKeyInterleaved(serviceId, hash)

var combined [32]byte
copy(combined[:4], encodedMaxUint32)
copy(combined[4:], hash[:28])
stateKey := generateStateKeyInterleaved(serviceId, combined)
serializedState[stateKey] = encodedValue
}

encodedMaxUint32MinusOne, err := jam.Marshal(math.MaxUint32 - 1)
if err != nil {
return err
}
for hash, value := range serviceAccount.PreimageLookup {
encodedValue, err := jam.Marshal(value)
if err != nil {
return err
}
stateKey := generateStateKeyInterleaved(serviceId, hash)

var combined [32]byte
copy(combined[:4], encodedMaxUint32MinusOne)
copy(combined[4:], hash[1:29])
stateKey := generateStateKeyInterleaved(serviceId, combined)
serializedState[stateKey] = encodedValue
}

Expand All @@ -187,11 +204,11 @@ func serializeStorageAndPreimage(serviceId block.ServiceId, serviceAccount servi
if err != nil {
return err
}
hashedPreImageHistoricalTimeslots := crypto.HashData(preImageHistoricalTimeslots.ToByteSlice())

var combined [32]byte
copy(combined[:4], encodedLength)
hashNotFirst4Bytes := bitwiseNotExceptFirst4Bytes(key.Hash)
copy(combined[4:], hashNotFirst4Bytes[:])
copy(combined[4:], hashedPreImageHistoricalTimeslots[2:30])
stateKey := generateStateKeyInterleaved(serviceId, key.Hash)
serializedState[stateKey] = encodedPreImageHistoricalTimeslots
}
Expand Down
109 changes: 25 additions & 84 deletions internal/state/merkle/serialization_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package state

import (
"fmt"
"github.com/eigerco/strawberry/internal/crypto"
"github.com/eigerco/strawberry/internal/safrole"
"github.com/eigerco/strawberry/pkg/serialization/codec/jam"
Expand All @@ -11,67 +10,41 @@ import (
)

func TestSerializeState(t *testing.T) {
// Step 1: Generate random state and serialize it
// Generate random state
state := RandomState(t)

// Serialize and log serialized keys
encodedState, err := SerializeState(state)
require.NoError(t, err)

// Step 2: Deserialize the serialized state
// Deserialize and check results
decodedState, err := DeserializeState(encodedState)
assert.NoError(t, err)
assert.NotEmpty(t, decodedState)

// Step 3: Compare the deserialized state with the original state

// Compare CoreAuthorizersPool
assert.Equal(t, state.CoreAuthorizersPool, decodedState.CoreAuthorizersPool, "CoreAuthorizersPool mismatch")

// Compare PendingAuthorizersQueues
assert.Equal(t, state.PendingAuthorizersQueues, decodedState.PendingAuthorizersQueues, "PendingAuthorizersQueues mismatch")

// Compare RecentBlocks
assert.Equal(t, state.RecentBlocks, decodedState.RecentBlocks, "RecentBlocks mismatch")

// Compare ValidatorState fields
assert.Equal(t, state.ValidatorState.SafroleState.NextValidators, decodedState.ValidatorState.SafroleState.NextValidators, "NextValidators mismatch")
assert.Equal(t, state.ValidatorState.CurrentValidators, decodedState.ValidatorState.CurrentValidators, "CurrentValidators mismatch")
assert.Equal(t, state.ValidatorState.QueuedValidators, decodedState.ValidatorState.QueuedValidators, "FutureValidators mismatch")
assert.Equal(t, state.ValidatorState.ArchivedValidators, decodedState.ValidatorState.ArchivedValidators, "PreviousValidators mismatch")
assert.Equal(t, state.ValidatorState.SafroleState.RingCommitment, decodedState.ValidatorState.SafroleState.RingCommitment, "RingCommitment mismatch")

// Ensure SealingKeySeries is correctly deserialized
assert.Equal(t, state.ValidatorState.SafroleState.SealingKeySeries, decodedState.ValidatorState.SafroleState.SealingKeySeries, "SealingKeySeries mismatch")

// Compare TicketAccumulator
assert.Equal(t, state.ValidatorState.SafroleState.TicketAccumulator, decodedState.ValidatorState.SafroleState.TicketAccumulator, "TicketAccumulator mismatch")

// Compare EntropyPool
assert.Equal(t, state.EntropyPool, decodedState.EntropyPool, "EntropyPool mismatch")

// Compare CoreAssignments
assert.Equal(t, state.CoreAssignments, decodedState.CoreAssignments, "CoreAssignments mismatch")

// Compare TimeslotIndex
assert.Equal(t, state.TimeslotIndex, decodedState.TimeslotIndex, "TimeslotIndex mismatch")

// Compare PrivilegedServices
assert.Equal(t, state.PrivilegedServices, decodedState.PrivilegedServices, "PrivilegedServices mismatch")
require.NoError(t, err)

// Compare ValidatorStatistics
assert.Equal(t, state.ValidatorStatistics, decodedState.ValidatorStatistics, "ValidatorStatistics mismatch")
// Compare services
assert.Equal(t, len(state.Services), len(decodedState.Services),
"Service map length mismatch (Original: %d, Decoded: %d)",
len(state.Services), len(decodedState.Services))

// Compare Services
assert.Equal(t, len(state.Services), len(decodedState.Services), "Service map length mismatch")
for serviceID, originalService := range state.Services {
decodedService, exists := decodedState.Services[serviceID]
require.True(t, exists, fmt.Sprintf("ServiceID %d missing in decoded state", serviceID))
if !exists {
t.Errorf("Service ID %d missing in decoded state. Original service details: %+v",
serviceID, originalService)
continue
}

// Compare individual fields in ServiceAccount
assert.Equal(t, originalService.CodeHash, decodedService.CodeHash, fmt.Sprintf("Mismatch in CodeHash for ServiceID %d", serviceID))
assert.Equal(t, originalService.Balance, decodedService.Balance, fmt.Sprintf("Mismatch in Balance for ServiceID %d", serviceID))
assert.Equal(t, originalService.GasLimitForAccumulator, decodedService.GasLimitForAccumulator, fmt.Sprintf("Mismatch in GasLimitForAccumulator for ServiceID %d", serviceID))
assert.Equal(t, originalService.GasLimitOnTransfer, decodedService.GasLimitOnTransfer, fmt.Sprintf("Mismatch in GasLimitOnTransfer for ServiceID %d", serviceID))
assert.Equal(t, originalService.CodeHash, decodedService.CodeHash)
assert.Equal(t, originalService.Balance, decodedService.Balance)
assert.Equal(t, originalService.GasLimitForAccumulator, decodedService.GasLimitForAccumulator)
assert.Equal(t, originalService.GasLimitOnTransfer, decodedService.GasLimitOnTransfer)
}

// Check for extra services in decoded state
for serviceID := range decodedState.Services {
if _, exists := state.Services[serviceID]; !exists {
t.Errorf("Extra service ID %d found in decoded state", serviceID)
}
}

// Compare Past Judgements
Expand Down Expand Up @@ -305,41 +278,9 @@ func TestSerializeStateServices(t *testing.T) {
require.NoError(t, err)

for serviceId := range state.Services {
stateKey := generateStateKey(255, serviceId)
stateKey := generateStateKeyInterleavedBasic(255, serviceId)
hashKey := crypto.Hash(stateKey)
assert.Contains(t, serializedState, hashKey)
assert.NotEmpty(t, serializedState[hashKey])
}
}

// TestSerializeStateStorage checks the serialization of storage items within services.
func TestSerializeStateStorage(t *testing.T) {
state := RandomState(t)
serializedState, err := SerializeState(state)
require.NoError(t, err)

for serviceId, serviceAccount := range state.Services {
for hash := range serviceAccount.Storage {
stateKey := generateStateKeyInterleaved(serviceId, hash)
hashKey := crypto.Hash(stateKey)
assert.Contains(t, serializedState, hashKey)
assert.NotEmpty(t, serializedState[hashKey])
}
}
}

// TestSerializeStatePreimageMeta checks the serialization of the preimage metadata within services.
func TestSerializeStatePreimageMeta(t *testing.T) {
state := RandomState(t)
serializedState, err := SerializeState(state)
require.NoError(t, err)

for serviceId, serviceAccount := range state.Services {
for key := range serviceAccount.PreimageMeta {
stateKey := generateStateKeyInterleaved(serviceId, key.Hash)
hashKey := crypto.Hash(stateKey)
assert.Contains(t, serializedState, hashKey)
assert.NotEmpty(t, serializedState[hashKey])
}
}
}
31 changes: 8 additions & 23 deletions internal/state/merkle/serialization_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,20 @@ func generateStateKeyBasic(i uint8) [32]byte {
return result
}

// generateStateKey to generate state key based on i and s
func generateStateKey(i uint8, s block.ServiceId) [32]byte {
// generateStateKeyInterleavedBasic to generate state key based on i and s
func generateStateKeyInterleavedBasic(i uint8, s block.ServiceId) [32]byte {
var result [32]byte

// Place i as the first byte
result[0] = i

// Convert s into a 4-byte buffer and place it starting at result[1]
sBuf := make([]byte, 4)
binary.BigEndian.PutUint32(sBuf, uint32(s)) // s is 4 bytes in BigEndian format

// Copy the 4-byte sBuf to result starting at index 1
copy(result[1:], sBuf)
// Extract individual bytes from s using bit shifting
result[1] = byte(s >> 24) // n0
result[3] = byte(s >> 16) // n1
result[5] = byte(s >> 8) // n2
result[7] = byte(s) // n3

// The rest of result is already zero-padded by default
// result[2,4,6,8] and the rest are already 0 by default
return result
}

Expand Down Expand Up @@ -112,17 +111,3 @@ func sortByteSlicesCopy(slice interface{}) interface{} {
panic("unsupported type for sorting")
}
}

// bitwiseNotExceptFirst4Bytes to apply bitwise NOT to all bytes except the first 4
func bitwiseNotExceptFirst4Bytes(h crypto.Hash) [28]byte {
// Clone the original array into a new one
var result [28]byte
copy(result[:], h[:])

// Apply bitwise NOT to all bytes except the first 4
for i := 4; i < len(result); i++ {
result[i] = ^result[i]
}

return result
}
Loading
Loading