Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7092cf5
Align Types between Bulletin and SDK
raymondkfcheung Dec 9, 2025
cb92c1a
Update from github-actions[bot] running command 'prdoc --audience run…
github-actions[bot] Dec 9, 2025
1d0a8e4
Merge branch 'master' into ray-align-types
raymondkfcheung Dec 10, 2025
fa44622
Update TODO
raymondkfcheung Dec 10, 2025
d95f12b
Merge branch 'master' into ray-align-types
bkontur Dec 10, 2025
c88603f
Sync with Bulletin
raymondkfcheung Dec 10, 2025
9e5e1c8
Merge remote-tracking branch 'origin/ray-align-types' into ray-align-…
raymondkfcheung Dec 10, 2025
7cbac59
Align Storages between Bulletin and SDK
raymondkfcheung Dec 10, 2025
596cb88
Update from github-actions[bot] running command 'prdoc --audience run…
github-actions[bot] Dec 10, 2025
64bb41e
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 10, 2025
5c2de88
Update PRDoc
raymondkfcheung Dec 10, 2025
57c65e4
Revert "Update PRDoc"
raymondkfcheung Dec 10, 2025
1bd7153
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 10, 2025
196d533
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 10, 2025
675c0e8
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 10, 2025
ce4abe0
Update error messages
raymondkfcheung Dec 11, 2025
1f9bacc
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 11, 2025
bfb3041
Fix fmt
raymondkfcheung Dec 11, 2025
b0d3788
Remove unused storages
raymondkfcheung Dec 11, 2025
2fefbfd
Sync with Bulletin
raymondkfcheung Dec 11, 2025
74a0e12
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 11, 2025
a686856
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 11, 2025
2e84d00
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 12, 2025
82c045f
Sync with Bulletin
raymondkfcheung Dec 12, 2025
61d7bbd
Update PRDoc
raymondkfcheung Dec 12, 2025
63049ed
Sync with Bulletin
raymondkfcheung Dec 12, 2025
39ad0cd
Merge branch 'master' into ray-align-storages
raymondkfcheung Dec 12, 2025
4aa0d5d
Sync with Bulletin
raymondkfcheung Dec 12, 2025
ba88242
Remove TransactionTooLarge
raymondkfcheung Dec 12, 2025
582cf00
Add transaction_info
raymondkfcheung Dec 12, 2025
ee622f1
Remove EmptyTransaction
raymondkfcheung Dec 12, 2025
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
7 changes: 7 additions & 0 deletions prdoc/pr_10593.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
title: Align Common Functions between Bulletin and SDK
doc:
- audience: Runtime Dev
description: The PR aligns common functions between Bulletin and SDK.
crates:
- name: pallet-transaction-storage
bump: major
121 changes: 82 additions & 39 deletions substrate/frame/transaction-storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ extern crate alloc;

use alloc::vec::Vec;
use codec::{Decode, Encode, MaxEncodedLen};
use core::{fmt::Debug, result};
use core::fmt::Debug;
use frame_support::{
dispatch::GetDispatchInfo,
pallet_prelude::InvalidTransaction,
Expand All @@ -57,7 +57,7 @@ pub type CreditOf<T> = Credit<<T as frame_system::Config>::AccountId, <T as Conf
pub use pallet::*;
pub use weights::WeightInfo;

// TODO: https://github.com/paritytech/polkadot-sdk/issues/10591 - Clarify purpose of allocator limits and decide whether to remove or use these constants.
// TODO: https://github.com/paritytech/polkadot-bulletin-chain/issues/139 - Clarify purpose of allocator limits and decide whether to remove or use these constants.
/// Maximum bytes that can be stored in one transaction.
// Setting higher limit also requires raising the allocator limit.
pub const DEFAULT_MAX_TRANSACTION_SIZE: u32 = 8 * 1024 * 1024;
Expand Down Expand Up @@ -168,8 +168,10 @@ pub mod pallet {
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
/// Maximum number of indexed transactions in the block.
#[pallet::constant]
type MaxBlockTransactions: Get<u32>;
/// Maximum data set in a single transaction in bytes.
#[pallet::constant]
type MaxTransactionSize: Get<u32>;
}

Expand All @@ -185,8 +187,6 @@ pub mod pallet {
NotConfigured,
/// Renewed extrinsic is not found.
RenewedNotFound,
/// Attempting to store an empty transaction
EmptyTransaction,
/// Proof was not expected in this block.
UnexpectedProof,
/// Proof failed verification.
Expand All @@ -199,8 +199,6 @@ pub mod pallet {
DoubleCheck,
/// Storage proof was not checked in the block.
ProofNotChecked,
/// Transaction is too large.
TransactionTooLarge,
/// Authorization was not found.
AuthorizationNotFound,
/// Authorization has not expired.
Expand Down Expand Up @@ -248,36 +246,58 @@ pub mod pallet {
},
"Storage proof must be checked once in the block"
);

// Insert new transactions, iff they have chunks.
let transactions = BlockTransactions::<T>::take();
let total_chunks = TransactionInfo::total_chunks(&transactions);
if total_chunks != 0 {
Transactions::<T>::insert(n, transactions);
}
}

fn integrity_test() {
assert!(
!T::MaxBlockTransactions::get().is_zero(),
"MaxTransactionSize must be greater than zero"
);
assert!(
!T::MaxTransactionSize::get().is_zero(),
"MaxTransactionSize must be greater than zero"
);
let default_period = sp_transaction_storage_proof::DEFAULT_STORAGE_PERIOD.into();
let storage_period = GenesisConfig::<T>::default().storage_period;
assert_eq!(
storage_period, default_period,
"GenesisConfig.storage_period must match DEFAULT_STORAGE_PERIOD"
);
}
}

