Skip to content

Commit 2760888

Browse files
authored
Add smoke tests for BABE (#749)
1 parent 8493a47 commit 2760888

File tree

3 files changed

+203
-0
lines changed

3 files changed

+203
-0
lines changed

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@polkadot/types-codec": "14.3.1",
3636
"@polkadot/util": "13.2.3",
3737
"@polkadot/util-crypto": "13.2.3",
38+
"@polkadot/wasm-crypto": "^7.4.1",
3839
"@tanssi/api-augment": "workspace:*",
3940
"@types/debug": "4.1.12",
4041
"@types/node": "22.9.0",
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
2+
import { getBlockArray } from "@moonwall/util";
3+
import { ApiPromise } from "@polkadot/api";
4+
import { GenericExtrinsic } from "@polkadot/types";
5+
import { FrameSystemEventRecord } from "@polkadot/types/lookup";
6+
import { AnyTuple } from "@polkadot/types/types";
7+
import { hexToU8a, stringToHex } from "@polkadot/util";
8+
import { sr25519Verify } from "@polkadot/wasm-crypto";
9+
import Bottleneck from "bottleneck";
10+
11+
const timePeriod = process.env.TIME_PERIOD ? Number(process.env.TIME_PERIOD) : 1 * 60 * 60 * 1000;
12+
const timeout = Math.max(Math.floor(timePeriod / 12), 5000);
13+
const hours = (timePeriod / (1000 * 60 * 60)).toFixed(2);
14+
15+
type BlockFilteredRecord = {
16+
blockNum: number;
17+
blockHash;
18+
header;
19+
preHash;
20+
extrinsics: GenericExtrinsic<AnyTuple>[];
21+
events: FrameSystemEventRecord[];
22+
logs;
23+
authorities;
24+
accountKeys;
25+
};
26+
27+
describeSuite({
28+
id: "S20",
29+
title: "Sample suite that only runs on Dancelight chains",
30+
foundationMethods: "read_only",
31+
testCases: ({ it, context, log }) => {
32+
let api: ApiPromise;
33+
let blockData: BlockFilteredRecord[];
34+
35+
beforeAll(async () => {
36+
api = context.polkadotJs();
37+
38+
const blockNumArray = await getBlockArray(api, timePeriod);
39+
log(`Collecting ${hours} hours worth of authors`);
40+
41+
const getBlockData = async (blockNum: number) => {
42+
const blockHash = await api.rpc.chain.getBlockHash(blockNum);
43+
const signedBlock = await api.rpc.chain.getBlock(blockHash);
44+
const header = signedBlock.block.header;
45+
const apiAt = await api.at(blockHash);
46+
const preHash = getPreHash(api, header);
47+
48+
// Get the session keys from all the authorities because we would need to parse the logs here to know
49+
// which one is the expected author.
50+
const authorities = await apiAt.query.session.validators();
51+
const accountKeys = new Map(
52+
await Promise.all(
53+
authorities.map(async (validator) => {
54+
const nextKeys = await apiAt.query.session.nextKeys(validator);
55+
return [validator.toJSON(), nextKeys.toJSON()];
56+
})
57+
)
58+
);
59+
60+
return {
61+
blockNum: blockNum,
62+
preHash,
63+
extrinsics: signedBlock.block.extrinsics,
64+
events: await apiAt.query.system.events(),
65+
logs: signedBlock.block.header.digest.logs,
66+
authorities,
67+
accountKeys,
68+
};
69+
};
70+
const limiter = new Bottleneck({ maxConcurrent: 5, minTime: 100 });
71+
blockData = await Promise.all(blockNumArray.map((num) => limiter.schedule(() => getBlockData(num))));
72+
}, timeout);
73+
74+
it({
75+
id: "C01",
76+
title: "BABE keys are set and validators from logs match validators from pallet",
77+
test: async function () {
78+
const blockToCheck = (await api.query.babe.epochStart()).toJSON()[1];
79+
80+
const apiAtSessionChange = await api.at(await api.rpc.chain.getBlockHash(blockToCheck));
81+
82+
const digestsInSessionChange = (await apiAtSessionChange.query.system.digest()).logs;
83+
const filteredDigests = digestsInSessionChange.filter(
84+
(log) => log.isConsensus === true && log.asConsensus[0].toHex() == stringToHex("BABE")
85+
);
86+
expect(filteredDigests.length).to.eq(1);
87+
88+
// 0x01 corresponds to ConsensusLog::NextEpochData enum variant.
89+
expect(filteredDigests[0].asConsensus[1].toHex().startsWith("0x01")).to.be.true;
90+
91+
// Assert that authorities from log == authorities from pallet
92+
const babeAuthoritiesFromPallet = await api.query.babe.authorities();
93+
const babeConsensusLog = api.registry.createType(
94+
"(u8, Vec<(SpConsensusBabeAppPublic, u64)>, [u8; 32])",
95+
filteredDigests[0].asConsensus[1].toHex()
96+
);
97+
98+
expect(babeConsensusLog[1]).to.deep.equal(babeAuthoritiesFromPallet);
99+
100+
// Get babe keys from pallet session
101+
const sessionValidators = await api.query.session.validators();
102+
103+
const babeKeysInPalletSession = [];
104+
105+
for (const account of sessionValidators) {
106+
const accountKeys = await api.query.session.nextKeys(account);
107+
expect(accountKeys.isSome, `Missing babe key for validator ${account.toJSON()}`).toBeTruthy();
108+
babeKeysInPalletSession.push(accountKeys.unwrap().babe.toHex());
109+
}
110+
111+
// Assert that all validators have babe keys
112+
const babeAuthoritiesSorted = babeAuthoritiesFromPallet.map((x) => x[0].toHex());
113+
babeAuthoritiesSorted.sort();
114+
babeKeysInPalletSession.sort();
115+
expect(babeKeysInPalletSession).to.deep.equal(babeAuthoritiesSorted);
116+
},
117+
});
118+
119+
it({
120+
id: "C02",
121+
title: "BABE author signature valid",
122+
test: async function () {
123+
const failures = blockData
124+
.map(({ blockNum, preHash, logs, authorities, accountKeys }) => {
125+
const babeLogs = logs.filter(
126+
(log) => log.isPreRuntime === true && log.asPreRuntime[0].toHex() == stringToHex("BABE")
127+
);
128+
expect(babeLogs.length).to.eq(1);
129+
130+
const babeLogEnum = api.registry.createType(
131+
"SpConsensusBabeDigestsPreDigest",
132+
babeLogs[0].asPreRuntime[1].toHex()
133+
);
134+
135+
expect(babeLogEnum.isSecondaryVRF || babeLogEnum.isPrimary).toBeTruthy();
136+
const babeLog = babeLogEnum.isSecondaryVRF ? babeLogEnum.asSecondaryVRF : babeLogEnum.asPrimary;
137+
138+
// Get expected author from BABE log and on chain authorities
139+
const authorityIndex = babeLog.authorityIndex;
140+
const orchestratorAuthorities = authorities.toJSON();
141+
const expectedAuthor = orchestratorAuthorities[authorityIndex.toNumber()];
142+
143+
// Get block author signature from seal log
144+
const sealLogs = logs.filter(
145+
(log) => log.isSeal === true && log.asSeal[0].toHex() == stringToHex("BABE")
146+
);
147+
148+
expect(sealLogs.length).to.eq(1);
149+
const sealLog = api.registry.createType(
150+
"PolkadotPrimitivesV7ValidatorAppSignature",
151+
sealLogs[0].asSeal[1].toHex()
152+
);
153+
154+
// Verify seal signature
155+
const message = hexToU8a(preHash);
156+
const signature = hexToU8a(sealLog.toHex());
157+
const authorKeys = accountKeys.get(expectedAuthor);
158+
expect(
159+
authorKeys && authorKeys.babe,
160+
`Missing babe key for block author: ${expectedAuthor}`
161+
).toBeTruthy();
162+
const pubKey = hexToU8a(authorKeys.babe);
163+
164+
const authorValid = sr25519Verify(signature, message, pubKey);
165+
166+
return { blockNum, expectedAuthor, authorValid };
167+
})
168+
.filter(({ authorValid }) => authorValid == false);
169+
170+
failures.forEach(({ blockNum, expectedAuthor }) => {
171+
log(
172+
`Author at block #${blockNum} should have been #${expectedAuthor.toString()}, but seal signature does not match`
173+
);
174+
});
175+
176+
expect(
177+
failures.length,
178+
`Please investigate blocks ${failures.map((a) => a.blockNum).join(`, `)}; authors `
179+
).to.equal(0);
180+
},
181+
});
182+
},
183+
});
184+
185+
// Given a block header, returns its preHash. This is the hash of the header before adding the seal.
186+
// The hash of the block header after adding the seal is the block hash.
187+
function getPreHash(api, header) {
188+
const logsNoSeal = header.digest.logs.filter((log) => !log.isSeal);
189+
const headerWithoutSeal = api.registry.createType("Header", {
190+
parentHash: header.parentHash,
191+
number: header.number,
192+
stateRoot: header.stateRoot,
193+
extrinsicsRoot: header.extrinsicsRoot,
194+
digest: {
195+
logs: logsNoSeal,
196+
},
197+
});
198+
return headerWithoutSeal.hash.toHex();
199+
}

0 commit comments

Comments
 (0)