Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
820 changes: 410 additions & 410 deletions Cargo.lock

Large diffs are not rendered by default.

270 changes: 135 additions & 135 deletions Cargo.toml

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion e2e/shield/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,8 @@ export const submitEncryptedRaw = async (
const tx = api.tx.MevShield.submit_encrypted({
ciphertext: Binary.fromBytes(ciphertext),
});
return tx.signAndSubmit(signer, nonce !== undefined ? { nonce } : {});
return tx.signAndSubmit(signer, {
...(nonce !== undefined ? { nonce } : {}),
mortality: { mortal: true, period: 8 },
});
};
13 changes: 1 addition & 12 deletions e2e/shield/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { writeFile, readFile, rm, mkdir } from "node:fs/promises";
import {
generateChainSpec,
insertKeys,
getGenesisPatch,
addAuthority,
} from "e2e-shared/chainspec.js";
import {
startNode,
Expand Down Expand Up @@ -35,10 +33,6 @@ const nodes: Node[] = [];

const BINARY_PATH = process.env.BINARY_PATH || "../../target/release/node-subtensor";

// The local chain spec has 2 built-in authorities (One, Two).
// We add "Three" dynamically by patching the chain spec JSON.
const EXTRA_AUTHORITY_SEEDS = ["Three"];

type NodeConfig = Omit<NodeOptions, "binaryPath" | "chainSpec"> & {
keySeed?: string;
};
Expand All @@ -62,12 +56,7 @@ export async function setup() {

await mkdir("/tmp/subtensor-e2e/shield", { recursive: true });

await generateChainSpec(BINARY_PATH, CHAIN_SPEC_PATH, (spec) => {
const patch = getGenesisPatch(spec);
for (const seed of EXTRA_AUTHORITY_SEEDS) {
addAuthority(patch, seed);
}
});
await generateChainSpec(BINARY_PATH, CHAIN_SPEC_PATH);

for (const config of NODE_CONFIGS) {
await rm(config.basePath, { recursive: true, force: true });
Expand Down
76 changes: 42 additions & 34 deletions e2e/shield/tests/00-basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,40 @@ describe("MEV Shield — encrypted transactions", () => {
});

// Pool validation rejects with FailedShieldedTxParsing (Custom code 23).
await expect(tx.signAndSubmit(alice.signer, { nonce })).rejects.toThrow();
await expect(
tx.signAndSubmit(alice.signer, { nonce, mortality: { mortal: true, period: 8 } }),
).rejects.toThrow();
});

it("Multiple encrypted txs in same block", async () => {
// Use different signers to avoid nonce ordering issues between
// the outer wrappers and decrypted inner transactions.
const nextKey = await getNextKey(api);
expect(nextKey).toBeDefined();

const balanceBefore = await getBalance(api, charlie.address);

const senders = [alice, bob];
const amount = 1_000_000_000n;
const txPromises = [];

for (const sender of senders) {
const nonce = await getAccountNonce(api, sender.address);

const innerTxHex = await api.tx.Balances.transfer_keep_alive({
dest: MultiAddress.Id(charlie.address),
value: amount,
}).sign(sender.signer, { nonce: nonce + 1 });

txPromises.push(
submitEncrypted(api, sender.signer, hexToU8a(innerTxHex), nextKey!, nonce),
);
}

await Promise.all(txPromises);

const balanceAfter = await getBalance(api, charlie.address);
expect(balanceAfter).toBeGreaterThan(balanceBefore);
});

it("Wrong key hash is not included by the block proposer", async () => {
Expand All @@ -148,7 +181,10 @@ describe("MEV Shield — encrypted transactions", () => {
const tx = api.tx.MevShield.submit_encrypted({
ciphertext: Binary.fromBytes(tampered),
});
const signedHex = await tx.sign(alice.signer, { nonce });
const signedHex = await tx.sign(alice.signer, {
nonce,
mortality: { mortal: true, period: 8 },
});
// Send without waiting — the tx enters the pool but the block
// proposer will skip it because the key_hash doesn't match.
client.submit(signedHex).catch(() => {});
Expand Down Expand Up @@ -181,7 +217,10 @@ describe("MEV Shield — encrypted transactions", () => {
const tx = api.tx.MevShield.submit_encrypted({
ciphertext: Binary.fromBytes(ciphertext),
});
const signedHex = await tx.sign(alice.signer, { nonce });
const signedHex = await tx.sign(alice.signer, {
nonce,
mortality: { mortal: true, period: 8 },
});
// Send without waiting — the block proposer will reject because
// key_hash no longer matches currentKey or nextKey.
client.submit(signedHex).catch(() => {});
Expand All @@ -192,35 +231,4 @@ describe("MEV Shield — encrypted transactions", () => {
const balanceAfter = await getBalance(api, bob.address);
expect(balanceAfter).toBe(balanceBefore);
});

it("Multiple encrypted txs in same block", async () => {
// Use different signers to avoid nonce ordering issues between
// the outer wrappers and decrypted inner transactions.
const nextKey = await getNextKey(api);
expect(nextKey).toBeDefined();

const balanceBefore = await getBalance(api, charlie.address);

const senders = [alice, bob];
const amount = 1_000_000_000n;
const txPromises = [];

for (const sender of senders) {
const nonce = await getAccountNonce(api, sender.address);

const innerTxHex = await api.tx.Balances.transfer_keep_alive({
dest: MultiAddress.Id(charlie.address),
value: amount,
}).sign(sender.signer, { nonce: nonce + 1 });

txPromises.push(
submitEncrypted(api, sender.signer, hexToU8a(innerTxHex), nextKey!, nonce),
);
}

await Promise.all(txPromises);

const balanceAfter = await getBalance(api, charlie.address);
expect(balanceAfter).toBeGreaterThan(balanceBefore);
});
});
127 changes: 127 additions & 0 deletions e2e/shield/tests/03-timing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { readFile } from "node:fs/promises";
import type { PolkadotClient, TypedApi } from "polkadot-api";
import { hexToU8a } from "@polkadot/util";
import { subtensor, MultiAddress } from "@polkadot-api/descriptors";
import type { NetworkState } from "../setup.js";
import {
connectClient,
createSigner,
getAccountNonce,
getBalance,
waitForFinalizedBlocks,
sleep,
} from "e2e-shared/client.js";
import { getNextKey, submitEncrypted } from "../helpers.js";

let client: PolkadotClient;
let api: TypedApi<typeof subtensor>;
let state: NetworkState;

const alice = createSigner("//Alice");
const bob = createSigner("//Bob");

beforeAll(async () => {
const data = await readFile("/tmp/subtensor-e2e/shield/nodes.json", "utf-8");
state = JSON.parse(data);
({ client, api } = await connectClient(state.nodes[0].rpcPort));
});

afterAll(() => {
client?.destroy();
});

describe("MEV Shield — timing boundaries", () => {
it("Submit immediately after a new block", async () => {
// Wait for a fresh finalized block, then immediately read NextKey and submit.
// This tests the "just after block" boundary where keys just rotated.
await waitForFinalizedBlocks(client, 1);

const nextKey = await getNextKey(api);
expect(nextKey).toBeDefined();

const balanceBefore = await getBalance(api, bob.address);

const nonce = await getAccountNonce(api, alice.address);
const innerTxHex = await api.tx.Balances.transfer_keep_alive({
dest: MultiAddress.Id(bob.address),
value: 1_000_000_000n,
}).sign(alice.signer, { nonce: nonce + 1 });

await submitEncrypted(api, alice.signer, hexToU8a(innerTxHex), nextKey!, nonce);

const balanceAfter = await getBalance(api, bob.address);
expect(balanceAfter).toBeGreaterThan(balanceBefore);
});

it("Submit mid-block (~6s after block)", async () => {
// Wait for a block, then sleep 6s (half of 12s slot) before submitting.
// The key should still be valid — the same NextKey applies until the next block.
await waitForFinalizedBlocks(client, 1);
await sleep(6_000);

const nextKey = await getNextKey(api);
expect(nextKey).toBeDefined();

const balanceBefore = await getBalance(api, bob.address);

const nonce = await getAccountNonce(api, alice.address);
const innerTxHex = await api.tx.Balances.transfer_keep_alive({
dest: MultiAddress.Id(bob.address),
value: 1_000_000_000n,
}).sign(alice.signer, { nonce: nonce + 1 });

await submitEncrypted(api, alice.signer, hexToU8a(innerTxHex), nextKey!, nonce);

const balanceAfter = await getBalance(api, bob.address);
expect(balanceAfter).toBeGreaterThan(balanceBefore);
});

it("Submit just before next block (~11s after block)", async () => {
// Wait for a block, then sleep ~11s to submit right before the next slot.
// The tx enters the pool just as the next block is about to be produced.
// It should still be included because the N+2 author hasn't changed yet,
// and PendingKey will match on the next block's proposer check.
await waitForFinalizedBlocks(client, 1);
await sleep(11_000);

const nextKey = await getNextKey(api);
expect(nextKey).toBeDefined();

const balanceBefore = await getBalance(api, bob.address);

const nonce = await getAccountNonce(api, alice.address);
const innerTxHex = await api.tx.Balances.transfer_keep_alive({
dest: MultiAddress.Id(bob.address),
value: 1_000_000_000n,
}).sign(alice.signer, { nonce: nonce + 1 });

await submitEncrypted(api, alice.signer, hexToU8a(innerTxHex), nextKey!, nonce);

const balanceAfter = await getBalance(api, bob.address);
expect(balanceAfter).toBeGreaterThan(balanceBefore);
});

it("Read key, wait full slot (12s), then submit", async () => {
// Read NextKey, wait a full slot duration, then submit.
// After one full slot, the key rotates: old NextKey becomes PendingKey.
// The tx should still be included by the target N+2 author.
const nextKey = await getNextKey(api);
expect(nextKey).toBeDefined();

await sleep(12_000);

const balanceBefore = await getBalance(api, bob.address);

const nonce = await getAccountNonce(api, alice.address);
const innerTxHex = await api.tx.Balances.transfer_keep_alive({
dest: MultiAddress.Id(bob.address),
value: 1_000_000_000n,
}).sign(alice.signer, { nonce: nonce + 1 });

await submitEncrypted(api, alice.signer, hexToU8a(innerTxHex), nextKey!, nonce);

const balanceAfter = await getBalance(api, bob.address);
expect(balanceAfter).toBeGreaterThan(balanceBefore);
});
});
Loading
Loading