#[pallet::call]
impl<T: Config> Pallet<T> {
/// Index and store data off chain. Minimum data size is 1 bytes, maximum is
/// `MaxTransactionSize`. Data will be removed after `STORAGE_PERIOD` blocks, unless `renew`
/// Index and store data off chain. Minimum data size is 1 byte, maximum is
/// `MaxTransactionSize`. Data will be removed after `StoragePeriod` blocks, unless `renew`
/// is called.
///
/// Emits [`Stored`](Event::Stored) when successful.
///
/// ## Complexity
/// - O(n*log(n)) of data size, as all data is pushed to an in-memory trie.
///
/// O(n*log(n)) of data size, as all data is pushed to an in-memory trie.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::store(data.len() as u32))]
pub fn store(origin: OriginFor<T>, data: Vec<u8>) -> DispatchResult {
ensure!(data.len() > 0, Error::<T>::EmptyTransaction);
ensure!(
data.len() <= T::MaxTransactionSize::get() as usize,
Error::<T>::TransactionTooLarge
Comment on lines -270 to -273
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raymondkfcheung shouldn't we remove EmptyTransaction and TransactionTooLarge now?

Copy link
Copy Markdown
Contributor Author

@raymondkfcheung raymondkfcheung Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

Removed TransactionTooLarge: ba88242

Changed to BadDataSize: ee622f1

ensure!(!tx_info.block_chunks.is_zero(), Error::<T>::EmptyTransaction);

);
// In the case of a regular unsigned transaction, this should have been checked by
// pre_dispatch. In the case of a regular signed transaction, this should have been
// checked by pre_dispatch_signed.
Self::ensure_data_size_ok(data.len())?;
let sender = ensure_signed(origin)?;
Self::apply_fee(sender, data.len() as u32)?;

// Chunk data and compute storage root
let chunk_count = num_chunks(data.len() as u32);
let chunks = data.chunks(CHUNK_SIZE).map(|c| c.to_vec()).collect();
let chunks: Vec<_> = data.chunks(CHUNK_SIZE).map(|c| c.to_vec()).collect();
let chunk_count = chunks.len() as u32;
debug_assert_eq!(chunk_count, num_chunks(data.len() as u32));
let root = sp_io::trie::blake2_256_ordered_root(chunks, sp_runtime::StateVersion::V1);

let content_hash = sp_io::hashing::blake2_256(&data);
Expand All @@ -299,19 +319,21 @@ pub mod pallet {
content_hash: content_hash.into(),
block_chunks: total_chunks,
})
.map_err(|_| Error::<T>::TooManyTransactions)?;
Ok(())
.map_err(|_| Error::<T>::TooManyTransactions)
})?;
Self::deposit_event(Event::Stored { index, content_hash });
Ok(())
}

