owned this note
owned this note
Published
Linked with GitHub
# Staking Command Center
## Context
With AHM, the workflow of the staking system will change. The "user interactions" (nominate, bond, etc) are by and large the same, yet some details outlined below will change:
On the relay chain, the session pallet will rotate session (aka. epochs) at a fixed rate as it did before. It will send these to AH in the form of a `SessionReport` message. Possibly, it will also send messages about offences to AH so that they can be applied and actually slash staker balances in AH.
`pallet-session` on the relay chain will only interact with `pallet-staking-next-ah-client`. `ah-client` could at any point, if it has one, return a validator set to session to be used for the next session. This is indicated by `None` and `Some(Vec<Account>)` in the below diagrams.
Where does `ah-client` possibly get this validator set from, and return a `Some(_)` to `session`? AH can send a `ValidatorSet` message to RC, which will contain new a new validator set. In RC, this set is first buffered in `ah-client` until the next session. Then, it is buffered in `pallet-session` for another session, and is then finally activated.
In any session change in `pallet-session` where a new validator set is activated, that `SessionReport` will contain two pieces of information:
* A secure timestamp originated from the RC
* The id of the validator set (used for bookkeeping)
> This is demonstrated as `SessionReport { active: None }` or `SessionReport { active: Some(ts, id) }` in the following diagrams
Crucially, this timestamp is used by AH to determine what is the duration of an era, and consequently how many new tokens need to be minted for inflation.
### Sequence Diagram
RC will have:
- pallet timestamp
- pallet-offences
- pallet-session-historical
- pallet-session
- pallet-staking-next-ah-client
AH will have:
- pallet-staking-next-rc-client
- pallet-election-provider::* (4 pallets)
- pallet-staking-next
- And rest of the staking related pallets, that don't play a role:
- pallet-nomination-pools
- pallet-fast-unstake
- pallet-delegated-staking
The flow between these pallets is demonstrated in the following diagrams.
First, a `SessionReport` that is a noop. As in, `staking-next` will receive it, but it will decide to do nothing about it.
```mermaid
sequenceDiagram
box transparent RC
participant session
participant ah-client
end
box transparent AH
participant rc-client
participant staking-next
participant election
end
par
session ->> ah-client: NewSession
ah-client ->> session: None
end
ah-client ->> rc-client: SessionReport { active: None }
rc-client ->>+ staking-next: SessionReport { active: None }
staking-next ->> staking-next: Ok, noop
```
Next, let's assume the upcoming `SessionReport` is one where `staking-next` will want to trigger a new election as a consequence of.
```mermaid
sequenceDiagram
box transparent RC
participant session
participant ah-client
end
box transparent AH
participant rc-client
participant staking-next
participant election
end
par
session ->> ah-client: NewSession
ah-client ->> session: None
end
ah-client ->> rc-client: SessionReport { active: None }
rc-client ->>+ staking-next: SessionReport { active: None }
par
staking-next ->> staking-next: CurrentEra += 1 ( = n)
staking-next ->>+ election: start()
end
election ->>- staking-next: result()
staking-next ->>- rc-client: ValidatorSet
rc-client ->> ah-client: ValidatorSet { id: n }
ah-client ->> ah-client: Buffer
par
session ->> ah-client: NewSession
ah-client ->> session: Some(Vec<AccountId>)
session ->> session: Buffer
end
par
session ->> ah-client: NewSession
session ->> session: Activate(n)
ah-client ->> session: None
end
ah-client ->> rc-client: SessionReport { active: Some(ts, n) }
par
rc-client ->>+ staking-next: SessionReport { active: Some(ts, n) }
staking-next ->> staking-next: ActiveEra += 1
staking-next ->> staking-next: EraPayout
end
```
Note that this is happy path of this system, and it assumes that the timeperiod between when `staking-next` decides to plan a new era, and star the election, and when the results of that election are ready is instant.
In reality, this might take some time, and in the middle, another session report might come in, which will have no effect. This can be seen in this diagram:
```mermaid
sequenceDiagram
box transparent RC
participant session
participant ah-client
end
box transparent AH
participant rc-client
participant staking-next
participant election
end
par
session ->> ah-client: NewSession
ah-client ->> session: None
end
ah-client ->> rc-client: SessionReport { active: None }
rc-client ->>+ staking-next: SessionReport { active: None }
par
staking-next ->> staking-next: CurrentEra += 1 ( = n)
staking-next ->>+ election: start()
end
par
session ->> ah-client: NewSession
ah-client ->> session: None
end
ah-client ->> rc-client: SessionReport { active: None }
rc-client ->> staking-next: SessionReport { active: None }
par
session ->> ah-client: NewSession
ah-client ->> session: None
end
ah-client ->> rc-client: SessionReport { active: None }
rc-client ->> staking-next: SessionReport { active: None }
Note right of rc-client: We receive two session reports while election is happening
election ->>- staking-next: result()
staking-next ->>- rc-client: ValidatorSet
rc-client ->> ah-client: ValidatorSet { id: n }
ah-client ->> ah-client: Buffer
par
session ->> ah-client: NewSession
ah-client ->> session: Some(Vec<AccountId>)
session ->> session: Buffer
end
par
session ->> ah-client: NewSession
session ->> session: Activate(n)
ah-client ->> session: None
end
ah-client ->> rc-client: SessionReport { active: Some(ts, n) }
par
rc-client ->>+ staking-next: SessionReport { active: Some(ts, n) }
staking-next ->> staking-next: ActiveEra += 1
staking-next ->> staking-next: EraPayout
end
```
In this diagram, two sessions happen while the election is ongoing. In reality, staking might decide to trigger an election at _any time_, for example when `Forcing::ForceNew` is set. Once any election is done, the result is sent back to RC, and is acted upon accordingly.
### Era and Session Definitions
This brings us to the definition of these terms.
* Session/Epoch: This is solely handled by the relay chain, and is tracked in `pallet-session`.
* `CurrentEra`: This storage item is tracked in staking-next, is better named the *Planning Era*, and is either:
* equal to `ActiveEra`, which means we are not planning any new elections
* `ActiveEra + 1`, which means we are planning a new election. This election might have been completed, and the results might have already been sent back to RC, but we have not received a `SessionReport` yet that indicates this era's validator set are active on the relay chain.
* `ActiveEra`: The era index, and the starting timestamp of the era who's validator set are currently active on the relay chain.
> Seeing when `CurrentEra` and `ActiveEra` are increment in the above diagrams will further clarify these definitions.
## PJS
What to do with PJS Apps. For simplicity, I suggest keeping everything in the current staking page as-is. What we can additionally mark it as `staking-classic`, or `relay-chain staking`. This page will be automatically removed once `pallet-staking` stops to exist on RC.
### Staking Next Page
Then, we can discuss a `staking-next` in PJS. I suggest the following structure for it, but it can be discussed:
* Command Center: See below
* Slashes: needs to be reworked, as the storage items have changed. See below
* Active Validators (what is currently in the "overview" page)
* All Validators (renamed version of what is now called "Targets")
* Payouts: remain as-is, don't care.
* Pools: remain as-is, don't care
* Bags: remain as-is, don't care
So, the main new pages are "Command Center" and "Slashes".
#### Command Center Page
> I don't think we can get over the fact that this dashboard will only work and be informative if we can connect to both AH and RC at the same time. I hope this is not a blocker.
> I comply with the requirement that PJS is a purely "live" dashboard, and can only query the tip of the chain.
The command center should be divided into two clear sections, one for AH and one for RC.
I have created a demo code to demonstrate what storage items we need to track reactively. Beyond live storage items, some events are useful to keep and show a list of, much like the "Explore" tab.
```javascript
export async function commandCenterHandler(): Promise<void> {
const rcApi = await getApi("ws://localhost:9955");
const ahApi = await getApi("ws://localhost:9966");
const manager = UpdateManager.getInstance();
// manager.hook();
let rcOutput: string[] = []
let ahOutput: string[] = []
const rcEvents: string[] = []
const ahEvents: string[] = []
rcApi.rpc.chain.subscribeFinalizedHeads(async (header) => {
// --- RC:
// current session index
const index = await rcApi.query.session.currentIndex();
// whether the session pallet has a queued validator set within it
const hasQueuedInSession = await rcApi.query.session.queuedChanged();
// the range of historical session data that we have in the RC.
const historicalRange = await rcApi.query.historical.storedRange();
// whether there is a validator set queued in ah-client. for this we need to display only the id and the length of the set.
const hasQueuedInClient = await rcApi.query.stakingNextAhClient.validatorSet();
// whether we have already passed a new validator set to session, and therefore in the next session rotation we want to pass this id to AH.
const hasNextActiveId = await rcApi.query.stakingNextAhClient.nextSessionChangesValidators();
// whether the AhClient pallet is blocked or not, useful for migration signal from the fellowship.
const isBlocked = await rcApi.query.stakingNextAhClient.isBlocked();
// Events that we are interested in from RC:
const eventsOfInterest = (await rcApi.query.system.events())
.map((e) => e.event)
.filter((e) => {
const ahClientEvents = (e: IEventData) => e.section == 'stakingNextAhClient';
const sessionEvents = (e: IEventData) => e.section == 'session' || e.section == 'historical';
return ahClientEvents(e.data) || sessionEvents(e.data);
})
.map((e) => `${e.section.toString()}::${e.method.toString()}(${e.data.toString()})`);
rcEvents.push(...eventsOfInterest);
rcOutput = [
`RC:`,
`finalized block ${header.number}`,
`RC.session: index=${index}, hasQueuedInSession=${hasQueuedInSession}, historicalRange=${historicalRange}`,
`RC.stakingNextAhClient: hasQueuedInClient=${hasQueuedInClient}, hasNextActiveId=${hasNextActiveId}, isBlocked=${isBlocked}`,
`RC.events: ${rcEvents}`,
`----`
]
manager.update(rcOutput.concat(ahOutput))
})
// AH:
ahApi.rpc.chain.subscribeFinalizedHeads(async (header) => {
// the current planned era
const currentEra = await ahApi.query.staking.currentEra();
// the active era
const activeEra = await ahApi.query.staking.activeEra();
// the starting index of the active era
const erasStartSessionIndex = await ahApi.query.staking.erasStartSessionIndex(activeEra.unwrap().index)
// the basic state of the election provider
const phase = await ahApi.query.multiBlock.currentPhase();
const round = await ahApi.query.multiBlock.round();
const snapshotRange = (await ahApi.query.multiBlock.pagedVoterSnapshotHash.entries()).map(([k, v]) => k.args[0]).sort();
const queuedScore = await ahApi.query.multiBlockVerifier.queuedSolutionScore();
const signedSubmissions = await ahApi.query.multiBlockSigned.sortedScores(round);
// Events that we are interested in from RC:
const eventsOfInterest = (await ahApi.query.system.events())
.map((e) => e.event)
.filter((e) => {
const election = (e: IEventData) => e.section == 'multiBlock' || e.section == 'multiBlockVerifier' || e.section == 'multiBlockSigned' || e.section == 'multiBlockUnsigned';
const rcClient = (e: IEventData) => e.section == 'stakingNextRcClient';
const staking = (e: IEventData) => e.section == 'staking' && (e.method == 'EraPaid' || e.method == 'SessionRotated' || e.method == 'PagedElectionProceeded');
return election(e.data) || rcClient(e.data) || staking(e.data);
})
.map((e) => `${e.section.toString()}::${e.method.toString()}(${e.data.toString()})`);
ahEvents.push(...eventsOfInterest);
ahOutput = [
`AH:`,
`finalized block ${header.number}`,
`AH.staking: currentEra=${currentEra}, activeEra=${activeEra}, erasStartSessionIndex(${activeEra.unwrap().index})=${erasStartSessionIndex}`,
`multiBlock: phase=${phase}, round=${round}, snapshotRange=${snapshotRange}, queuedScore=${queuedScore}, signedSubmissions=${signedSubmissions}`,
`AH.events: ${ahEvents}`,
`----`,
]
manager.update(rcOutput.concat(ahOutput))
});
// Prevent the function from returning by creating a promise that never resolves
return new Promise<void>((resolve) => {
// Set up signal handlers for graceful shutdown
process.on('SIGINT', () => {
console.log('Received SIGINT. Shutting down...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('Received SIGTERM. Shutting down...');
process.exit(0);
});
console.log('Command center running. Press Ctrl+C to exit.');
});
}
```
I have put this together in a very short amount of time, but I hope it gives a good idea about the framework of what is needed.
A non-PJS implementation (turboflakes app?) that has more liberty in storing histrocal data can do a better job here.
## Next Steps
The code for this system is still in development, yet is stable enough for local testing. We expect this to also be live soon on Westend.
Referecen types:
* [`SessionReport` structure](https://github.com/paritytech/polkadot-sdk/blob/8680d3a693cd341c4f1691b57f4555c6db0dfa3f/substrate/frame/staking-next/rc-client/src/lib.rs#L204-L229)
* [`relay_session_report`](https://github.com/paritytech/polkadot-sdk/blob/8680d3a693cd341c4f1691b57f4555c6db0dfa3f/substrate/frame/staking-next/rc-client/src/lib.rs#L372-L375) call, triggered by XCM from RC
* [`ValidatorSet` structure](https://github.com/paritytech/polkadot-sdk/blob/8680d3a693cd341c4f1691b57f4555c6db0dfa3f/substrate/frame/staking-next/rc-client/src/lib.rs#L147-L162)
* [`validator_set`](https://github.com/paritytech/polkadot-sdk/blob/8680d3a693cd341c4f1691b57f4555c6db0dfa3f/substrate/frame/staking-next/ah-client/src/lib.rs#L285) call, triggered by XCM from AH
This is all from [this branch](https://github.com/paritytech/polkadot-sdk/pull/7601). At the time of this writing, commit `8680d3a693cd341c4f1691b57f4555c6db0dfa3f` is stable and compiles.
To run a setup with all of the said above, use this script:
https://github.com/paritytech/polkadot-sdk/blob/8680d3a693cd341c4f1691b57f4555c6db0dfa3f/substrate/frame/staking-next/runtimes/parachain/build-and-run-zn.sh
This will:
- compile `staking-next/runtimes/parachain`
- compile `staking-next/runtimes/rc`
- generate chain-specs
- launch ZN
Once setup, you can see the two networks working together, for example sending `SessionReport` back and forth by having the node for bob and charlie (rc and parachain node respectively) open:
