Skip to content
Merged
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
48 changes: 30 additions & 18 deletions pallets/transaction-storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,9 +1067,17 @@ pub mod pallet {
}
}

fn preimage_store_renew_valid_transaction(content_hash: ContentHash) -> ValidTransaction {
ValidTransaction::with_tag_prefix("TransactionStorageStoreRenew")
.and_provides(content_hash)
.priority(T::StoreRenewPriority::get())
.longevity(T::StoreRenewLongevity::get())
.into()
}

fn check_store_renew_unsigned(
size: usize,
hash: impl FnOnce() -> ContentHash,
content_hash: impl FnOnce() -> ContentHash,
context: CheckContext,
) -> Result<Option<ValidTransaction>, TransactionValidityError> {
if !Self::data_size_ok(size) {
Expand All @@ -1080,21 +1088,17 @@ pub mod pallet {
return Err(InvalidTransaction::ExhaustsResources.into());
}

let hash = hash();
let content_hash = content_hash();

Self::check_authorization(
AuthorizationScope::Preimage(hash),
AuthorizationScope::Preimage(content_hash),
size as u32,
context.consume_authorization(),
)?;

Ok(context.want_valid_transaction().then(|| {
ValidTransaction::with_tag_prefix("TransactionStorageStoreRenew")
.and_provides(hash)
.priority(T::StoreRenewPriority::get())
.longevity(T::StoreRenewLongevity::get())
.into()
}))
Ok(context
.want_valid_transaction()
.then(|| Self::preimage_store_renew_valid_transaction(content_hash)))
}

fn check_unsigned(
Expand Down Expand Up @@ -1192,23 +1196,31 @@ pub mod pallet {
// This allows anyone to store/renew pre-authorized content without consuming their
// own account authorization.
let consume = context.consume_authorization();
Self::check_authorization(
let used_preimage_auth = Self::check_authorization(
AuthorizationScope::Preimage(content_hash),
size as u32,
consume,
)
.or_else(|_| {
.is_ok();

if !used_preimage_auth {
Self::check_authorization(
AuthorizationScope::Account(who.clone()),
size as u32,
consume,
)
})?;
)?;
}

Ok(context.want_valid_transaction().then(|| ValidTransaction {
priority: T::StoreRenewPriority::get(),
longevity: T::StoreRenewLongevity::get(),
..Default::default()
Ok(context.want_valid_transaction().then(|| {
if used_preimage_auth {
Self::preimage_store_renew_valid_transaction(content_hash)
} else {
ValidTransaction::with_tag_prefix("TransactionStorageCheckedSigned")
.and_provides((who, content_hash))
.priority(T::StoreRenewPriority::get())
.longevity(T::StoreRenewLongevity::get())
.into()
}
}))
}

Expand Down
67 changes: 67 additions & 0 deletions pallets/transaction-storage/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,73 @@ fn preimage_authorize_store_with_cid_config_and_renew() {
});
}

#[test]
fn validate_signed_account_authorization_has_provides_tag() {
new_test_ext().execute_with(|| {
run_to_block(1, || None);
let who = 1u64;
assert_ok!(TransactionStorage::authorize_account(RuntimeOrigin::root(), who, 1, 2000,));

let call = Call::store { data: vec![0u8; 2000] };

// validate_signed still doesn't consume authorization (correct behaviour).
for _ in 0..2 {
assert_ok!(TransactionStorage::validate_signed(&who, &call));
}
assert_eq!(
TransactionStorage::account_authorization_extent(who),
AuthorizationExtent { transactions: 1, bytes: 2000 },
);

let vt = TransactionStorage::validate_signed(&who, &call).unwrap();
assert!(!vt.provides.is_empty(), "validate_signed must emit a `provides` tag");

// Two calls with the same signer + content produce identical tags, confirming
// that the mempool will deduplicate them.
let vt2 = TransactionStorage::validate_signed(&who, &call).unwrap();
assert_eq!(vt.provides, vt2.provides);

// pre_dispatch still enforces the authorization: only the first succeeds.
assert_ok!(TransactionStorage::pre_dispatch_signed(&who, &call));
assert_noop!(
TransactionStorage::pre_dispatch_signed(&who, &call),
InvalidTransaction::Payment,
);

// Now test the preimage-authorized path: signed preimage tags must match unsigned
// preimage tags so the pool deduplicates across both submission types.
let data = vec![0u8; 2000];
let content_hash = blake2_256(&data);
assert_ok!(TransactionStorage::authorize_preimage(
RuntimeOrigin::root(),
content_hash,
2000,
));
// Re-authorize account so validate_signed can fall through if needed.
assert_ok!(TransactionStorage::authorize_account(RuntimeOrigin::root(), who, 1, 2000));

let signed_vt = TransactionStorage::validate_signed(&who, &call).unwrap();
let unsigned_vt = <TransactionStorage as ValidateUnsigned>::validate_unsigned(
TransactionSource::External,
&call,
)
.unwrap();
assert_eq!(
signed_vt.provides, unsigned_vt.provides,
"signed preimage path must produce the same tag as unsigned preimage path"
);

// A different signer submitting the same pre-authorized content must get the same
// tag, proving dedup is content-based, not signer-based.
let other_who = 2u64;
let other_vt = TransactionStorage::validate_signed(&other_who, &call).unwrap();
assert_eq!(
signed_vt.provides, other_vt.provides,
"different signers with same preimage-authorized content must share the same tag"
);
});
}

// ---- Migration tests ----

/// Write old-format `OldTransactionInfo` entries as raw bytes into the `Transactions`
Expand Down
Loading