/// Renew previously stored data. Parameters are the block number that contains
/// previous `store` or `renew` call and transaction index within that block.
/// Transaction index is emitted in the `Stored` or `Renewed` event.
/// Applies same fees as `store`.
/// Renew previously stored data. Parameters are the block number that contains previous
/// `store` or `renew` call and transaction index within that block. Transaction index is
/// emitted in the `Stored` or `Renewed` event. Applies same fees as `store`.
///
/// Emits [`Renewed`](Event::Renewed) when successful.
///
/// ## Complexity
/// - O(1).
///
/// O(1).
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::renew())]
pub fn renew(
Expand All @@ -320,11 +342,15 @@ pub mod pallet {
index: u32,
) -> DispatchResultWithPostInfo {
let sender = ensure_signed(origin)?;
let transactions = Transactions::<T>::get(block).ok_or(Error::<T>::RenewedNotFound)?;
let info = transactions.get(index as usize).ok_or(Error::<T>::RenewedNotFound)?;
let info = Self::transaction_info(block, index).ok_or(Error::<T>::RenewedNotFound)?;

// In the case of a regular unsigned transaction, this should have been checked by
// pre_dispatch. In the case of a regular signed transaction, this should have been
// checked by pre_dispatch_signed.
Self::ensure_data_size_ok(info.size as usize)?;

let extrinsic_index =
frame_system::Pallet::<T>::extrinsic_index().ok_or(Error::<T>::BadContext)?;

Self::apply_fee(sender, info.size)?;
let content_hash = info.content_hash.into();
sp_io::transaction_index::renew(extrinsic_index, content_hash);
Expand All @@ -350,12 +376,12 @@ pub mod pallet {
Ok(().into())
}

/// Check storage proof for block number `block_number() - StoragePeriod`.
/// If such a block does not exist, the proof is expected to be `None`.
/// Check storage proof for block number `block_number() - StoragePeriod`. If such a block
/// does not exist, the proof is expected to be `None`.
///
/// ## Complexity
/// - Linear w.r.t the number of indexed transactions in the proved block for random
/// probing.
///
/// Linear w.r.t the number of indexed transactions in the proved block for random probing.
/// There's a DB read for each transaction.
#[pallet::call_index(2)]
#[pallet::weight((T::WeightInfo::check_proof_max(), DispatchClass::Mandatory))]
Expand Down Expand Up @@ -464,9 +490,9 @@ pub mod pallet {
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
ByteFee::<T>::put(&self.byte_fee);
EntryFee::<T>::put(&self.entry_fee);
StoragePeriod::<T>::put(&self.storage_period);
ByteFee::<T>::put(self.byte_fee);
EntryFee::<T>::put(self.entry_fee);
StoragePeriod::<T>::put(self.storage_period);
}
}

Expand All @@ -483,10 +509,7 @@ pub mod pallet {
proof.map(|proof| Call::check_proof { proof })
}

fn check_inherent(
_call: &Self::Call,
_data: &InherentData,
) -> result::Result<(), Self::Error> {
fn check_inherent(_call: &Self::Call, _data: &InherentData) -> Result<(), Self::Error> {
Ok(())
}

Expand Down Expand Up @@ -523,6 +546,26 @@ pub mod pallet {
Ok(())
}

/// Returns `true` if a blob of the given size can be stored.
fn data_size_ok(size: usize) -> bool {
(size > 0) && (size <= T::MaxTransactionSize::get() as usize)
}

/// Ensures that the given data size is valid for storage.
fn ensure_data_size_ok(size: usize) -> Result<(), Error<T>> {
ensure!(Self::data_size_ok(size), Error::<T>::BadDataSize);
Ok(())
}

/// Returns the [`TransactionInfo`] for the specified store/renew transaction.
fn transaction_info(
block_number: BlockNumberFor<T>,
index: u32,
) -> Option<TransactionInfo> {
let transactions = Transactions::<T>::get(block_number)?;
transactions.into_iter().nth(index as usize)
}

/// Verifies that the provided proof corresponds to a randomly selected chunk from a list of
/// transactions.
pub(crate) fn verify_chunk_proof(
Expand All @@ -533,7 +576,7 @@ pub mod pallet {
// Get the random chunk index - from all transactions in the block = [0..total_chunks).
let total_chunks: ChunkIndex = TransactionInfo::total_chunks(&infos);
ensure!(total_chunks != 0, Error::<T>::UnexpectedProof);
let selected_block_chunk_index = random_chunk(random_hash, total_chunks as _);
let selected_block_chunk_index = random_chunk(random_hash, total_chunks);

// Let's find the corresponding transaction and its "local" chunk index for "global"
// `selected_block_chunk_index`.
Expand All @@ -553,7 +596,7 @@ pub mod pallet {
// We shouldn't reach this point; we rely on the fact that `fn store` does not allow
// empty transactions. Without this check, it would fail anyway below with
// `InvalidProof`.
ensure!(!tx_info.block_chunks.is_zero(), Error::<T>::EmptyTransaction);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed: #10593 (comment)

ensure!(!tx_info.block_chunks.is_zero(), Error::<T>::BadDataSize);

// Convert a global chunk index into a transaction-local one.
let tx_chunks = num_chunks(tx_info.size);
Expand Down
8 changes: 4 additions & 4 deletions substrate/frame/transaction-storage/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ fn discards_data() {
let caller = 1;
assert_ok!(TransactionStorage::<Test>::store(
RawOrigin::Signed(caller).into(),
vec![0u8; 2000 as usize]
vec![0u8; 2000]
));
assert_ok!(TransactionStorage::<Test>::store(
RawOrigin::Signed(caller).into(),
vec![0u8; 2000 as usize]
vec![0u8; 2000]
));
let proof_provider = || {
let block_num = frame_system::Pallet::<Test>::block_number();
Expand Down Expand Up @@ -176,7 +176,7 @@ fn renews_data() {
let caller = 1;
assert_noop!(
TransactionStorage::<Test>::store(RawOrigin::Signed(caller).into(), vec![]),
Error::<Test>::EmptyTransaction
Error::<Test>::BadDataSize
);
assert_ok!(TransactionStorage::<Test>::store(
RawOrigin::Signed(caller).into(),
Expand All @@ -201,7 +201,7 @@ fn renews_data() {
};
run_to_block(16, proof_provider);
assert!(Transactions::<Test>::get(1).is_none());
assert_eq!(Transactions::<Test>::get(6).unwrap().get(0), Some(info).as_ref());
assert_eq!(Transactions::<Test>::get(6).unwrap().first(), Some(info).as_ref());
run_to_block(17, proof_provider);
assert!(Transactions::<Test>::get(6).is_none());
});
Expand Down
Loading