diff --git a/go.mod b/go.mod index 730b4c1..a2cf589 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,13 @@ module github.com/eigerco/strawberry go 1.22.5 -require github.com/ChainSafe/gossamer v0.9.0 +require ( + github.com/ChainSafe/gossamer v0.9.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index c25553e..e103966 100644 --- a/go.sum +++ b/go.sum @@ -6,7 +6,9 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/block/block.go b/internal/block/block.go index 2ccf300..a0336ed 100644 --- a/internal/block/block.go +++ b/internal/block/block.go @@ -8,8 +8,9 @@ type Block struct { // Extrinsic represents the block extrinsic data type Extrinsic struct { - ET []*TicketProof + ET *TicketExtrinsic EP *PreimageExtrinsic ED *DisputeExtrinsic EA *AssurancesExtrinsic + EG *GuaranteesExtrinsic } diff --git a/internal/block/guarantee.go b/internal/block/guarantee.go new file mode 100644 index 0000000..31c8309 --- /dev/null +++ b/internal/block/guarantee.go @@ -0,0 +1,79 @@ +package block + +import ( + "github.com/eigerco/strawberry/internal/crypto" + "github.com/eigerco/strawberry/internal/jamtime" +) + +// WorkResultError represents the type of error that occurred during work execution +type WorkResultError int + +// WorkResultError represents the possible errors for a work result +const ( + NoError WorkResultError = iota + OutOfGas // ∞: Out of gas error + Panic // ☇: Panic error + BadCode // BAD: Service's code not available + CodeTooLarge // BIG: Code available but exceeds maximum size +) + +// GuaranteesExtrinsic represents the E_G extrinsic +type GuaranteesExtrinsic struct { + Guarantees []Guarantee +} + +// Guarantee represents a single guarantee within the E_G extrinsic +type Guarantee struct { + WorkReport WorkReport // The work report being guaranteed + Credentials []CredentialSignature // The credentials proving the guarantee's validity + Timeslot jamtime.Timeslot // The timeslot when this guarantee was made +} + +// CredentialSignature represents a single signature within the credential +type CredentialSignature struct { + ValidatorIndex uint32 // Index of the validator providing this signature + Signature [crypto.Ed25519SignatureSize]byte // The Ed25519 signature +} + +// WorkReport represents a report of completed work +type WorkReport struct { + Specification WorkPackageSpecification + Context RefinementContext + CoreIndex uint16 // N_C + AuthorizerHash crypto.Hash // H + Output []byte // Y a set of octet strings + Results []WorkResult // r ∈ ⟦L⟧_1:I: Sequence of 1 to I work results +} + +// WorkPackageSpecification defines the specification of a work package +type WorkPackageSpecification struct { + Hash crypto.Hash // h ∈ H: Work package hash + Length uint32 // l ∈ N_L: Auditable work bundle length (N_L is the set of blob length values) + ErasureRoot crypto.Hash // u ∈ H: Erasure root + SegmentRoot crypto.Hash // e ∈ H: Segment root +} + +// RefinementContext provides context for the refinement process +type RefinementContext struct { + AnchorHeaderHash crypto.Hash // a ∈ H: Anchor header hash + AnchorPosteriorStateRoot crypto.Hash // s ∈ H: Anchor state root + AnchorPosteriorBeefyRoot crypto.Hash // b ∈ H: Anchor Beefy root + LookupAnchorHeaderHash crypto.Hash // l ∈ H: Lookup anchor hash + LookupAnchorTimeslot jamtime.Timeslot // t ∈ N_T: Lookup anchor timeslot + PrerequisiteHash *crypto.Hash // p ∈ H?: Optional prerequisite work package hash +} + +// WorkResult represents the result of a single work item +type WorkResult struct { + ServiceIndex uint32 // s ∈ N_S: Service index (N_S is the set of service indices) + CodeHash crypto.Hash // c ∈ H: Code hash + PayloadHash crypto.Hash // l ∈ H: Payload hash + GasRatio uint64 // g ∈ N_G: Gas prioritization ratio + Output WorkResultOutput // o ∈ Y ∪ J: Output or error (Y is the set of octet strings, J is the set of work execution errors) +} + +// WorkResultOutput represents either the successful output or an error from a work result +type WorkResultOutput struct { + Data []byte // Represents successful output (Y) + Error WorkResultError // Represents error output (J) +} diff --git a/internal/block/header.go b/internal/block/header.go index 6955d8c..1969d06 100644 --- a/internal/block/header.go +++ b/internal/block/header.go @@ -2,24 +2,24 @@ package block import ( "github.com/eigerco/strawberry/internal/crypto" - "github.com/eigerco/strawberry/internal/time" + "github.com/eigerco/strawberry/internal/jamtime" ) const NumberOfValidators uint16 = 1023 // Header as defined in the section 5 in the paper type Header struct { - ParentHash crypto.Hash // Hp - PriorStateRoot crypto.Hash // Hr - ExtrinsicHash crypto.Hash // Hx - TimeSlotIndex time.Timeslot // Ht - EpochMarker *EpochMarker // He - WinningTicketsMarker [time.TimeslotsPerEpoch]*Ticket // Hw - Verdicts []crypto.Hash // Hj - OffendersMarkers []crypto.Ed25519PublicKey // Ho, the culprit's and fault's public keys - BlockAuthorIndex uint16 // Hi - VRFSignature crypto.BandersnatchSignature // Hv - BlockSealSignature crypto.BandersnatchSignature // Hs + ParentHash crypto.Hash // Hp + PriorStateRoot crypto.Hash // Hr + ExtrinsicHash crypto.Hash // Hx + TimeSlotIndex jamtime.Timeslot // Ht + EpochMarker *EpochMarker // He + WinningTicketsMarker [jamtime.TimeslotsPerEpoch]*Ticket // Hw + Verdicts []crypto.Hash // Hj + OffendersMarkers []crypto.Ed25519PublicKey // Ho, the culprit's and fault's public keys + BlockAuthorIndex uint16 // Hi + VRFSignature crypto.BandersnatchSignature // Hv + BlockSealSignature crypto.BandersnatchSignature // Hs } // EpochMarker consists of epoch randomness and a sequence of diff --git a/internal/block/ticket.go b/internal/block/ticket.go index 5a94531..4cd9efd 100644 --- a/internal/block/ticket.go +++ b/internal/block/ticket.go @@ -18,3 +18,8 @@ type TicketProof struct { EntryIndex uint8 // r ∈ Nn (0, 1) Proof [ticketProofSize]byte // RingVRF proof } + +// TicketExtrinsic represents the E_T extrinsic +type TicketExtrinsic struct { + TicketProofs []TicketProof +} diff --git a/internal/crypto/keys.go b/internal/crypto/keys.go index 943a2bb..6bb80ed 100644 --- a/internal/crypto/keys.go +++ b/internal/crypto/keys.go @@ -2,7 +2,8 @@ package crypto import ( "crypto/ed25519" - "github.com/eigerco/strawberry/internal/time" + + "github.com/eigerco/strawberry/internal/jamtime" ) type Ed25519PublicKey ed25519.PublicKey @@ -11,4 +12,4 @@ type BlsKey [BLSSize]byte type BandersnatchKey [BandersnatchSize]byte type MetadataKey [MetadataSize]byte type RingCommitment [BandersnatchRingSize]byte -type EpochKeys [time.TimeslotsPerEpoch]BandersnatchKey +type EpochKeys [jamtime.TimeslotsPerEpoch]BandersnatchKey diff --git a/internal/jamtime/epoch.go b/internal/jamtime/epoch.go new file mode 100644 index 0000000..7d362d1 --- /dev/null +++ b/internal/jamtime/epoch.go @@ -0,0 +1,81 @@ +package jamtime + +import ( + "time" +) + +const ( + // MinEpoch represents the first epoch in the JAM protocol. + // It corresponds to the epoch containing the JAM Epoch start time + // (12:00pm on January 1, 2024 UTC). + MinEpoch Epoch = 0 + + // MaxEpoch represents the last possible epoch in the JAM protocol. + // It is calculated as the maximum value of Epoch (uint32) divided by + // TimeslotsPerEpoch. This ensures that the last epoch can contain + // a full complement of timeslots without overflowing. + MaxEpoch Epoch = ^Epoch(0) / TimeslotsPerEpoch + + // TimeslotsPerEpoch defines the number of timeslots in each epoch. + // In the JAM protocol, each epoch consists of exactly 600 timeslots, + // as specified in the JAM Graypaper. + TimeslotsPerEpoch = 600 + + // EpochDuration defines the total duration of each epoch. + // It is calculated by multiplying TimeslotsPerEpoch by TimeslotDuration, + // resulting in a duration of 1 hour per epoch. + EpochDuration = TimeslotsPerEpoch * TimeslotDuration +) + +// Epoch represents a JAM Epoch +type Epoch uint32 + +// FromEpoch creates a JamTime from an Epoch (start of the epoch) +func FromEpoch(e Epoch) JamTime { + return JamTime{Seconds: uint64(e) * uint64(EpochDuration.Seconds())} +} + +// CurrentEpoch returns the current epoch +func CurrentEpoch() Epoch { + now := Now() + return now.ToEpoch() +} + +// EpochStart returns the JamTime at the start of the epoch +func (e Epoch) EpochStart() JamTime { + return FromEpoch(e) +} + +// EpochEnd returns the JamTime at the end of the epoch +func (e Epoch) EpochEnd() (JamTime, error) { + if e == MaxEpoch { + // For the last epoch, we calculate its end based on the last timeslot + return FromTimeslot(MaxTimeslot), nil + } + + return FromEpoch(e + 1).Add(-time.Nanosecond) +} + +// NextEpoch returns the next epoch +func (e Epoch) NextEpoch() (Epoch, error) { + if e == MaxEpoch { + return e, ErrMaxEpochReached + } + return e + 1, nil +} + +// PreviousEpoch returns the previous epoch +func (e Epoch) PreviousEpoch() (Epoch, error) { + if e == MinEpoch { + return e, ErrMinEpochReached + } + return e - 1, nil +} + +// ValidateEpoch checks if a given Epoch is within the valid range +func ValidateEpoch(e Epoch) error { + if e > MaxEpoch { + return ErrEpochExceedsMaxJamTime + } + return nil +} diff --git a/internal/jamtime/epoch_test.go b/internal/jamtime/epoch_test.go new file mode 100644 index 0000000..daeb234 --- /dev/null +++ b/internal/jamtime/epoch_test.go @@ -0,0 +1,139 @@ +package jamtime + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestEpoch_FromEpoch(t *testing.T) { + t.Run("first epoch", func(t *testing.T) { + jt := FromEpoch(0) + assert.Equal(t, uint64(0), jt.Seconds) + }) + + t.Run("arbitrary epoch", func(t *testing.T) { + jt := FromEpoch(100) + expected := uint64(360000) // 100 * 3600 + assert.Equal(t, expected, jt.Seconds) + }) + + t.Run("last possible epoch", func(t *testing.T) { + jt := FromEpoch(7158278) + expected := uint64(25769800800) // 7158278 * 3600 + assert.Equal(t, expected, jt.Seconds) + }) +} + +func TestEpoch_ToEpoch(t *testing.T) { + t.Run("start of first epoch", func(t *testing.T) { + jt := JamTime{Seconds: 0} + epoch := jt.ToEpoch() + assert.Equal(t, Epoch(0), epoch) + }) + + t.Run("middle of arbitrary epoch", func(t *testing.T) { + jt := JamTime{Seconds: 3601} // 1 second into the second epoch + epoch := jt.ToEpoch() + assert.Equal(t, Epoch(1), epoch) + }) + + t.Run("end of last possible epoch", func(t *testing.T) { + jt := JamTime{Seconds: 25769803199} // Last second of the last epoch + epoch := jt.ToEpoch() + assert.Equal(t, Epoch(7158278), epoch) + }) +} + +func TestEpoch_CurrentEpoch(t *testing.T) { + currentEpoch := CurrentEpoch() + now := time.Now().UTC() + expectedEpoch := Epoch((now.Unix() - JamEpoch.Unix()) / 3600) + assert.Equal(t, expectedEpoch, currentEpoch) +} + +func TestEpoch_EpochStart(t *testing.T) { + t.Run("first epoch", func(t *testing.T) { + start := Epoch(0).EpochStart() + expected := uint64(0) + assert.Equal(t, expected, start.Seconds) + }) + + t.Run("arbitrary epoch", func(t *testing.T) { + start := Epoch(100).EpochStart() + expected := uint64(360000) // 100 * 3600 + assert.Equal(t, expected, start.Seconds) + }) +} + +func TestEpoch_EpochEnd(t *testing.T) { + t.Run("first epoch", func(t *testing.T) { + end, err := Epoch(0).EpochEnd() + assert.Nil(t, err) + expected := uint64(3599) + assert.Equal(t, expected, end.Seconds) + }) + + t.Run("arbitrary epoch", func(t *testing.T) { + end, err := Epoch(100).EpochEnd() + assert.Nil(t, err) + expected := uint64(363599) // (100 * 3600) + 3599 + assert.Equal(t, expected, end.Seconds) + }) + + t.Run("max epoch returns jam time for last timeslot", func(t *testing.T) { + jamTime, err := MaxEpoch.EpochEnd() + assert.Nil(t, err) + expected := FromTimeslot(MaxTimeslot) + assert.Equal(t, expected.Seconds, jamTime.Seconds) + }) +} + +func TestEpoch_NextEpoch(t *testing.T) { + t.Run("from first epoch", func(t *testing.T) { + next, err := Epoch(0).NextEpoch() + assert.NoError(t, err) + assert.Equal(t, Epoch(1), next) + }) + + t.Run("call to NextEpoch at MaxEpoch causes error", func(t *testing.T) { + _, err := MaxEpoch.NextEpoch() + assert.ErrorIs(t, err, ErrMaxEpochReached) + }) +} + +func TestEpoch_PreviousEpoch(t *testing.T) { + t.Run("from second epoch", func(t *testing.T) { + prev, err := Epoch(1).PreviousEpoch() + assert.NoError(t, err) + assert.Equal(t, Epoch(0), prev) + }) + + t.Run("call to PreviousEpoch at MinEpoch causes error", func(t *testing.T) { + _, err := MinEpoch.PreviousEpoch() + assert.ErrorIs(t, err, ErrMinEpochReached) + }) +} + +func TestEpoch_ValidateEpoch(t *testing.T) { + t.Run("valid epoch", func(t *testing.T) { + err := ValidateEpoch(1000) + assert.NoError(t, err) + }) + + t.Run("min valid epoch", func(t *testing.T) { + err := ValidateEpoch(MinEpoch) + assert.NoError(t, err) + }) + + t.Run("max valid epoch", func(t *testing.T) { + err := ValidateEpoch(MaxEpoch) + assert.NoError(t, err) + }) + + t.Run("Epoch too large", func(t *testing.T) { + err := ValidateEpoch(MaxEpoch + 1) + assert.ErrorIs(t, err, ErrEpochExceedsMaxJamTime) + }) +} diff --git a/internal/jamtime/errors.go b/internal/jamtime/errors.go new file mode 100644 index 0000000..d634c79 --- /dev/null +++ b/internal/jamtime/errors.go @@ -0,0 +1,44 @@ +package jamtime + +import "errors" + +var ( + // ErrBeforeJamEpoch is returned when attempting to create or manipulate a + // JamTime that represents a moment before the JAM Epoch (1200 UTC on January 1, 2024). + // This error indicates that the given time is outside the valid range for + // the JAM protocol. + ErrBeforeJamEpoch = errors.New("time is before JAM Epoch") + + // ErrAfterMaxJamTime is returned when attempting to create or manipulate a + // JamTime that represents a moment after the maximum representable time in the + // JAM protocol (typically around mid-August 2840). This error indicates that + // the given time exceeds the maximum value that can be represented within the + // protocol's time range. + ErrAfterMaxJamTime = errors.New("time is after maximum representable JAM time") + + // ErrMinEpochReached is returned when attempting to get the previous epoch + // from the minimum possible epoch value. + ErrMinEpochReached = errors.New("minimum epoch reached") + + // ErrMaxEpochReached is returned when attempting to get the next epoch + // from the maximum possible epoch value. + ErrMaxEpochReached = errors.New("maximum epoch reached") + + // ErrEpochExceedsMaxJamTime is returned when an epoch value exceeds the maximum + // representable time in the JAM system, typically during epoch calculations + // or conversions. + ErrEpochExceedsMaxJamTime = errors.New("epoch is after maximum representable JAM time") + + // ErrMinTimeslotReached is returned when attempting to get the previous timeslot + // from the minimum possible timeslot value. + ErrMinTimeslotReached = errors.New("minimum timeslot reached") + + // ErrMaxTimeslotReached is returned when attempting to get the next timeslot + // from the maximum possible timeslot value. + ErrMaxTimeslotReached = errors.New("maximum timeslot reached") + + // ErrTimeslotExceedsEpochLength is returned when a timeslot number is greater than + // or equal to the number of timeslots in an epoch. This typically occurs when + // converting between epochs and timeslots or when validating timeslot values. + ErrTimeslotExceedsEpochLength = errors.New("timeslot number exceeds epoch length") +) diff --git a/internal/jamtime/jamtime.go b/internal/jamtime/jamtime.go new file mode 100644 index 0000000..a650e11 --- /dev/null +++ b/internal/jamtime/jamtime.go @@ -0,0 +1,179 @@ +package jamtime + +import ( + "fmt" + "time" +) + +var now = time.Now + +// JamEpoch represents the start of the JAM Common Era +// 2024-01-01 12:00:00 +var JamEpoch = time.Date(2024, time.January, 1, 12, 0, 0, 0, time.UTC) + +// MaxRepresentableJamTime is the latest date and time that can be represented +// in the JAM protocol. It corresponds to the end of the last timeslot in the +// last epoch (2^32 - 1 timeslots after the JAM Epoch). This time is set to +// 23:59:59.999999999 UTC on August 15, 2840, as specified in the JAM Graypaper. +// Any attempt to represent a time beyond this will result in an error. +var MaxRepresentableJamTime = time.Date(2840, time.August, 15, 23, 59, 59, 999999999, time.UTC) + +// JamTime represents a time in the JAM Common Era +type JamTime struct { + src time.Time + Seconds uint64 +} + +// Now returns the current time as a JamTime +func Now() JamTime { + t := now() + seconds := t.Unix() - JamEpoch.Unix() + + return JamTime{src: t, Seconds: uint64(seconds)} +} + +// FromTime converts a standard time.Time to JamTime +func FromTime(t time.Time) (JamTime, error) { + if t.Before(JamEpoch) { + return JamTime{}, ErrBeforeJamEpoch + } + + if t.Equal(JamEpoch) { + return JamTime{Seconds: 0}, nil + } + + if t.After(MaxRepresentableJamTime) { + return JamTime{}, ErrAfterMaxJamTime + } + + seconds := t.Unix() - JamEpoch.Unix() + + return JamTime{src: t, Seconds: uint64(seconds)}, nil +} + +// EpochAndTimeslotToJamTime converts an Epoch and a timeslot within that epoch to JamTime +func EpochAndTimeslotToJamTime(e Epoch, timeslot Timeslot) (JamTime, error) { + if timeslot >= TimeslotsPerEpoch { + return JamTime{}, ErrTimeslotExceedsEpochLength + } + epochStart := FromEpoch(e) + return JamTime{Seconds: epochStart.Seconds + uint64(timeslot)*uint64(TimeslotDuration.Seconds())}, nil +} + +// ToTime converts a JamTime to a standard time.Time +func (jt JamTime) ToTime() time.Time { + if jt.src.IsZero() { + t := JamEpoch.Unix() + int64(jt.Seconds) + + return time.Unix(t, 0).UTC() + } + + return jt.src +} + +// FromSeconds creates a JamTime from the number of seconds since the JAM Epoch +func FromSeconds(seconds uint64) JamTime { + return JamTime{Seconds: seconds} +} + +// Before reports whether the time instant jt is before u +func (jt JamTime) Before(u JamTime) bool { + return jt.Seconds < u.Seconds +} + +// After reports whether the time instant jt is after u +func (jt JamTime) After(u JamTime) bool { + return jt.Seconds > u.Seconds +} + +// Equal reports whether jt and u represent the same time instant +func (jt JamTime) Equal(u JamTime) bool { + return jt.Seconds == u.Seconds +} + +// Add returns the time jt+d +func (jt JamTime) Add(d time.Duration) (JamTime, error) { + // Get JamTime back in time.Time representation + t := jt.ToTime() + t = t.Add(d) + + // Check for overflow after MaxRepresentableJamTime + if t.After(MaxRepresentableJamTime) { + return JamTime{}, ErrAfterMaxJamTime + } + + // Check for underflow past JamEpoch + if t.Before(JamEpoch) { + return JamTime{}, ErrBeforeJamEpoch + } + + return FromTime(t) +} + +// Sub returns the duration jt-u +func (jt JamTime) Sub(u JamTime) time.Duration { + return time.Duration(int64(jt.Seconds-u.Seconds)) * time.Second +} + +// IsInFutureTimeSlot checks if a given JamTime is in a future timeslot +func (jt JamTime) IsInFutureTimeSlot() bool { + return jt.ToTimeslot() > CurrentTimeslot() +} + +// ToTimeslot converts a JamTime to its corresponding Timeslot +func (jt JamTime) ToTimeslot() Timeslot { + return Timeslot(jt.Seconds / uint64(TimeslotDuration.Seconds())) +} + +// IsZero reports whether jt represents the zero time instant, +// IsZero is true when the date and time equal to 2024-01-01 12:00:00 +func (jt JamTime) IsZero() bool { + return jt.Seconds == 0 +} + +// MarshalJSON implements the json.Marshaler interface +func (jt JamTime) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, []byte(jt.ToTime().Format(time.RFC3339)))), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface +func (jt *JamTime) UnmarshalJSON(data []byte) error { + t, err := time.Parse(`"`+time.RFC3339+`"`, string(data)) + if err != nil { + return err + } + *jt, err = FromTime(t) + if err != nil { + return err + } + return nil +} + +// ToEpochAndTimeslot converts a JamTime to its Epoch and timeslot within that epoch +func (jt JamTime) ToEpochAndTimeslot() (Epoch, Timeslot) { + epoch := jt.ToEpoch() + timeslotInEpoch := uint32((jt.Seconds / uint64(TimeslotDuration.Seconds())) % TimeslotsPerEpoch) + return epoch, Timeslot(timeslotInEpoch) +} + +// IsInSameEpoch checks if two JamTimes are in the same epoch +func (jt JamTime) IsInSameEpoch(other JamTime) bool { + return jt.ToEpoch() == other.ToEpoch() +} + +// ToEpoch converts a JamTime to its corresponding Epoch +func (jt JamTime) ToEpoch() Epoch { + return Epoch(jt.Seconds / uint64(EpochDuration.Seconds())) +} + +// ValidateJamTime checks if a given time.Time is within the valid range for JamTime +// Returns nil if valid and non-nil err if the given time.Time is outside the valid range for JamTime +func ValidateJamTime(t time.Time) error { + if t.Before(JamEpoch) { + return ErrBeforeJamEpoch + } + if t.After(MaxRepresentableJamTime) { + return ErrAfterMaxJamTime + } + return nil +} diff --git a/internal/jamtime/jamtime_test.go b/internal/jamtime/jamtime_test.go new file mode 100644 index 0000000..5e6e82e --- /dev/null +++ b/internal/jamtime/jamtime_test.go @@ -0,0 +1,378 @@ +package jamtime + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJamTime_FromTime(t *testing.T) { + t.Run("successfully convert from time.Time to JamTime", func(t *testing.T) { + standardTime := time.Date(2025, time.March, 15, 12, 0, 0, 0, time.UTC) + jamTime, err := FromTime(standardTime) + assert.Nil(t, err) + convertedTime := jamTime.ToTime() + assert.True(t, standardTime.Equal(convertedTime)) + }) + + t.Run("converts correct JamEpoch to JamTime", func(t *testing.T) { + jamEpoch, err := FromTime(JamEpoch) + assert.Nil(t, err) + assert.EqualValues(t, 0, jamEpoch.Seconds) + }) + + t.Run("converts time between JamEpoch and MaxJamTime to JamTime correctly", func(t *testing.T) { + expected := MaxRepresentableJamTime.Add(-1 * time.Hour) + jt, err := FromTime(expected) + assert.Nil(t, err) + got := jt.ToTime() + assert.True(t, expected.Equal(got)) + }) + + t.Run("fails to convert time.Time past MaxJamTime to JamTime", func(t *testing.T) { + year3000 := time.Date(3000, time.March, 15, 12, 0, 0, 0, time.UTC) + jamTime, err := FromTime(year3000) + assert.NotNil(t, err) + + assert.True(t, jamTime.IsZero()) + }) + + t.Run("fails to convert time.Time from the past", func(t *testing.T) { + year2000 := time.Date(2000, time.March, 15, 12, 0, 0, 0, time.UTC) + jamTime, err := FromTime(year2000) + assert.NotNil(t, err) + assert.ErrorIs(t, err, ErrBeforeJamEpoch) + + assert.True(t, jamTime.IsZero()) + }) +} + +func TestJamTime_FromSeconds(t *testing.T) { + t.Run("successfully convert from seconds to JamTime", func(t *testing.T) { + secondsInYear := uint64(31_536_000) + jamTime := FromSeconds(secondsInYear) + expectedTime := JamEpoch.Add(time.Duration(secondsInYear) * time.Second) + + assert.True(t, jamTime.ToTime().Equal(expectedTime)) + }) +} + +func TestJamTime_ToTime(t *testing.T) { + t.Run("converts JamEpoch to and from time.Time", func(t *testing.T) { + start, err := FromTime(JamEpoch) + assert.Nil(t, err) + assert.EqualValues(t, 0, start.Seconds) + got := start.ToTime() + assert.Equal(t, JamEpoch, got) + }) + + t.Run("converts time.Time to jamTime and back", func(t *testing.T) { + in := time.Date(2024, 07, 27, 01, 01, 00, 00, time.UTC) + jt, err := FromTime(in) + assert.Nil(t, err) + + got := jt.ToTime() + assert.True(t, in.Equal(got)) + }) + + t.Run("converts MaxRepresentableJamTime to and from time.Time", func(t *testing.T) { + in := MaxRepresentableJamTime + start, err := FromTime(in) + assert.Nil(t, err) + + got := start.ToTime() + assert.Equal(t, MaxRepresentableJamTime, got) + }) +} + +func TestJamTimeComparison(t *testing.T) { + t.Run("non equal", func(t *testing.T) { + t1 := FromSeconds(1000) + t2 := FromSeconds(2000) + + assert.True(t, t1.Before(t2)) + assert.True(t, t2.After(t1)) + assert.False(t, t1.Equal(t2)) + }) + + t.Run("equal", func(t *testing.T) { + t1 := FromSeconds(1000) + t2 := FromSeconds(1000) + + assert.True(t, t1.Equal(t2)) + assert.True(t, t2.Equal(t1)) + }) +} + +func TestJamTimeArithmetic(t *testing.T) { + t.Run("adding jamtime", func(t *testing.T) { + t1 := FromSeconds(1000) + duration := 500 * time.Second + + t2, err := t1.Add(duration) + assert.Nil(t, err) + assert.False(t, t2.Equal(t1)) + assert.NotEqual(t, t2.Seconds, t1.Seconds) + }) + + t.Run("subbing time from jamtime epoch", func(t *testing.T) { + t1, err := FromTime(JamEpoch) + assert.Nil(t, err) + duration := time.Duration(-500 * time.Second) + + got, err := t1.Add(duration) + assert.NotNil(t, err) + assert.ErrorIs(t, err, ErrBeforeJamEpoch) + assert.True(t, got.IsZero()) + }) + + t.Run("adding past max jamtime", func(t *testing.T) { + t1, err := FromTime(MaxRepresentableJamTime) + assert.Nil(t, err) + fmt.Println(t1.ToTime()) + duration := time.Duration(500 * time.Second) + got, err := t1.Add(duration) + assert.NotNil(t, err) + assert.ErrorIs(t, err, ErrAfterMaxJamTime) + assert.True(t, got.IsZero()) + }) + + t.Run("subbing jamtime", func(t *testing.T) { + t1 := FromSeconds(1000) + t2 := FromSeconds(500) + + duration := t1.Sub(t2) + assert.Equal(t, time.Duration(500)*time.Second, duration) + }) +} + +func TestJamTime_MarshalJSON(t *testing.T) { + t.Run("successfully marshal to json", func(t *testing.T) { + jamTime := FromSeconds(1000) + jsonData, err := json.Marshal(jamTime) + require.NoError(t, err) + + expected := []byte(`"2024-01-01T12:16:40Z"`) + + assert.Equal(t, expected, jsonData) + }) +} + +func TestJamTime_UnmarshalJSON(t *testing.T) { + t.Run("successfully unmarshal jamtime", func(t *testing.T) { + jsonData := []byte(`"2024-01-01T12:00:00Z"`) + + var unmarshaledTime JamTime + err := json.Unmarshal(jsonData, &unmarshaledTime) + require.NoError(t, err) + + got := unmarshaledTime.ToTime() + + assert.True(t, got.Equal(JamEpoch)) + }) + + t.Run("successfully unmarshal jamtime in future", func(t *testing.T) { + jsonData := []byte(`"2024-01-01T12:00:01Z"`) + want := JamEpoch.Add(1 * time.Second) + + var unmarshaledTime JamTime + err := json.Unmarshal(jsonData, &unmarshaledTime) + require.NoError(t, err) + + got := unmarshaledTime.ToTime() + assert.True(t, got.Equal(want)) + }) + + t.Run("errors when unmarshalling unknown data structure", func(t *testing.T) { + jsonData := []byte(`asdasdasd`) + var unmarshaledTime JamTime + + err := unmarshaledTime.UnmarshalJSON(jsonData) + assert.Error(t, err) + + assert.EqualError(t, err, `parsing time "asdasdasd" as "\"2006-01-02T15:04:05Z07:00\"": cannot parse "asdasdasd" as "\""`) + }) +} + +func TestJamTimeFromToTimeslotConversion(t *testing.T) { + t.Run("convert jamtime to timeslot", func(t *testing.T) { + jamTime := FromSeconds(3600) // 10 minutes after JAM Epoch + timeslot := jamTime.ToTimeslot() + + expected := Timeslot(600) + assert.Equal(t, expected, timeslot) + }) + + t.Run("convert timeslot to jamtime", func(t *testing.T) { + slot := Timeslot(100) + + jamTime := FromTimeslot(slot) + expected := uint64(600) + assert.Equal(t, expected, jamTime.Seconds) + }) +} + +func TestJamTime_IsInFutureTimeSlot(t *testing.T) { + currentTime := Now() + pastTime, err := currentTime.Add(-5 * time.Minute) + assert.Nil(t, err) + futureTime, err := currentTime.Add(10 * time.Minute) + assert.Nil(t, err) + + assert.False(t, currentTime.IsInFutureTimeSlot()) + assert.False(t, pastTime.IsInFutureTimeSlot()) + assert.True(t, futureTime.IsInFutureTimeSlot()) +} + +func TestJamTime_ToEpoch(t *testing.T) { + t.Run("jamtime to epoch", func(t *testing.T) { + jamTime := FromSeconds(3600) // 1 hour after JAM Epoch + epoch := jamTime.ToEpoch() + expected := Epoch(1) + assert.Equal(t, expected, epoch) + }) +} + +func TestJamTech_FromEpoch(t *testing.T) { + t.Run("epoch to jamtime", func(t *testing.T) { + e := Epoch(1) + + convertedJamTime := FromEpoch(e) + + assert.Equal(t, uint64(3600), convertedJamTime.Seconds) + }) +} + +func TestEpochAndTimeslotConversion(t *testing.T) { + t.Run("successfully converts jamtime to epoch and timeslot", func(t *testing.T) { + jamTime := FromSeconds(3600) // 1 hour after JAM Epoch + + epoch, timeslot := jamTime.ToEpochAndTimeslot() + expectedEpoch := Epoch(1) + expectedTimeslot := Timeslot(0) + + assert.Equal(t, expectedEpoch, epoch) + assert.Equal(t, expectedTimeslot, timeslot) + }) + + t.Run("successfully converts epoch and timeslot to jamtime", func(t *testing.T) { + timeslot := Timeslot(1) + epoch := Epoch(1) + + jamTime, err := EpochAndTimeslotToJamTime(epoch, timeslot) + require.NoError(t, err) + + expected := uint64(3606) + + assert.Equal(t, expected, jamTime.Seconds) + }) + + t.Run("returns an error when timeslot is outside of accepted range", func(t *testing.T) { + timeslot := Timeslot(601) + epoch := Epoch(1) + + jamTime, err := EpochAndTimeslotToJamTime(epoch, timeslot) + assert.Error(t, err) + assert.ErrorIs(t, err, ErrTimeslotExceedsEpochLength) + assert.True(t, jamTime.IsZero()) + }) +} + +func TestValidateJamTime(t *testing.T) { + t.Run("today is valid", func(t *testing.T) { + now := time.Now() + + err := ValidateJamTime(now) + assert.NoError(t, err) + }) + + t.Run("the future should be valid", func(t *testing.T) { + validTime := time.Date(2500, time.July, 27, 0, 0, 0, 0, time.UTC) + + err := ValidateJamTime(validTime) + assert.NoError(t, err) + }) + + t.Run("far into the future should be invalid", func(t *testing.T) { + inValidTime := time.Date(2840, time.August, 31, 23, 59, 59, 999999999, time.UTC) + + err := ValidateJamTime(inValidTime) + assert.Error(t, err) + assert.ErrorIs(t, err, ErrAfterMaxJamTime) + }) + + t.Run("date before January 1st 2024 is invalid", func(t *testing.T) { + invalidTime := time.Date(2023, time.December, 31, 0, 0, 0, 0, time.UTC) + err := ValidateJamTime(invalidTime) + assert.Error(t, err) + assert.ErrorIs(t, err, ErrBeforeJamEpoch) + }) +} + +func TestJamTime_IsInSameEpoch(t *testing.T) { + t.Run("same epoch - beginning", func(t *testing.T) { + time1 := JamTime{Seconds: 0} + time2 := JamTime{Seconds: 3599} + + assert.True(t, time1.IsInSameEpoch(time2)) + }) + + t.Run("same epoch - middle", func(t *testing.T) { + time1 := JamTime{Seconds: 3600*100 + 1800} + time2 := JamTime{Seconds: 3600*100 + 3599} + + assert.True(t, time1.IsInSameEpoch(time2)) + }) + + t.Run("different epochs - consecutive", func(t *testing.T) { + time1 := JamTime{Seconds: 3599} + time2 := JamTime{Seconds: 3600} + + assert.False(t, time1.IsInSameEpoch(time2)) + }) + + t.Run("different epochs - far apart", func(t *testing.T) { + time1 := JamTime{Seconds: 3600 * 100} + time2 := JamTime{Seconds: 3600 * 200} + + assert.False(t, time1.IsInSameEpoch(time2)) + }) + + t.Run("same time", func(t *testing.T) { + time1 := JamTime{Seconds: 3600 * 50} + + assert.True(t, time1.IsInSameEpoch(time1)) + }) + + t.Run("epoch boundary - end of epoch", func(t *testing.T) { + time1 := JamTime{Seconds: 3600 - 1} + time2 := JamTime{Seconds: 3600} + + assert.False(t, time1.IsInSameEpoch(time2)) + }) + + t.Run("epoch boundary - start of epoch", func(t *testing.T) { + time1 := JamTime{Seconds: 3600} + time2 := JamTime{Seconds: 3600 + 1} + + assert.True(t, time1.IsInSameEpoch(time2)) + }) + + t.Run("max time value", func(t *testing.T) { + maxTime := JamTime{Seconds: ^uint64(0)} + almostMaxTime := JamTime{Seconds: ^uint64(0) - 3599} + + assert.False(t, maxTime.IsInSameEpoch(almostMaxTime)) + }) + + t.Run("zero and almost one epoch", func(t *testing.T) { + time1 := JamTime{Seconds: 0} + time2 := JamTime{Seconds: 3599} + + assert.True(t, time1.IsInSameEpoch(time2)) + }) +} diff --git a/internal/jamtime/timeslot.go b/internal/jamtime/timeslot.go new file mode 100644 index 0000000..bc89c7a --- /dev/null +++ b/internal/jamtime/timeslot.go @@ -0,0 +1,98 @@ +package jamtime + +import ( + "time" +) + +const ( + // MinTimeslot represents the first timeslot in the JAM protocol. + // It corresponds to the beginning of the JAM Epoch (12:00pm on January 1, 2024 UTC). + MinTimeslot Timeslot = 0 + + // MaxTimeslot represents the last possible timeslot in the JAM protocol. + // It is set to the maximum value of a uint32 (2^32 - 1), which allows + // the protocol to represent time up to mid-August 2840. + MaxTimeslot Timeslot = ^Timeslot(0) + + // TimeslotDuration defines the length of each timeslot in the JAM protocol. + // Each timeslot is exactly 6 seconds long, as specified in the JAM Graypaper. + // This constant duration is used for conversions between timeslots and actual time. + TimeslotDuration = 6 * time.Second +) + +// Timeslot represents a 6-second window in JAM time +type Timeslot uint32 + +// FromTimeslot creates a JamTime from a Timeslot (start of the timeslot) +func FromTimeslot(ts Timeslot) JamTime { + return JamTime{Seconds: uint64(ts) * uint64(TimeslotDuration.Seconds())} +} + +// CurrentTimeslot returns the current timeslot +func CurrentTimeslot() Timeslot { + now := Now() + return now.ToTimeslot() +} + +// IsInFutureTimeslot checks if a given Timeslot is in the future +func (ts Timeslot) IsInFuture() bool { + return ts > CurrentTimeslot() +} + +// TimeslotStart returns the JamTime at the start of the timeslot +func (ts Timeslot) TimeslotStart() JamTime { + return FromTimeslot(ts) +} + +// TimeslotEnd returns the JamTime at the end of the timeslot +func (ts Timeslot) TimeslotEnd() (JamTime, error) { + if ts == MaxTimeslot { + return JamTime{}, ErrMaxTimeslotReached + } + + nextTs := ts + 1 + jamTime := FromTimeslot(nextTs) + return jamTime.Add(-time.Nanosecond) +} + +// NextTimeslot returns the next timeslot +func (ts Timeslot) NextTimeslot() (Timeslot, error) { + if ts == MaxTimeslot { + return ts, ErrMaxTimeslotReached + } + return ts + 1, nil +} + +// PreviousTimeslot returns the previous timeslot +func (ts Timeslot) PreviousTimeslot() (Timeslot, error) { + if ts == MinTimeslot { + return ts, ErrMinTimeslotReached + } + return ts - 1, nil +} + +// TimeslotInEpoch returns the timeslot number within the epoch (0-599) +func (ts Timeslot) TimeslotInEpoch() uint32 { + return uint32(ts % TimeslotsPerEpoch) +} + +// IsFirstTimeslotInEpoch checks if the timeslot is the first in its epoch +func (ts Timeslot) IsFirstTimeslotInEpoch() bool { + return ts.TimeslotInEpoch() == 0 +} + +// IsLastTimeslotInEpoch checks if the timeslot is the last in its epoch +func (ts Timeslot) IsLastTimeslotInEpoch() bool { + return ts.TimeslotInEpoch() == TimeslotsPerEpoch-1 +} + +// ToEpoch converts a Timeslot to its corresponding Epoch +func (ts Timeslot) ToEpoch() Epoch { + return Epoch(ts / TimeslotsPerEpoch) +} + +// ValidateTimeslot checks if a given Timeslot is within the valid range +func ValidateTimeslot(ts Timeslot) error { + jamTime := FromTimeslot(ts) + return ValidateJamTime(jamTime.ToTime()) +} diff --git a/internal/jamtime/timeslot_test.go b/internal/jamtime/timeslot_test.go new file mode 100644 index 0000000..2f066a6 --- /dev/null +++ b/internal/jamtime/timeslot_test.go @@ -0,0 +1,246 @@ +package jamtime + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCurrentTimeslot(t *testing.T) { + t.Run("current timeslot is valid", func(t *testing.T) { + currentTimeslot := CurrentTimeslot() + err2 := ValidateTimeslot(currentTimeslot) + assert.NoError(t, err2) + }) +} + +func TestValidateTimeSlot(t *testing.T) { + t.Run("is valid timeslot", func(t *testing.T) { + validTimeslot := Timeslot(1000000) + err := ValidateTimeslot(validTimeslot) + assert.NoError(t, err) + }) + + t.Run("max timeslot is valid", func(t *testing.T) { + maxTimeslot := MaxTimeslot + + err := ValidateTimeslot(maxTimeslot) + assert.NoError(t, err) + }) +} + +func TestTimeSlot_IsInFuture(t *testing.T) { + t.Run("timeslot should be in the future", func(t *testing.T) { + validTime := time.Date(2500, time.July, 27, 0, 0, 0, 0, time.UTC) + + jamTime, err := FromTime(validTime) + assert.Nil(t, err) + ts := jamTime.ToTimeslot() + + isInFuture := ts.IsInFuture() + + assert.True(t, isInFuture) + }) + + t.Run("timeslot should be not in the future", func(t *testing.T) { + validTime := time.Date(2024, time.January, 27, 0, 0, 0, 0, time.UTC) + + jamTime, err := FromTime(validTime) + assert.Nil(t, err) + ts := jamTime.ToTimeslot() + + isInFuture := ts.IsInFuture() + + assert.False(t, isInFuture) + }) +} + +func TestTimeSlot_TimeslotStart(t *testing.T) { + t.Run("should be able to get to the start timeslot", func(t *testing.T) { + first := Timeslot(1) + want, err := FromTime(time.Date(2024, time.January, 01, 12, 00, 06, 0, time.UTC)) + assert.Nil(t, err) + + got := first.TimeslotStart() + assert.Equal(t, want.Seconds, got.Seconds) + }) +} + +func TestTimeSlot_TimeslotEnd(t *testing.T) { + t.Run("should be able to go to the end timeslot", func(t *testing.T) { + first := Timeslot(1) + want, err := FromTime(time.Date(2024, time.January, 01, 12, 00, 11, 0, time.UTC)) + assert.Nil(t, err) + + got, err := first.TimeslotEnd() + assert.NoError(t, err) + assert.Equal(t, want.Seconds, got.Seconds) + }) + + t.Run("if max time slot then we've already reached the end", func(t *testing.T) { + maxTimeslot := MaxTimeslot + + zeroJamTime, err := maxTimeslot.TimeslotEnd() + assert.Error(t, err) + assert.True(t, zeroJamTime.IsZero()) + assert.ErrorIs(t, err, ErrMaxTimeslotReached) + }) +} + +func TestTimeSlot_NextTimeslot(t *testing.T) { + t.Run("should get the next timeslot", func(t *testing.T) { + first := Timeslot(1) + got, err := first.NextTimeslot() + assert.NoError(t, err) + + expected := Timeslot(2) + + assert.Equal(t, expected, got) + }) + + t.Run("call to NextTimeslot at MaxTimeslot should error", func(t *testing.T) { + ts := MaxTimeslot + _, err := ts.NextTimeslot() + assert.ErrorIs(t, err, ErrMaxTimeslotReached) + }) +} + +func TestTimeSlot_PreviousTimeslot(t *testing.T) { + t.Run("should get the previous timeslot", func(t *testing.T) { + first := Timeslot(2) + got, err := first.PreviousTimeslot() + assert.NoError(t, err) + + expected := Timeslot(1) + + assert.Equal(t, expected, got) + }) + + t.Run("call to PreviousTimeslot at MinTimeslot should return error", func(t *testing.T) { + ts := MinTimeslot + _, err := ts.PreviousTimeslot() + assert.ErrorIs(t, err, ErrMinTimeslotReached) + }) +} + +func TestTimeSlot_TimeslotInEpoch(t *testing.T) { + t.Run("first timeslot of first epoch", func(t *testing.T) { + ts := Timeslot(0) + result := ts.TimeslotInEpoch() + + assert.Equal(t, uint32(0), result) + assert.True(t, ts.IsFirstTimeslotInEpoch()) + assert.False(t, ts.IsLastTimeslotInEpoch()) + }) + + t.Run("last timeslot of first epoch", func(t *testing.T) { + ts := Timeslot(599) + result := ts.TimeslotInEpoch() + + assert.Equal(t, uint32(599), result) + assert.False(t, ts.IsFirstTimeslotInEpoch()) + assert.True(t, ts.IsLastTimeslotInEpoch()) + }) + + t.Run("middle timeslot of first epoch", func(t *testing.T) { + ts := Timeslot(300) + result := ts.TimeslotInEpoch() + + assert.Equal(t, uint32(300), result) + assert.False(t, ts.IsFirstTimeslotInEpoch()) + assert.False(t, ts.IsLastTimeslotInEpoch()) + }) + + t.Run("first timeslot of second epoch", func(t *testing.T) { + ts := Timeslot(600) + result := ts.TimeslotInEpoch() + + assert.Equal(t, uint32(0), result) + assert.True(t, ts.IsFirstTimeslotInEpoch()) + assert.False(t, ts.IsLastTimeslotInEpoch()) + }) + + t.Run("random timeslot in a later epoch", func(t *testing.T) { + ts := Timeslot(123456) + result := ts.TimeslotInEpoch() + + assert.Equal(t, uint32(456), result) + assert.False(t, ts.IsFirstTimeslotInEpoch()) + assert.False(t, ts.IsLastTimeslotInEpoch()) + }) + + t.Run("Max timeslot", func(t *testing.T) { + ts := Timeslot(4294967295) // 2^32 - 1 + result := ts.TimeslotInEpoch() + assert.Equal(t, uint32(495), result) + assert.False(t, ts.IsFirstTimeslotInEpoch()) + assert.False(t, ts.IsLastTimeslotInEpoch()) + }) +} + +func TestTimeSlot_ToEpoch(t *testing.T) { + t.Run("first timeslot of first epoch", func(t *testing.T) { + ts := Timeslot(0) + epoch := ts.ToEpoch() + assert.Equal(t, Epoch(0), epoch) + }) + + t.Run("last timeslot of first epoch", func(t *testing.T) { + ts := Timeslot(599) + epoch := ts.ToEpoch() + assert.Equal(t, Epoch(0), epoch) + }) + + t.Run("first timeslot of second epoch", func(t *testing.T) { + ts := Timeslot(600) + epoch := ts.ToEpoch() + assert.Equal(t, Epoch(1), epoch) + }) + + t.Run("middle timeslot of arbitrary epoch", func(t *testing.T) { + ts := Timeslot(123456) + epoch := ts.ToEpoch() + assert.Equal(t, Epoch(205), epoch) + }) + + t.Run("last timeslot of arbitrary epoch", func(t *testing.T) { + ts := Timeslot(1199) + epoch := ts.ToEpoch() + assert.Equal(t, Epoch(1), epoch) + }) + + t.Run("first timeslot of last possible epoch", func(t *testing.T) { + ts := Timeslot(4294966800) // 7158278 * 600 + epoch := ts.ToEpoch() + assert.Equal(t, Epoch(7158278), epoch) + }) + + t.Run("last timeslot of last possible epoch", func(t *testing.T) { + ts := Timeslot(4294967295) // uint32 max + epoch := ts.ToEpoch() + assert.Equal(t, Epoch(7158278), epoch) + }) + + t.Run("epoch boundary check", func(t *testing.T) { + ts1 := Timeslot(599) + ts2 := Timeslot(600) + assert.NotEqual(t, ts1.ToEpoch(), ts2.ToEpoch()) + }) + + t.Run("large timeslot value", func(t *testing.T) { + ts := Timeslot(1000000) + epoch := ts.ToEpoch() + assert.Equal(t, Epoch(1666), epoch) + }) + + t.Run("consistency check with TimeslotInEpoch", func(t *testing.T) { + ts := Timeslot(12345) + epoch := ts.ToEpoch() + inEpoch := ts.TimeslotInEpoch() + + want := Timeslot(uint32(epoch)*600 + inEpoch) + + assert.Equal(t, ts, want) + }) +} diff --git a/internal/safrole/safrole.go b/internal/safrole/safrole.go index 9e0262c..14a9122 100644 --- a/internal/safrole/safrole.go +++ b/internal/safrole/safrole.go @@ -2,14 +2,15 @@ package safrole import ( "fmt" + "github.com/ChainSafe/gossamer/pkg/scale" "github.com/eigerco/strawberry/internal/block" "github.com/eigerco/strawberry/internal/crypto" - "github.com/eigerco/strawberry/internal/time" + "github.com/eigerco/strawberry/internal/jamtime" ) -type TicketsBodies [time.TimeslotsPerEpoch]block.Ticket -type TicketsMark [time.TimeslotsPerEpoch]block.Ticket +type TicketsBodies [jamtime.TimeslotsPerEpoch]block.Ticket +type TicketsMark [jamtime.TimeslotsPerEpoch]block.Ticket type CustomErrorCode int diff --git a/internal/time/constants.go b/internal/time/constants.go deleted file mode 100644 index 186c1e8..0000000 --- a/internal/time/constants.go +++ /dev/null @@ -1,9 +0,0 @@ -package time - -import "time" - -const ( - TimeslotsPerEpoch = 600 - TimeslotDuration = 6 * time.Second - EpochDuration = TimeslotsPerEpoch * TimeslotDuration -) diff --git a/internal/time/epoch.go b/internal/time/epoch.go deleted file mode 100644 index 9dc60b0..0000000 --- a/internal/time/epoch.go +++ /dev/null @@ -1,3 +0,0 @@ -package time - -type Epoch uint32 diff --git a/internal/time/timeslot.go b/internal/time/timeslot.go deleted file mode 100644 index 2b3b77e..0000000 --- a/internal/time/timeslot.go +++ /dev/null @@ -1,3 +0,0 @@ -package time - -type Timeslot uint32 diff --git a/pkg/serialization/codec/codec.go b/pkg/serialization/codec/codec.go new file mode 100644 index 0000000..d994723 --- /dev/null +++ b/pkg/serialization/codec/codec.go @@ -0,0 +1,6 @@ +package codec + +type Codec interface { + Marshal(v interface{}) ([]byte, error) + Unmarshal(data []byte, v interface{}) error +} diff --git a/pkg/serialization/codec/json_codec.go b/pkg/serialization/codec/json_codec.go new file mode 100644 index 0000000..fb5e78f --- /dev/null +++ b/pkg/serialization/codec/json_codec.go @@ -0,0 +1,16 @@ +package codec + +import ( + "encoding/json" +) + +// JSONCodec implements the Codec interface for JSON encoding and decoding. +type JSONCodec struct{} + +func (j *JSONCodec) Marshal(v interface{}) ([]byte, error) { + return json.Marshal(v) +} + +func (j *JSONCodec) Unmarshal(data []byte, v interface{}) error { + return json.Unmarshal(data, v) +} diff --git a/pkg/serialization/codec/scale_codec.go b/pkg/serialization/codec/scale_codec.go new file mode 100644 index 0000000..e9e101a --- /dev/null +++ b/pkg/serialization/codec/scale_codec.go @@ -0,0 +1,14 @@ +package codec + +import "github.com/ChainSafe/gossamer/pkg/scale" + +// SCALECodec implements the Codec interface for SCALE encoding and decoding. +type SCALECodec struct{} + +func (s *SCALECodec) Marshal(v interface{}) ([]byte, error) { + return scale.Marshal(v) +} + +func (s *SCALECodec) Unmarshal(data []byte, v interface{}) error { + return scale.Unmarshal(data, v) +} diff --git a/pkg/serialization/serializer.go b/pkg/serialization/serializer.go new file mode 100644 index 0000000..41f2982 --- /dev/null +++ b/pkg/serialization/serializer.go @@ -0,0 +1,23 @@ +package serialization + +import "github.com/eigerco/strawberry/pkg/serialization/codec" + +// Serializer provides methods to encode and decode using a specified codec. +type Serializer struct { + codec codec.Codec +} + +// NewSerializer initializes a new Serializer with the given codec. +func NewSerializer(c codec.Codec) *Serializer { + return &Serializer{codec: c} +} + +// Encode serializes the given value using the codec. +func (s *Serializer) Encode(v interface{}) ([]byte, error) { + return s.codec.Marshal(v) +} + +// Decode deserializes the given data into the specified value using the codec. +func (s *Serializer) Decode(data []byte, v interface{}) error { + return s.codec.Unmarshal(data, v) +} diff --git a/pkg/serialization/serializer_test.go b/pkg/serialization/serializer_test.go new file mode 100644 index 0000000..6bf1865 --- /dev/null +++ b/pkg/serialization/serializer_test.go @@ -0,0 +1,52 @@ +package serialization_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/eigerco/strawberry/pkg/serialization" + "github.com/eigerco/strawberry/pkg/serialization/codec" +) + +type PayloadExample struct { + ID int `json:"id"` + Data []byte `json:"data"` +} + +func TestJSONSerializer(t *testing.T) { + jsonCodec := &codec.JSONCodec{} + serializer := serialization.NewSerializer(jsonCodec) + + example := PayloadExample{ID: 1, Data: []byte{1, 2, 3}} + + // Test Encoding + encoded, err := serializer.Encode(&example) + require.NoError(t, err) + require.NotNil(t, encoded) + + // Test Decoding + var decoded PayloadExample + err = serializer.Decode(encoded, &decoded) + require.NoError(t, err) + assert.Equal(t, example, decoded) +} + +func TestSCALESerializer(t *testing.T) { + scaleCodec := &codec.SCALECodec{} + serializer := serialization.NewSerializer(scaleCodec) + + example := PayloadExample{ID: 2, Data: []byte{1, 2, 3}} + + // Test Encoding + encoded, err := serializer.Encode(example) + require.NoError(t, err) + require.NotNil(t, encoded) + + // Test Decoding + var decoded PayloadExample + err = serializer.Decode(encoded, &decoded) + require.NoError(t, err) + assert.Equal(t, example, decoded) +}