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
10 changes: 6 additions & 4 deletions node/src/chain_spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ use midnight_node_ledger_helpers::BlockContext;
use midnight_node_runtime::{
AccountId, BeefyConfig, Block, BridgeConfig, CNightObservationCall, CNightObservationConfig,
CouncilConfig, CouncilMembershipConfig, CrossChainPublic, FederatedAuthorityObservationConfig,
MidnightCall, MidnightConfig, MidnightSystemCall, RuntimeCall, RuntimeGenesisConfig,
MidnightCall, MidnightConfig, MidnightSystemCall, Runtime, RuntimeCall, RuntimeGenesisConfig,
SessionCommitteeManagementConfig, SessionConfig, SidechainConfig, Signature, SystemCall,
SystemParametersConfig, TechnicalCommitteeConfig, TechnicalCommitteeMembershipConfig,
TimestampCall, UncheckedExtrinsic, WASM_BINARY, opaque::SessionKeys,
};

use frame_system::offchain::CreateAuthorizedTransaction;
use midnight_primitives_cnight_observation::ObservedUtxos;
use sc_chain_spec::{ChainSpecExtension, GenericChainSpec};
use sidechain_domain::{AssetName, MainchainAddress};
Expand Down Expand Up @@ -134,9 +135,10 @@ pub fn get_chainspec_extrinsics(
for tx in txs {
match tx.tx {
RawTransaction::Midnight(midnight_tx) => {
let extrinsic = UncheckedExtrinsic::new_bare(RuntimeCall::Midnight(
MidnightCall::send_mn_transaction { midnight_tx },
));
let extrinsic = UncheckedExtrinsic::new_transaction(
RuntimeCall::Midnight(MidnightCall::send_mn_transaction { midnight_tx }),
<Runtime as CreateAuthorizedTransaction<RuntimeCall>>::create_extension(),
);
extrinsics.push(hex::encode(extrinsic.encode()));
},
RawTransaction::System(midnight_system_tx) => {
Expand Down
146 changes: 70 additions & 76 deletions pallets/midnight/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ pub mod pallet {
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(Pallet::<T>::get_tx_weight(midnight_tx))]
#[pallet::weight_of_authorize(Weight::zero())]
#[pallet::authorize(Self::authorize_send_mn_transaction)]
pub fn send_mn_transaction(_origin: OriginFor<T>, midnight_tx: Vec<u8>) -> DispatchResult {
let state_key = StateKey::<T>::get();
let block_context = Self::get_block_context();
Expand Down Expand Up @@ -430,57 +432,6 @@ pub mod pallet {
}
}

#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
match source {
TransactionSource::InBlock => {
let Call::send_mn_transaction { midnight_tx } = call else {
return Err(Self::invalid_transaction(Default::default()));
};

// Substrate's Bare extrinsic path runs pallet pre_dispatch before the
// CheckWeight extension, so we pre-check here to avoid expensive ledger
// validation for txs that won't fit in the block.
Self::check_weight(call)?;

let block_context = Self::get_block_context();
let state_key = StateKey::<T>::get();
let runtime_version = <frame_system::Pallet<T>>::runtime_version().spec_version;

LedgerApi::validate_guaranteed_execution(
&state_key,
midnight_tx,
block_context,
runtime_version,
)
.map_err(|e| Self::invalid_transaction(e.into()))?;
Ok(ValidTransaction::default())
},
TransactionSource::Local | TransactionSource::External => {
let mut block_context = Self::get_block_context();
let slot_duration: u64 = T::SlotDuration::get().unique_saturated_into();
let slot_duration_secs = slot_duration.saturating_div(1000);

// Simulate the expected next block time during validation.
// This is needed to avoid potential `OutOfDustValidityWindow` tx validation errors where `ctime > tblock`.
// During transaction pool validation, the stored Timestamp still corresponds to the last produced block.
// Validity is increased by `slot_duration_secs * MaxSkippedSlots` to prevent the node
// from rejecting potentially valid transactions if an AURA block production slots are skipped.
let skipped_slots_margin =
slot_duration_secs.saturating_mul(MaxSkippedSlots::<T>::get() as u64);
block_context.tblock = block_context
.tblock
.saturating_add(slot_duration_secs)
.saturating_add(skipped_slots_margin);

Self::validate_unsigned(call, block_context)
},
}
}
}

// grcov-excl-start
impl<T: Config> Pallet<T> {
pub fn initialize_state(network_id: &str, state_key: &[u8]) {
Expand Down Expand Up @@ -522,6 +473,52 @@ pub mod pallet {
}
// grcov-excl-stop

/// Authorization for general (`AuthorizeCall`) Midnight transactions — pool vs block inclusion.
fn authorize_send_mn_transaction(
source: TransactionSource,
midnight_tx: &Vec<u8>,
) -> TransactionValidityWithRefund {
let call = Call::send_mn_transaction { midnight_tx: (*midnight_tx).clone() };
match source {
TransactionSource::InBlock => {
// General-transaction pipeline runs extensions after authorization; mirror the
// old bare `pre_dispatch` ordering by checking weight before ledger work.
Self::check_weight(&call)?;
let block_context = Self::get_block_context();
let state_key = StateKey::<T>::get();
let runtime_version = <frame_system::Pallet<T>>::runtime_version().spec_version;
LedgerApi::validate_guaranteed_execution(
&state_key,
midnight_tx,
block_context,
runtime_version,
)
.map_err(|e| Self::invalid_transaction(e.into()))?;
Ok((ValidTransaction::default(), Weight::zero()))
},
TransactionSource::Local | TransactionSource::External => {
let mut block_context = Self::get_block_context();
let slot_duration: u64 = T::SlotDuration::get().unique_saturated_into();
let slot_duration_secs = slot_duration.saturating_div(1000);

// Simulate the expected next block time during validation.
// This is needed to avoid potential `OutOfDustValidityWindow` tx validation errors where `ctime > tblock`.
// During transaction pool validation, the stored Timestamp still corresponds to the last produced block.
// Validity is increased by `slot_duration_secs * MaxSkippedSlots` to prevent the node
// from rejecting potentially valid transactions if an AURA block production slots are skipped.
let skipped_slots_margin =
slot_duration_secs.saturating_mul(MaxSkippedSlots::<T>::get() as u64);
block_context.tblock = block_context
.tblock
.saturating_add(slot_duration_secs)
.saturating_add(skipped_slots_margin);

Self::validate_midnight_for_external_pool(midnight_tx, block_context)
.map(|valid| (valid, Weight::zero()))
},
}
}

/// Early block weight check to avoid expensive ledger validation for
/// transactions that won't fit. It is slightly more relaxed than
/// `frame_system::extensions::check_weight::calculate_consumed_weight`.
Expand Down Expand Up @@ -561,31 +558,28 @@ pub mod pallet {
TransactionValidityError::Invalid(InvalidTransaction::Custom(error_code))
}

fn validate_unsigned(call: &Call<T>, block_context: BlockContext) -> TransactionValidity {
if let Call::send_mn_transaction { midnight_tx } = call {
let state_key = StateKey::<T>::get();
let runtime_version = <frame_system::Pallet<T>>::runtime_version().spec_version;
let max_weight = T::BlockWeights::get().max_block.ref_time();

let tx_hash = LedgerApi::validate_transaction(
&state_key,
midnight_tx,
block_context,
runtime_version,
max_weight,
)
.map_err(|e| Self::invalid_transaction(e.into()))?;

ValidTransaction::with_tag_prefix("Midnight")
// Transactions can live in the pool for max 600 blocks before they must be revalidated
.longevity(600)
.and_provides(tx_hash)
.build()
} else {
// grcov-excl-start
Err(Self::invalid_transaction(Default::default()))
// grcov-excl-stop
}
fn validate_midnight_for_external_pool(
midnight_tx: &[u8],
block_context: BlockContext,
) -> TransactionValidity {
let state_key = StateKey::<T>::get();
let runtime_version = <frame_system::Pallet<T>>::runtime_version().spec_version;
let max_weight = T::BlockWeights::get().max_block.ref_time();

let tx_hash = LedgerApi::validate_transaction(
&state_key,
midnight_tx,
block_context,
runtime_version,
max_weight,
)
.map_err(|e| Self::invalid_transaction(e.into()))?;

ValidTransaction::with_tag_prefix("Midnight")
// Transactions can live in the pool for max 600 blocks before they must be revalidated
.longevity(600)
.and_provides(tx_hash)
.build()
}

pub fn get_unclaimed_amount(beneficiary: &[u8]) -> Result<u128, LedgerApiError> {
Expand Down
69 changes: 38 additions & 31 deletions pallets/midnight/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
use super::*;
use crate::{
Call as MidnightCall, mock,
mock::{RuntimeOrigin, Test},
mock::{RuntimeCall, RuntimeOrigin, Test},
};
use assert_matches::assert_matches;
use frame_support::{assert_err, assert_ok, pallet_prelude::Weight, traits::OnFinalize};
use frame_support::{
assert_err, assert_ok,
pallet_prelude::Weight,
traits::{Authorize, OnFinalize},
};
use frame_system::RawOrigin;
use midnight_node_ledger::types::active_version::{
BlockContext, DeserializationError, LedgerApiError, MalformedError, TransactionError,
Expand All @@ -29,9 +33,8 @@ use midnight_node_res::{
CHECK_TX, CONTRACT_ADDR, DEPLOY_TX, MAINTENANCE_TX, STORE_TX, ZSWAP_TX,
},
};
use sp_runtime::{
traits::ValidateUnsigned,
transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError},
use sp_runtime::transaction_validity::{
InvalidTransaction, TransactionSource, TransactionValidityError,
};
use test_log::test;

Expand Down Expand Up @@ -144,30 +147,30 @@ fn test_validation_works() {
let (tx, block_context) =
midnight_node_ledger_helpers::ledger_8::extract_tx_with_context(DEPLOY_TX);

let call = MidnightCall::send_mn_transaction { midnight_tx: tx };
let call = RuntimeCall::Midnight(MidnightCall::send_mn_transaction { midnight_tx: tx });
mock::new_test_ext().execute_with(|| {
init_ledger_state(block_context.into());

assert_ok!(<mock::Midnight as ValidateUnsigned>::validate_unsigned(
TransactionSource::External,
&call
));
let auth = call
.authorize(TransactionSource::External)
.expect("send_mn_transaction must authorize");
assert_ok!(auth);
})
}

#[test]
fn test_validation_fails() {
let call = MidnightCall::send_mn_transaction { midnight_tx: vec![1, 2, 3] };
let call =
RuntimeCall::Midnight(MidnightCall::send_mn_transaction { midnight_tx: vec![1, 2, 3] });

mock::new_test_ext().execute_with(|| {
init_ledger_state(BlockContext::default());

let auth = call
.authorize(TransactionSource::External)
.expect("send_mn_transaction must authorize");
assert_err!(
<mock::Midnight as ValidateUnsigned>::validate_unsigned(
TransactionSource::External,
&call
),
//todo here
auth,
TransactionValidityError::Invalid(InvalidTransaction::Custom(
LedgerApiError::Deserialization(DeserializationError::Transaction).into()
))
Expand All @@ -180,12 +183,14 @@ fn test_pre_dispatch_accepts_valid_transaction() {
let (tx, block_context) =
midnight_node_ledger_helpers::ledger_8::extract_tx_with_context(DEPLOY_TX);

let call = MidnightCall::send_mn_transaction { midnight_tx: tx };
let call = RuntimeCall::Midnight(MidnightCall::send_mn_transaction { midnight_tx: tx });
mock::new_test_ext().execute_with(|| {
init_ledger_state(block_context.into());

// pre_dispatch should succeed for a valid transaction
assert_ok!(<mock::Midnight as ValidateUnsigned>::pre_dispatch(&call));
let auth = call
.authorize(TransactionSource::InBlock)
.expect("send_mn_transaction must authorize");
assert_ok!(auth);
})
}

Expand All @@ -197,13 +202,12 @@ fn test_pre_dispatch_rejects_contract_not_present() {
let (tx, block_context) =
midnight_node_ledger_helpers::ledger_8::extract_tx_with_context(STORE_TX);

let call = MidnightCall::send_mn_transaction { midnight_tx: tx };
let call = RuntimeCall::Midnight(MidnightCall::send_mn_transaction { midnight_tx: tx });
mock::new_test_ext().execute_with(|| {
init_ledger_state(block_context.into());
// Note: DEPLOY_TX not applied - contract doesn't exist

// pre_dispatch should fail because the contract doesn't exist
let result = <mock::Midnight as ValidateUnsigned>::pre_dispatch(&call);
let result = call.authorize(TransactionSource::InBlock).unwrap();
assert!(
result.is_err(),
"pre_dispatch should reject transaction with missing contract dependency"
Expand All @@ -213,14 +217,15 @@ fn test_pre_dispatch_rejects_contract_not_present() {

#[test]
fn test_pre_dispatch_rejects_malformed_transaction() {
let call = MidnightCall::send_mn_transaction { midnight_tx: vec![1, 2, 3] };
let call =
RuntimeCall::Midnight(MidnightCall::send_mn_transaction { midnight_tx: vec![1, 2, 3] });

mock::new_test_ext().execute_with(|| {
init_ledger_state(BlockContext::default());

// pre_dispatch should fail for malformed transaction
let auth = call.authorize(TransactionSource::InBlock).unwrap();
assert_err!(
<mock::Midnight as ValidateUnsigned>::pre_dispatch(&call),
auth,
TransactionValidityError::Invalid(InvalidTransaction::Custom(
LedgerApiError::Deserialization(DeserializationError::Transaction).into()
))
Expand Down Expand Up @@ -253,8 +258,10 @@ fn test_pre_dispatch_rejects_replay_attack() {

// Step 3: Try to replay the same STORE_TX via pre_dispatch
// This should fail because the replay protection counter has been consumed
let call = MidnightCall::send_mn_transaction { midnight_tx: store_tx_clone };
let result = <mock::Midnight as ValidateUnsigned>::pre_dispatch(&call);
let call = RuntimeCall::Midnight(MidnightCall::send_mn_transaction {
midnight_tx: store_tx_clone,
});
let result = call.authorize(TransactionSource::InBlock).unwrap();

// pre_dispatch should reject the replay attempt
assert!(result.is_err(), "pre_dispatch should reject replayed transaction");
Expand All @@ -276,8 +283,8 @@ fn test_pre_dispatch_validation_does_not_modify_state() {
mock::Midnight::get_zswap_state_root().expect("Should be able to get state root");

// Create call and run pre_dispatch
let call = MidnightCall::send_mn_transaction { midnight_tx: tx };
let _result = <mock::Midnight as ValidateUnsigned>::pre_dispatch(&call);
let call = RuntimeCall::Midnight(MidnightCall::send_mn_transaction { midnight_tx: tx });
let _result = call.authorize(TransactionSource::InBlock).unwrap();

// Record state after validation
let state_root_after =
Expand Down Expand Up @@ -306,8 +313,8 @@ fn test_pre_dispatch_validation_does_not_modify_state_on_failure() {
mock::Midnight::get_zswap_state_root().expect("Should be able to get state root");

// Create call and run pre_dispatch (will fail)
let call = MidnightCall::send_mn_transaction { midnight_tx: tx };
let result = <mock::Midnight as ValidateUnsigned>::pre_dispatch(&call);
let call = RuntimeCall::Midnight(MidnightCall::send_mn_transaction { midnight_tx: tx });
let result = call.authorize(TransactionSource::InBlock).unwrap();
assert!(result.is_err(), "pre_dispatch should fail for missing contract");

// Record state after validation
Expand Down
1 change: 1 addition & 0 deletions runtime/src/check_call_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ impl Contains<RuntimeCall> for GovernanceAuthorityCallFilter {
| RuntimeCall::FederatedAuthority(
pallet_federated_authority::Call::motion_close { .. }
) | RuntimeCall::System(frame_system::Call::apply_authorized_upgrade { .. })
| RuntimeCall::Midnight(pallet_midnight::Call::send_mn_transaction { .. })
)
}
}
Expand Down
Loading