diff --git a/types/extrinsic_era.go b/types/extrinsic_era.go index 50df12542..a929ea2b2 100644 --- a/types/extrinsic_era.go +++ b/types/extrinsic_era.go @@ -17,6 +17,9 @@ package types import ( + "math" + "math/bits" + "github.com/centrifuge/go-substrate-rpc-client/v3/scale" ) @@ -74,3 +77,26 @@ type MortalEra struct { First byte Second byte } + +// Mortal describes a mortal era based on a period of validity and a block number on which it should start +func Mortal(validityPeriod, currentBlock uint64) (period, phase uint64) { + calPeriod := math.Pow(2, math.Ceil(math.Log2(float64(validityPeriod)))) + period = uint64(math.Min(math.Max(calPeriod, 4), 1<<16)) + + quantizeFactor := math.Max(float64(period>>12), 1) + quantizedPhase := float64(currentBlock%period) / quantizeFactor * quantizeFactor + + phase = uint64(quantizedPhase) + + return +} + +// NewMortalEra encodes a mortal era based on period and phase +func NewMortalEra(period, phase uint64) MortalEra { + quantizeFactor := math.Max(float64(period>>12), 1) + + trailingZeros := bits.TrailingZeros16(uint16(period)) + encoded := uint16(float64(phase)/quantizeFactor)<<4 | uint16(math.Min(15, math.Max(1, float64(trailingZeros-1)))) + + return MortalEra{First: byte(encoded & 0xff), Second: byte(encoded >> 8)} +} diff --git a/types/extrinsic_era_test.go b/types/extrinsic_era_test.go index 83b99f596..80d4f2b7c 100644 --- a/types/extrinsic_era_test.go +++ b/types/extrinsic_era_test.go @@ -45,3 +45,71 @@ func TestExtrinsicEra_EncodeDecode(t *testing.T) { assert.NoError(t, err) assertRoundtrip(t, e) } + +func TestExtrinsicEra_MortalEraEncoding(t *testing.T) { + testCases := []struct { + Current uint64 + ValidityPeriod uint64 + Era MortalEra + }{ + { + Current: 2251516, + ValidityPeriod: 64, + Era: MortalEra{First: 0xc5, Second: 0x03}, + }, + } + + for _, testCase := range testCases { + period, phase := Mortal(testCase.ValidityPeriod, testCase.Current) + + era := NewMortalEra(period, phase) + assert.Equal(t, testCase.Era.First, era.First) + assert.Equal(t, testCase.Era.Second, era.Second) + } +} + +func TestMortal(t *testing.T) { + period, phase := Mortal(200, 1400) + assert.Equal(t, uint64(120), phase) + assert.Equal(t, uint64(256), period) +} + +func TestNewMortalEra(t *testing.T) { + testCases := []struct { + Period uint64 + Phase uint64 + Era MortalEra + }{ + { + Period: 128, + Phase: 125, + Era: MortalEra{First: 0xd6, Second: 0x07}, + }, + { + Period: 4096, + Phase: 3641, + Era: MortalEra{First: 0x9b, Second: 0xe3}, + }, + { + Period: 64, + Phase: 38, + Era: MortalEra{First: 0x65, Second: 0x02}, + }, + { + Period: 64, + Phase: 40, + Era: MortalEra{First: 0x85, Second: 0x02}, + }, + { + Period: 32768, + Phase: 20000, + Era: MortalEra{First: 78, Second: 156}, + }, + } + + for _, testCase := range testCases { + era := NewMortalEra(testCase.Period, testCase.Phase) + assert.Equal(t, testCase.Era.First, era.First) + assert.Equal(t, testCase.Era.Second, era.Second) + } +}