Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 17 additions & 2 deletions docs/001_authorizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Block-throughput reference numbers:
| Sustained max throughput | **~127 GiB/day**, **~1.73 TiB / 2 weeks** |
| `RetentionPeriod` | `2 × 100 800` blocks = 14 days |
| `AuthorizationPeriod` | `14 days` (Westend / Paseo configs) |
| `GracePeriod` | `14 days` (Westend / Paseo configs) |

### 1. Wasted block space (soft)

Expand Down Expand Up @@ -62,7 +63,21 @@ Per existing entry state:

### `refresh_account_authorization`

Extends `expiration` by another `AuthorizationPeriod` and leaves all consumed counters (`bytes`, `bytes_permanent`, `transactions`) untouched. Refresh does **not** grant additional capacity. To start a fresh window, let the authorization expire and re-authorize. Origin is `T::Authorizer` (e.g. PoP).
Extends `expiration` (and `grace_until`) by another `AuthorizationPeriod` (+ `GracePeriod`) and leaves all consumed counters (`bytes`, `bytes_permanent`, `transactions`) untouched. Refresh does **not** grant additional capacity. To start a fresh window, let the authorization expire and re-authorize. Origin is `T::Authorizer` (e.g. PoP).

## Lifecycle: Active → In grace → Expired

`AuthorizationExtent` carries two block numbers — `expiration` and `grace_until = expiration + GracePeriod`. Lifecycle by `now`:

| State | Condition | `store` | `renew` | `remove_expired_*` |
|---|---|:---:|:---:|:---:|
| **Active** | `now < expiration` | ✔ | ✔ | rejected |
| **In grace** | `expiration ≤ now < grace_until` | ✘ (`Payment`) | ✔ | rejected |
| **Expired** | `now ≥ grace_until` | ✘ | ✘ | ✔ |

Grace is a forgiveness window for `renew` only: it lets the holder keep already-stored content on chain after the authorization expires, instead of losing auth and data simultaneously. `store` is rejected throughout grace, and `remove_expired_account_authorization` waits for `grace_until` so it can't race a still-permitted `renew`. `authorize_account` against an in-grace or fully-expired entry hits the expired-but-present branch (re-grant + reset).

`GracePeriod` is a `Config` constant; setting it to `0` collapses the lifecycle back to Active → Expired.

## Soft limit (priority boost)

