PIP-5: Change in SprintLength

PIP-5: Change in SprintLength

Authors:

Sandeep Sreenath
Paul O’Leary
Arpit Temani

Status : Final

Motivation

Block reorgs are possible on the Polygon POS chain as the consensus mechanism bor uses is probabilistic - meaning that finality is eventual and typically based on the number of confirmations layered on top of the block holding your transaction. Although there has been a reduction in the frequency of reorgs with the introduction of the BDN, it is still prevalent and a cause for concern among DApp developers.

We have observed that reorg length is a function of sprint length. If a primary producer is down for some time and comes back online, then it will only affect one sprint as the primary producer will change for the next sprint.

Based on the above, we propose a decrease in the sprint length from 64 to 16 blocks. This means that a block producer produces blocks continuously for much lower time as compared with the current 128 sec. This will help a great deal in reducing the frequency and depth of reorgs. This doesn’t affect the total time/no of blocks a validator is producing over a span and hence there would be no change in the rewards overall.

Specification

Calculating what’s the sprint length to use

func (c *BorConfig) CalculateSprint(number uint64) uint64 {
	return c.calculateSprintSizeHelper(c.Sprint, number)
}
func (c *BorConfig) calculateSprintSizeHelper(field map[string]uint64, number uint64) uint64 {
	keys := make([]string, 0, len(field))
	for k := range field {
		keys = append(keys, k)
	}

	sort.Strings(keys)

	for i := 0; i < len(keys)-1; i++ {
		valUint, _ := strconv.ParseUint(keys[i], 10, 64)
		valUintNext, _ := strconv.ParseUint(keys[i+1], 10, 64)

		if number >= valUint && number < valUintNext {
			return field[keys[i]]
		}
	}

	return field[keys[len(keys)-1]]
}

Calculating sprint end block

isSprintEnd := IsSprintStart(number+1, c.config.Sprint)

New sprint length, producer delay types

// BorConfig is the consensus engine configs for Matic bor based sealing.
type BorConfig struct {
	Period                   map[string]uint64      `json:"period"`                   // Number of seconds between blocks to enforce
	ProducerDelay            map[string]uint64      `json:"producerDelay"`            // Number of seconds delay between two producer interval
	Sprint                   map[string]uint64      `json:"sprint"`                   // Epoch length to proposer
	BackupMultiplier         map[string]uint64      `json:"backupMultiplier"`         // Backup multiplier to determine the wiggle time
	ValidatorContract        string                 `json:"validatorContract"`        // Validator set contract
	StateReceiverContract    string                 `json:"stateReceiverContract"`    // State receiver contract
	OverrideStateSyncRecords map[string]int         `json:"overrideStateSyncRecords"` // override state records count
	BlockAlloc               map[string]interface{} `json:"blockAlloc"`
	BurntContract            map[string]string      `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork
	JaipurBlock              *big.Int               `json:"jaipurBlock"`   // Jaipur switch block (nil = no fork, 0 = already on jaipur)
	DelhiBlock               *big.Int               `json:"delhiBlock"`    // Delhi switch block (nil = no fork, 0 = already on delhi)
}

Test Cases

func TestSprintLengths(t *testing.T) {
	t.Parallel()

	testBorConfig := params.TestChainConfig.Bor
	testBorConfig.Sprint = map[string]uint64{
		"0": 16,
		"8": 4,
	}
	assert.Equal(t, testBorConfig.CalculateSprint(0), uint64(16))
	assert.Equal(t, testBorConfig.CalculateSprint(8), uint64(4))
	assert.Equal(t, testBorConfig.CalculateSprint(9), uint64(4))
}
func TestSprintLengthReorg(t *testing.T) {
	t.Parallel()

	reorgsLengthTests := getTestSprintLengthReorgCases()
	f, err := os.Create("sprintReorg.csv")

	defer func() {
		err = f.Close()

		if err != nil {
			panic(err)
		}
	}()

	if err != nil {
		_log.Fatalln("failed to open file", err)
	}

	w := csv.NewWriter(f)
	err = w.Write([]string{"Induced Reorg Length", "Start Block", "Sprint Size", "Disconnected Node Id", "Disconnected Node Id's Reorg Length", "Observer Node Id's Reorg Length"})
	w.Flush()

	if err != nil {
		panic(err)
	}

	var wg sync.WaitGroup

	for index, tt := range reorgsLengthTests {
		if index%4 == 0 {
			wg.Wait()
		}

		wg.Add(1)

		go SprintLengthReorgIndividualHelper(t, index, tt, w, &wg)
	}
}

References

Forum Post:

3 Likes