Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
aa9ec59
Measure pallet timeouts in relay chain blocks, not parachain blocks
ilchu Jul 7, 2026
df9390c
Source the relay chain block number in off-chain consumers
ilchu Jul 7, 2026
8d0442a
Add provider integration tests for relay-clock semantics
ilchu Jul 7, 2026
fe45843
Merge branch 'dev' into ic/relay-block-provider
ilchu Jul 10, 2026
f636e25
Inline relay-block query in provider node, drop storage-client coupling
ilchu Jul 13, 2026
37797e2
Prefix relay-chain time constants with rc_
ilchu Jul 13, 2026
7d6d1a8
Merge branch 'dev' into ic/relay-block-provider
ilchu Jul 13, 2026
9400455
Merge branch 'dev' into ic/relay-block-provider
ilchu Jul 13, 2026
06f74d4
Simplify challenge-sweep doc comments
ilchu Jul 16, 2026
feaf834
Fix on_initialize_slash_challenges benchmark to drive the real sweep
ilchu Jul 16, 2026
82f6030
Update slashing flow diagram for the on_initialize range sweep
ilchu Jul 16, 2026
5bc92a3
Cap the sweep's per-block slash budget below MaxChallengesPerDeadline
ilchu Jul 16, 2026
392b58f
Rename ChainState.current_block to current_relay_block
ilchu Jul 16, 2026
a3c35f6
Add try-state invariant for the challenge sweep cursor
ilchu Jul 16, 2026
b20223e
Merge branch 'dev' into ic/relay-block-provider
ilchu Jul 16, 2026
427e9a7
Re-home the sweep-cursor invariant into the try_state module
ilchu Jul 16, 2026
26c510c
Merge branch 'dev' into ic/relay-block-provider
ilchu Jul 16, 2026
aeb580c
Fix stale current_block field refs in provider-node integration tests
ilchu Jul 16, 2026
cc23056
Expose current_anchor_block runtime API; make provider block-agnostic
ilchu Jul 16, 2026
5c38e4e
Merge branch 'dev' into ic/relay-block-provider
ilchu Jul 17, 2026
4b3e686
Merge branch 'dev' into ic/relay-block-provider
ilchu Jul 17, 2026
357b0bb
Fix challenger checkpoint age to use the anchor clock
bkontur Jul 17, 2026
ecbceb0
Rename anchor-clock locals from current_block to anchor_block
bkontur Jul 17, 2026
4abe26d
Expose anchor_block_time_millis runtime API; format durations on it
bkontur Jul 17, 2026
fcfd9fa
Move the anchor tick into the pallet as Config::AnchorBlockTimeMillis
bkontur Jul 17, 2026
859bd27
Track the pallet's anchor block in the UIs; fix six pallet-clock comp…
bkontur Jul 17, 2026
5205e00
Drop anchor refresh results from a torn-down connection
bkontur Jul 17, 2026
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
14 changes: 6 additions & 8 deletions client/src/challenger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! - Automated challenge strategies

