Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add commitment input syntactic checks #2133

6 changes: 6 additions & 0 deletions sdk/src/types/block/payload/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ pub enum PayloadError {
MissingCreationSlot,
#[display(fmt = "input count and unlock count mismatch: {input_count} != {unlock_count}")]
InputUnlockCountMismatch { input_count: usize, unlock_count: usize },
#[display(fmt = "missing commitment context input for staking feature")]
MissingCommitmentInputForStakingFeature,
#[display(fmt = "missing commitment context input for block issuer feature")]
MissingCommitmentInputForBlockIssuerFeature,
#[display(fmt = "missing commitment context input for delegation output")]
MissingCommitmentInputForDelegationOutput,
#[from]
TransactionSemantic(TransactionFailureReason),
#[from]
Expand Down
33 changes: 31 additions & 2 deletions sdk/src/types/block/payload/signed_transaction/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ impl TransactionBuilder {
verify_outputs(&outputs, protocol_parameters)?;
}

Ok(Transaction {
let transaction = Transaction {
network_id: self.network_id,
creation_slot,
context_inputs: ContextInputs::from_vec(self.context_inputs)?,
Expand All @@ -196,7 +196,11 @@ impl TransactionBuilder {
capabilities: self.capabilities,
payload: self.payload,
outputs,
})
};

verify_transaction(&transaction)?;

Ok(transaction)
}

/// Finishes a [`TransactionBuilder`] into a [`Transaction`] without protocol
Expand All @@ -213,6 +217,7 @@ pub(crate) type OutputCount = BoundedU16<{ *OUTPUT_COUNT_RANGE.start() }, { *OUT
#[derive(Clone, Debug, Eq, PartialEq, Packable)]
#[packable(unpack_error = PayloadError)]
#[packable(unpack_visitor = ProtocolParameters)]
#[packable(verify_with = verify_transaction_packable)]
pub struct Transaction {
/// The unique value denoting whether the block was meant for mainnet, testnet, or a private network.
#[packable(verify_with = verify_network_id)]
Expand Down Expand Up @@ -439,6 +444,30 @@ fn verify_outputs(outputs: &[Output], visitor: &ProtocolParameters) -> Result<()
Ok(())
}

fn verify_transaction_packable(transaction: &Transaction, _: &ProtocolParameters) -> Result<(), PayloadError> {
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
verify_transaction(transaction)
}

fn verify_transaction(transaction: &Transaction) -> Result<(), PayloadError> {
let has_commitment_input = transaction.context_inputs().commitment().is_some();

for output in transaction.outputs.iter() {
if output.features().is_some_and(|f| f.staking().is_some()) && !has_commitment_input {
return Err(PayloadError::MissingCommitmentInputForStakingFeature);
}

if output.features().is_some_and(|f| f.block_issuer().is_some()) && !has_commitment_input {
return Err(PayloadError::MissingCommitmentInputForBlockIssuerFeature);
}

if output.is_delegation() && !has_commitment_input {
return Err(PayloadError::MissingCommitmentInputForDelegationOutput);
}
}
Alex6323 marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}

#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[non_exhaustive]
pub enum TransactionCapabilityFlag {
Expand Down
6 changes: 0 additions & 6 deletions sdk/src/types/block/semantic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,6 @@ impl<'a> SemanticValidationContext<'a> {
if output.features().block_issuer().is_some() {
let account_id = output.account_id_non_null(&OutputId::new(self.transaction_id, index as u16));

if self.commitment_context_input.is_none() {
DaughterOfMars marked this conversation as resolved.
Show resolved Hide resolved
return Err(TransactionFailureReason::BlockIssuerCommitmentInputMissing);
}
if !bic_context_inputs.contains(&account_id) {
return Err(TransactionFailureReason::BlockIssuanceCreditInputMissing);
}
Expand All @@ -318,9 +315,6 @@ impl<'a> SemanticValidationContext<'a> {
}
}
if output.features().staking().is_some() {
if self.commitment_context_input.is_none() {
return Err(TransactionFailureReason::StakingCommitmentInputMissing);
}
if output.features().block_issuer().is_none() {
return Err(TransactionFailureReason::StakingBlockIssuerFeatureMissing);
}
Expand Down
47 changes: 15 additions & 32 deletions sdk/tests/client/signing/delegation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ use iota_sdk::{
output::DelegationId,
payload::{
signed_transaction::{Transaction, TransactionCapabilityFlag},
SignedTransactionPayload,
PayloadError, SignedTransactionPayload,
},
protocol::iota_mainnet_protocol_parameters,
rand::{address::rand_account_address, output::rand_delegation_id, slot::rand_slot_commitment_id},
semantic::TransactionFailureReason,
slot::SlotCommitmentHash,
unlock::SignatureUnlock,
},
};
Expand Down Expand Up @@ -164,7 +165,7 @@ async fn creation_missing_commitment_input() -> Result<(), Box<dyn std::error::E
end_epoch: 0,
}]);

let transaction = Transaction::builder(protocol_parameters.network_id())
let err = Transaction::builder(protocol_parameters.network_id())
.with_inputs(
inputs
.iter()
Expand All @@ -174,37 +175,10 @@ async fn creation_missing_commitment_input() -> Result<(), Box<dyn std::error::E
.with_outputs(outputs)
.with_creation_slot(slot_index + 1)
.with_capabilities([TransactionCapabilityFlag::BurnMana])
.finish_with_params(&protocol_parameters)?;

let prepared_transaction_data = PreparedTransactionData {
transaction,
inputs_data: inputs,
remainders: Vec::new(),
mana_rewards: Default::default(),
};

let unlocks = secret_manager
.transaction_unlocks(&prepared_transaction_data, &protocol_parameters)
.await?;

assert_eq!(unlocks.len(), 1);
assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND);

let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?;
.finish_with_params(&protocol_parameters)
.unwrap_err();

validate_signed_transaction_payload_length(&tx_payload)?;

let conflict = verify_semantic(
&prepared_transaction_data.inputs_data,
&tx_payload,
prepared_transaction_data.mana_rewards,
protocol_parameters,
);

assert_eq!(
conflict,
Err(TransactionFailureReason::DelegationCommitmentInputMissing)
);
assert_eq!(err, PayloadError::MissingCommitmentInputForDelegationOutput);

Ok(())
}
Expand Down Expand Up @@ -263,6 +237,9 @@ async fn non_null_id_creation() -> Result<(), Box<dyn std::error::Error>> {
.with_outputs(outputs)
.with_creation_slot(slot_index + 1)
.with_capabilities([TransactionCapabilityFlag::BurnMana])
.with_context_inputs([
CommitmentContextInput::new(SlotCommitmentHash::null().into_slot_commitment_id(0)).into(),
])
.finish_with_params(&protocol_parameters)?;

let prepared_transaction_data = PreparedTransactionData {
Expand Down Expand Up @@ -349,6 +326,9 @@ async fn mismatch_amount_creation() -> Result<(), Box<dyn std::error::Error>> {
.with_outputs(outputs)
.with_creation_slot(slot_index + 1)
.with_capabilities([TransactionCapabilityFlag::BurnMana])
.with_context_inputs([
CommitmentContextInput::new(SlotCommitmentHash::null().into_slot_commitment_id(0)).into(),
])
.finish_with_params(&protocol_parameters)?;

let prepared_transaction_data = PreparedTransactionData {
Expand Down Expand Up @@ -435,6 +415,9 @@ async fn non_zero_end_epoch_creation() -> Result<(), Box<dyn std::error::Error>>
.with_outputs(outputs)
.with_creation_slot(slot_index + 1)
.with_capabilities([TransactionCapabilityFlag::BurnMana])
.with_context_inputs([
CommitmentContextInput::new(SlotCommitmentHash::null().into_slot_commitment_id(0)).into(),
])
.finish_with_params(&protocol_parameters)?;

let prepared_transaction_data = PreparedTransactionData {
Expand Down
Loading