Expand Down Expand Up @@ -187,7 +202,7 @@ A user across many accounts (Sybil-like) is bounded by the chain-wide cap (Examp

## Migration

`STORAGE_VERSION = 3`. Migrations are only relevant for the Paseo/Westend testnets carrying pre-existing on-chain state forward; see the `pallet_bulletin_transaction_storage::migrations::{v1, v2, v3}` modules for the wiring.
`STORAGE_VERSION = 4`. Migrations are only relevant for the Paseo/Westend testnets carrying pre-existing on-chain state forward; see the `pallet_bulletin_transaction_storage::migrations::{v1, v2, v3, v4}` modules for the wiring. `v4` tail-extends `Authorization` with `grace_until = expiration + GracePeriod`.

## Capacity planning operational steps

Expand Down
2 changes: 2 additions & 0 deletions pallets/hop-promotion/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl pallet_timestamp::Config for Test {

parameter_types! {
pub const AuthorizationPeriod: BlockNumberFor<Test> = 10;
pub const GracePeriod: BlockNumberFor<Test> = 0;
pub const StoreRenewPriority: TransactionPriority = TransactionPriority::MAX;
pub const StoreRenewLongevity: TransactionLongevity = 10;
pub const RemoveExpiredAuthorizationPriority: TransactionPriority = TransactionPriority::MAX;
Expand Down Expand Up @@ -78,6 +79,7 @@ impl pallet_bulletin_transaction_storage::Config for Test {
type MaxTransactionSize = ConstU32<TEST_MAX_TRANSACTION_SIZE>;
type MaxPermanentStorageSize = ConstU64<{ u64::MAX }>;
type AuthorizationPeriod = AuthorizationPeriod;
type GracePeriod = GracePeriod;
type Authorizer = EnsureRoot<Self::AccountId>;
type StoreRenewPriority = StoreRenewPriority;
type StoreRenewLongevity = StoreRenewLongevity;
Expand Down
8 changes: 5 additions & 3 deletions pallets/transaction-storage/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,10 @@ mod benchmarks {

// `AuthorizationPeriod` is ~14 days of blocks on real runtimes; iterating
// `on_initialize`/`on_finalize` for each is ~1.3M no-op iterations per step.
// The dispatchable only compares `block_number >= expiration`, so we can jump
// The dispatchable only compares `block_number >= grace_until`, so we can jump
// the system block number directly without running intermediate block hooks.
let period = T::AuthorizationPeriod::get();
// Jump past `grace_until = expiration + GracePeriod`.
let period = T::AuthorizationPeriod::get() + T::GracePeriod::get();
let now = System::<T>::block_number();
System::<T>::set_block_number(now + period);

Expand All @@ -275,7 +276,8 @@ mod benchmarks {
TransactionStorage::<T>::authorize_preimage(origin, content_hash, 1)
.map_err(|_| BenchmarkError::Stop("unable to authorize preimage"))?;

let period = T::AuthorizationPeriod::get();
// Jump past `grace_until = expiration + GracePeriod`.
let period = T::AuthorizationPeriod::get() + T::GracePeriod::get();
let now = System::<T>::block_number();
System::<T>::set_block_number(now + period);

Expand Down
37 changes: 28 additions & 9 deletions pallets/transaction-storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ pub mod pallet {
/// Authorizations expire after this many blocks.
#[pallet::constant]
type AuthorizationPeriod: Get<BlockNumberFor<Self>>;
/// After `expiration`, the authorization stays in a renew-only grace window for
/// this many blocks. `0` disables grace (lifecycle collapses to Active → Expired).
#[pallet::constant]
type GracePeriod: Get<BlockNumberFor<Self>>;
/// The origin that can authorize data storage.
type Authorizer: EnsureOrigin<Self::RuntimeOrigin>;
/// Priority of store/renew transactions.
Expand Down Expand Up @@ -216,7 +220,7 @@ pub mod pallet {
NotAutoRenewalOwner,
}

const STORAGE_VERSION: StorageVersion = StorageVersion::new(3);
const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -932,6 +936,7 @@ pub mod pallet {
EntryFee::<T>::put(self.entry_fee);
RetentionPeriod::<T>::put(self.retention_period);
let expiration = T::AuthorizationPeriod::get();
let grace_until = expiration.saturating_add(T::GracePeriod::get());
for (who, transactions_allowance, bytes_allowance) in &self.account_authorizations {
let scope = AuthorizationScope::Account(who.clone());
Authorizations::<T>::insert(
Expand All @@ -945,6 +950,7 @@ pub mod pallet {
transactions_allowance: *transactions_allowance,
},
expiration,
grace_until,
},
);
Pallet::<T>::authorization_added(&scope);
Expand All @@ -962,6 +968,7 @@ pub mod pallet {
transactions_allowance: 1,
},
expiration,
grace_until,
},
);
}
Expand Down Expand Up @@ -1309,7 +1316,7 @@ pub mod pallet {
}
}

fn authorization_removed(scope: &AuthorizationScopeFor<T>) {
pub(crate) fn authorization_removed(scope: &AuthorizationScopeFor<T>) {
match scope {
AuthorizationScope::Account(who) => {
// Cleanup nonce storage. Authorized accounts should be careful to use a short
Expand Down Expand Up @@ -1349,14 +1356,17 @@ pub mod pallet {
) {
let now = Self::now();
let expiration = now.saturating_add(T::AuthorizationPeriod::get());
let grace_until = expiration.saturating_add(T::GracePeriod::get());

Authorizations::<T>::mutate(&scope, |maybe_authorization| {
if let Some(authorization) = maybe_authorization {
if authorization.expired(now) {
// Expired-but-present: re-grant the caps, reset all consumed counters.
// The new window's `bytes_permanent` quota is independent of any
// renewed bytes still on chain from the old window.
// Expired-but-present (in-grace or fully expired): re-grant the caps,
// reset all consumed counters. The new window's `bytes_permanent`
// quota is independent of any renewed bytes still on chain from the
// old window.
authorization.expiration = expiration;
authorization.grace_until = grace_until;
authorization.extent.bytes = 0;
authorization.extent.bytes_permanent = 0;
authorization.extent.transactions = 0;
Expand Down Expand Up @@ -1396,6 +1406,7 @@ pub mod pallet {
transactions_allowance,
},
expiration,
grace_until,
});
Self::authorization_added(&scope);
}
Expand All @@ -1409,10 +1420,12 @@ pub mod pallet {
/// expire and re-authorize.
fn refresh_authorization(scope: AuthorizationScopeFor<T>) -> DispatchResult {
let expiration = Self::now().saturating_add(T::AuthorizationPeriod::get());
let grace_until = expiration.saturating_add(T::GracePeriod::get());

Authorizations::<T>::mutate(&scope, |maybe_authorization| {
if let Some(authorization) = maybe_authorization {
authorization.expiration = expiration;
authorization.grace_until = grace_until;
Ok(())
} else {
// No previous authorization to refresh.
Expand All @@ -1421,11 +1434,11 @@ pub mod pallet {
})
}

/// Remove an expired authorization.
/// Remove an authorization once it has passed its grace window (`now >= grace_until`).
fn remove_expired_authorization(scope: AuthorizationScopeFor<T>) -> DispatchResult {
let authorization =
Authorizations::<T>::get(&scope).ok_or(Error::<T>::AuthorizationNotFound)?;
ensure!(authorization.expired(Self::now()), Error::<T>::AuthorizationNotExpired);
ensure!(authorization.past_grace(Self::now()), Error::<T>::AuthorizationNotExpired);
Authorizations::<T>::remove(&scope);
Self::authorization_removed(&scope);
Ok(())
Expand Down Expand Up @@ -1620,7 +1633,11 @@ pub mod pallet {
let Some(authorization) = maybe_authorization else {
return Err(InvalidTransaction::Payment.into())
};
if authorization.expired(now) {
// Past grace: both rejected. In grace: only `renew` survives.
if authorization.past_grace(now) {
return Err(InvalidTransaction::Payment.into())
}
if !is_renew && authorization.expired(now) {
return Err(InvalidTransaction::Payment.into())
}
if is_renew {
Expand Down Expand Up @@ -1676,7 +1693,9 @@ pub mod pallet {
let Some(authorization) = Authorizations::<T>::get(scope) else {
return Err(AUTHORIZATION_NOT_FOUND.into());
};
if !authorization.expired(Self::now()) {
// Cleanup only after grace has elapsed: while in-grace, `renew` is still
// allowed against this entry, so it is not yet safe to remove.
if !authorization.past_grace(Self::now()) {
return Err(AUTHORIZATION_NOT_EXPIRED.into());
}
Ok(())
Expand Down
78 changes: 76 additions & 2 deletions pallets/transaction-storage/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,15 @@ pub mod v2 {
pub expiration: BlockNumber,
}

impl<BlockNumber> From<V2Authorization<BlockNumber>> for Authorization<BlockNumber> {
impl<BlockNumber: Copy> From<V2Authorization<BlockNumber>> for Authorization<BlockNumber> {
fn from(v2: V2Authorization<BlockNumber>) -> Self {
Self { extent: v2.extent, expiration: v2.expiration }
Self {
extent: v2.extent,
expiration: v2.expiration,
// v2 had no grace concept; later migrations (v3→v4) populate this
// with `expiration + GracePeriod`. Until then grace is zero-length.
grace_until: v2.expiration,
}
}
}

Expand Down Expand Up @@ -635,3 +641,71 @@ pub mod v3 {
}
}
}

/// Migration v3→v4: adds `grace_until` to `Authorization`. Existing entries get
/// `grace_until = expiration + GracePeriod` (saturating). New `Authorization`s minted
/// by `authorize` / `refresh_authorization` already set the field directly.
pub mod v4 {
use super::*;
use crate::{
pallet::{Authorizations, Pallet},
Authorization, AuthorizationExtent,
};
use polkadot_sdk_frame::{
deps::frame_support::{migrations::VersionedMigration, traits::UncheckedOnRuntimeUpgrade},
prelude::Saturating,
};

#[derive(Encode, Decode)]
pub(crate) struct V3Authorization<BlockNumber> {
pub extent: AuthorizationExtent,
pub expiration: BlockNumber,
}

pub struct VersionUncheckedMigrateV3ToV4<T>(PhantomData<T>);

impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV3ToV4<T> {
fn on_runtime_upgrade() -> Weight {
let grace = T::GracePeriod::get();
let now = Pallet::<T>::now();
let mut migrated: u64 = 0;
let mut dropped: u64 = 0;
Authorizations::<T>::translate::<V3Authorization<BlockNumberFor<T>>, _>(
|scope, old| {
let grace_until = old.expiration.saturating_add(grace);
// Drop entries that are already past their v4 grace window.
// Account scopes had their provider reference bumped at grant
// time; release it here so we don't leak references.
if now >= grace_until {
Pallet::<T>::authorization_removed(&scope);
dropped = dropped.saturating_add(1);
return None;
}
migrated = migrated.saturating_add(1);
Some(Authorization {
extent: old.extent,
expiration: old.expiration,
grace_until,
})
},
);
tracing::info!(
target: LOG_TARGET,
migrated,
dropped,
"v3->v4 Authorization grace_until migration complete",
);
let touched = migrated.saturating_add(dropped);
T::DbWeight::get().reads_writes(touched, touched)
}
}

/// Versioned migration v3→v4: tail-extends `Authorization` with `grace_until`.
pub type MigrateV3ToV4<T> = VersionedMigration<
3,
4,
VersionUncheckedMigrateV3ToV4<T>,
Pallet<T>,
<T as polkadot_sdk_frame::deps::frame_system::Config>::DbWeight,
>;
}
2 changes: 2 additions & 0 deletions pallets/transaction-storage/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ impl frame_system::Config for Test {

parameter_types! {
pub const AuthorizationPeriod: BlockNumberFor<Test> = 10;
pub storage GracePeriod: BlockNumberFor<Test> = 0;
pub const StoreRenewPriority: TransactionPriority = TransactionPriority::MAX;
pub const StoreRenewLongevity: TransactionLongevity = 10;
pub const RemoveExpiredAuthorizationPriority: TransactionPriority = TransactionPriority::MAX;
Expand All @@ -71,6 +72,7 @@ impl pallet_bulletin_transaction_storage::Config for Test {
type MaxTransactionSize = ConstU32<{ DEFAULT_MAX_TRANSACTION_SIZE }>;
type MaxPermanentStorageSize = MaxPermanentStorageSize;
type AuthorizationPeriod = AuthorizationPeriod;
type GracePeriod = GracePeriod;
type Authorizer = EnsureRoot<Self::AccountId>;
type StoreRenewPriority = StoreRenewPriority;
type StoreRenewLongevity = StoreRenewLongevity;
Expand Down
Loading
Loading