use crate::base::{BaseClient, ClientConfig, ClientError, ClientResult};
use crate::substrate::{extrinsics, storage, SubstrateClient};
use crate::substrate::{extrinsics, fetch_current_anchor_block, storage, SubstrateClient};
use sp_runtime::AccountId32;
use storage_primitives::{BucketId, ChunkLocation, Commitment};
use subxt::dynamic::At;
Expand Down Expand Up @@ -382,12 +382,10 @@ impl ChallengerClient {

// Compute checkpoint age from bucket snapshot
let last_checkpoint_age = {
let current_block = chain
.api()
.at_current_block()
.await
.map_err(|e| ClientError::Chain(format!("Failed to get latest block: {e}")))?
.block_number() as u32;
// `checkpoint_block` is on the pallet's anchor clock (relay
// blocks), so measure the age on that clock, not the parachain
// height.
let anchor_block = fetch_current_anchor_block(&at).await?;

let (addr, keys) = storage::bucket_info(bucket_id);
let bucket_thunk = at
Expand All @@ -413,7 +411,7 @@ impl ChallengerClient {
.and_then(|v| v.as_u128())
.unwrap_or(0) as u32;

current_block.saturating_sub(checkpoint_block)
anchor_block.saturating_sub(checkpoint_block)
} else {
0
}
Expand Down
24 changes: 24 additions & 0 deletions client/src/substrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,30 @@ pub mod storage {

// Helper functions for common operations

/// The pallet's anchor block — the clock every on-chain duration is measured
/// against — via the `StorageProviderApi::current_anchor_block` runtime API.
/// Compare anchor-denominated values (deadlines, expiries, `checkpoint_block`)
/// against this, never the parachain height.
pub async fn fetch_current_anchor_block<C>(
at: &subxt::client::ClientAtBlock<PolkadotConfig, C>,
) -> Result<u32, ClientError>
where
C: subxt::client::OnlineClientAtBlockT<PolkadotConfig>,
{
use codec::Decode;
// Raw `state_call` + manual decode so the API needn't be in the node's
// metadata snapshot.
let bytes = at
.runtime_apis()
.call_raw("StorageProviderApi_current_anchor_block", None)
.await
.map_err(|e| {
ClientError::Chain(format!("current_anchor_block runtime API call failed: {e}"))
})?;
u32::decode(&mut &bytes[..])
.map_err(|e| ClientError::Chain(format!("Failed to decode anchor block: {e}")))
}

/// Parse a hex string to H256.
pub fn parse_h256(hex: &str) -> Result<H256, ClientError> {
let hex = hex.strip_prefix("0x").unwrap_or(hex);
Expand Down
6 changes: 4 additions & 2 deletions docs/design/EXECUTION_FLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -538,9 +538,11 @@ sequenceDiagram
participant C as Chain (Pallet)
participant B as Balances

Note over C: on_finalize(block_number) hook
Note over C: on_initialize(n) slash sweep

C->>C: expired = Challenges::take(block_number)
Note over C: Deadlines are relay-chain blocks. Drain every deadline key in<br/>(LastSweptChallengeBlock, previous relay parent], budget-capped.

C->>C: expired = Challenges::drain_prefix(deadline_key)

loop For each expired challenge
Note over C: Provider failed to respond!
Expand Down
7 changes: 4 additions & 3 deletions examples/papi/checkpoint-missed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ import assert from "node:assert";
import {
configureCheckpointWindow,
connect,
currentRelayBlock,
ensureProviderRegistered,
establishStorageAgreement,
makeSigner,
negotiateTerms,
READ_OPTS,
reportMissedCheckpoint,
sameAddress,
waitForBlock,
waitForRelayBlock,
waitForBlockProduction,
waitForChainReady,
waitForNextBlock,
Expand Down Expand Up @@ -107,7 +108,7 @@ async function main() {
);

console.log("\n=== Step 3: Pick a window and let it elapse without a checkpoint ===");
const head = Number(await api.query.System.Number.getValue(READ_OPTS));
const head = await currentRelayBlock(api);
const missedWindow = BigInt(Math.floor(head / WINDOW_INTERVAL));
// window_end = (missedWindow + 1) * interval ; need current_block > window_end
const windowEnd = (Number(missedWindow) + 1) * WINDOW_INTERVAL;
Expand All @@ -118,7 +119,7 @@ async function main() {
windowEnd,
windowEnd
);
await waitForBlock(papi, windowEnd);
await waitForRelayBlock(papi, api, windowEnd);

console.log("\n=== Step 4: Record balances, then report_missed_checkpoint ===");
const providerBefore = (await api.query.StorageProvider.Providers.getValue(
Expand Down
6 changes: 3 additions & 3 deletions examples/papi/e2e/02-agreement-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
negotiateTerms,
READ_OPTS,
sameAddress,
waitForBlock,
waitForRelayBlock,
} from "@web3-storage/sdk";
import {
getFree,
Expand Down Expand Up @@ -90,7 +90,7 @@ async function main() {
))!;
const expiresAt = Number(agreement.expires_at);
console.log(" Waiting for expiry at block %d...", expiresAt);
await waitForBlock(papi, expiresAt);
await waitForRelayBlock(papi, api, expiresAt);
const provBefore = await getFree(api, provider);
await endAgreement(api, client, provider, bucketId, "Pay");
const provAfter = await getFree(api, provider);
Expand All @@ -117,7 +117,7 @@ async function main() {
READ_OPTS
))!;
console.log(" Waiting for expiry at block %d...", Number(agreement.expires_at));
await waitForBlock(papi, Number(agreement.expires_at));
await waitForRelayBlock(papi, api, Number(agreement.expires_at));
const result = await endAgreement(api, client, provider, bucketId, "Burn", {
burn_percent: 100,
});
Expand Down
15 changes: 8 additions & 7 deletions examples/papi/e2e/04-data-upload-and-retrieval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import assert from "node:assert";
import { blake2b256 } from "@polkadot-labs/hdkd-helpers";
import {
currentRelayBlock,
downloadChunk,
ensureProviderRegistered,
makeSigner,
Expand Down Expand Up @@ -55,7 +56,7 @@ async function main() {
name: "4.1 Small chunk (100 bytes)",
fn: async () => {
const data = "x".repeat(100);
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const { hash, commit } = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
assert.ok(commit.mmr_root, "Should return mmr_root");
const downloaded = await downloadChunk(PROVIDER_URL, hash);
Expand All @@ -67,7 +68,7 @@ async function main() {
name: "4.2 Medium chunk (64 KB)",
fn: async () => {
const data = randomBytes(64 * 1024);
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const { hash } = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
const downloaded = await downloadChunk(PROVIDER_URL, hash);
assert.deepStrictEqual(new Uint8Array(downloaded), data, "64KB roundtrip integrity");
Expand All @@ -78,7 +79,7 @@ async function main() {
name: "4.3 Max chunk size (256 KB)",
fn: async () => {
const data = randomBytes(256 * 1024);
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const { hash } = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
const downloaded = await downloadChunk(PROVIDER_URL, hash);
assert.deepStrictEqual(new Uint8Array(downloaded), data, "256KB roundtrip integrity");
Expand All @@ -89,7 +90,7 @@ async function main() {
name: "4.4 Multiple sequential uploads",
fn: async () => {
const uploads = [];
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
for (let i = 0; i < 5; i++) {
const data = `sequential upload #${i} @ ${Date.now()}`;
const result = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
Expand Down Expand Up @@ -207,7 +208,7 @@ async function main() {
name: "4.10 Upload binary (non-UTF8) data",
fn: async () => {
const binary = randomBytes(512);
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const { hash } = await uploadChunk(PROVIDER_URL, bucketId, binary, nonce);
const downloaded = await downloadChunk(PROVIDER_URL, hash);
assert.deepStrictEqual(new Uint8Array(downloaded), binary, "Binary roundtrip should match");
Expand All @@ -218,7 +219,7 @@ async function main() {
name: "4.11 Upload identical content twice — different MMR leaves",
fn: async () => {
const data = "duplicate content for e2e";
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const first = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
const second = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
assert.strictEqual(first.hash, second.hash, "Same data should produce same hash");
Expand All @@ -236,7 +237,7 @@ async function main() {
const data = "verify hash computation";
const bytes = new TextEncoder().encode(data);
const expectedHash = toHex(blake2b256(bytes));
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const { hash } = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
assert.strictEqual(hash, expectedHash, "Provider hash should match local blake2-256");
},
Expand Down
13 changes: 7 additions & 6 deletions examples/papi/e2e/05-checkpoint-and-challenges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
challengeOffchain,
claimCheckpointRewards,
configureCheckpointWindow,
currentRelayBlock,
ensureProviderRegistered,
fetchChallengeProof,
fetchCheckpointDuty,
Expand All @@ -28,7 +29,7 @@ import {
submitClientCheckpoint,
submitProviderCheckpoint,
uploadChunk,
waitForBlock,
waitForRelayBlock,
} from "@web3-storage/sdk";
import { ensureSoleAcceptingProvider } from "../support.js";
import { negotiateAndEstablish, runSuite, submitTxExpectFailure, setupChain } from "./helpers.js";
Expand Down Expand Up @@ -57,7 +58,7 @@ async function main() {
});

const payload = `checkpoint-test @ ${Date.now()}`;
const uploadNonce = Number(await api.query.System.Number.getValue());
const uploadNonce = await currentRelayBlock(api);
const upload = await uploadChunk(PROVIDER_URL, bucketId, payload, uploadNonce);
const uploadInfo = {
leafIndex: upload.commit.leaf_indices[0],
Expand All @@ -75,7 +76,7 @@ async function main() {
tests.push({
name: "5.1 Client checkpoint",
fn: async () => {
const ckNonce = Number(await api.query.System.Number.getValue());
const ckNonce = await currentRelayBlock(api);
const ck = await fetchCheckpointSignature(PROVIDER_URL, bucketId, ckNonce);
assert.ok(ck.mmr_root, "Checkpoint should have mmr_root");
const result = await submitClientCheckpoint(api, client, provider, bucketId, ck);
Expand Down Expand Up @@ -131,12 +132,12 @@ async function main() {

// Compute current window with headroom.
const HEADROOM = 8;
let currentBlock = Number(await api.query.System.Number.getValue(READ_OPTS));
let currentBlock = await currentRelayBlock(api);
let windowNum = Math.floor(currentBlock / WINDOW_INTERVAL);
let nextWindowStart = (windowNum + 1) * WINDOW_INTERVAL;
if (nextWindowStart - currentBlock < HEADROOM) {
await waitForBlock(papi, nextWindowStart - 1);
currentBlock = Number(await api.query.System.Number.getValue(READ_OPTS));
await waitForRelayBlock(papi, api, nextWindowStart - 1);
currentBlock = await currentRelayBlock(api);
windowNum = Math.floor(currentBlock / WINDOW_INTERVAL);
}
const window = BigInt(windowNum);
Expand Down
15 changes: 8 additions & 7 deletions examples/papi/e2e/10-edge-cases-and-adversarial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Enum } from "polkadot-api";
import { blake2b256 } from "@polkadot-labs/hdkd-helpers";
import {
endAgreement,
currentRelayBlock,
ensureProviderRegistered,
fetchCheckpointSignature,
freezeBucket,
Expand All @@ -24,7 +25,7 @@ import {
submitClientCheckpoint,
toHex,
uploadChunk,
waitForBlock,
waitForRelayBlock,
} from "@web3-storage/sdk";
import { ensureSoleAcceptingProvider } from "../support.js";
import {
Expand Down Expand Up @@ -108,7 +109,7 @@ async function main() {
provider.address,
READ_OPTS
))!;
await waitForBlock(papi, Number(agreement.expires_at));
await waitForRelayBlock(papi, api, Number(agreement.expires_at));
await endAgreement(api, bob, provider, bucketId, "Pay");
const infoAfter = (await api.query.StorageProvider.Providers.getValue(
provider.address,
Expand All @@ -131,7 +132,7 @@ async function main() {
duration: 100,
});
// freeze_bucket requires a snapshot (checkpoint) to exist.
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
await uploadChunk(PROVIDER_URL, bucketId, "data for snapshot", nonce);
const ck = await fetchCheckpointSignature(PROVIDER_URL, bucketId, nonce);
await submitClientCheckpoint(api, bob, provider, bucketId, ck);
Expand All @@ -153,15 +154,15 @@ async function main() {
duration: 100,
});
// Upload some data.
const nonce1 = Number(await api.query.System.Number.getValue());
const nonce1 = await currentRelayBlock(api);
await uploadChunk(PROVIDER_URL, bucketId, "pre-freeze data", nonce1);
// Checkpoint before freeze.
const ck1 = await fetchCheckpointSignature(PROVIDER_URL, bucketId, nonce1);
await submitClientCheckpoint(api, bob, provider, bucketId, ck1);
// Freeze.
await freezeBucket(api, bob, bucketId);
// Upload more data.
const nonce2 = Number(await api.query.System.Number.getValue());
const nonce2 = await currentRelayBlock(api);
await uploadChunk(PROVIDER_URL, bucketId, "post-freeze data", nonce2);
// Checkpoint after freeze — should still work (captures frozen_start_seq).
const ck2 = await fetchCheckpointSignature(PROVIDER_URL, bucketId, nonce2);
Expand Down Expand Up @@ -213,7 +214,7 @@ async function main() {
const data = "integrity check data for blake2-256";
const bytes = new TextEncoder().encode(data);
const expectedHash = toHex(blake2b256(bytes));
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const { hash } = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
assert.strictEqual(hash, expectedHash, "Provider hash should match local blake2-256");
},
Expand All @@ -227,7 +228,7 @@ async function main() {
duration: 100,
});
const data = "identical content for dedup test";
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const r1 = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
const r2 = await uploadChunk(PROVIDER_URL, bucketId, data, nonce);
assert.strictEqual(r1.hash, r2.hash, "Hashes should match for identical content");
Expand Down
14 changes: 9 additions & 5 deletions examples/papi/full-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
challengeCheckpoint,
challengeOffchain,
connect,
currentRelayBlock,
downloadChunk,
endAgreement,
ensureProviderRegistered,
Expand All @@ -37,7 +38,7 @@ import {
respondToChallenge,
submitClientCheckpoint,
uploadChunk,
waitForBlock,
waitForRelayBlock,
waitForBlockProduction,
waitForChainReady,
waitForNextBlock,
Expand Down Expand Up @@ -65,7 +66,10 @@ async function setupAgreement(
provider: ChainSigner
): Promise<bigint> {
const maxBytes = 1_073_741_824n; // 1 GiB
const duration = 15;
// Relay-chain blocks (6s each). The relay clock keeps ticking while the
// parachain onboards or skips slots, so this needs headroom for steps 2-7
// before the step-8 expiry wait.
const duration = 40;
console.log(
" Negotiating signed terms with provider (max_bytes=%s, duration=%d)...",
maxBytes,
Expand All @@ -92,7 +96,7 @@ async function setupAgreement(

async function uploadAndVerify(api: ParachainApi, bucketId: bigint) {
const payload = `Hello, Web3 Storage! [${new Date().toISOString()}] provider=${PROVIDER_SEED}`;
const nonce = Number(await api.query.System.Number.getValue());
const nonce = await currentRelayBlock(api);
const { hash, data, commit } = await uploadChunk(PROVIDER_URL, bucketId, payload, nonce);
console.log(" Uploaded %d bytes, mmr_root=%s", data.length, commit.mmr_root);

Expand Down Expand Up @@ -129,7 +133,7 @@ async function claimPaymentAfterExpiry(api: ParachainApi, papi: PolkadotClient,
console.log(" Provider balance before:", freeBefore.toString());

console.log(" Waiting for agreement to expire...");
await waitForBlock(papi, expiresAt);
await waitForRelayBlock(papi, api, expiresAt);
await endAgreement(api, client, provider, bucketId, "Pay");

const freeAfter = (
Expand Down Expand Up @@ -209,7 +213,7 @@ async function main() {
console.log(" Challenge defended");

console.log("\n=== Step 5: Submit checkpoint ===");
const ckNonce = Number(await api.query.System.Number.getValue());
const ckNonce = await currentRelayBlock(api);
const ck = await fetchCheckpointSignature(PROVIDER_URL, bucketId, ckNonce);
console.log(" Checkpoint mmr_root:", ck.mmr_root);
console.log(" Checkpoint leaf_count:", ck.leaf_count);
Expand Down
Loading
Loading