瀏覽代碼

feat: add input and output limits to txs (#1588)

* feat: add input and output limits to txs

* feat: add tests for too many inputs/outputs
tsk 1 月之前
父節點
當前提交
029dd1f920

+ 16 - 0
crates/cdk-common/src/error.rs

@@ -196,6 +196,22 @@ pub enum Error {
     /// Duplicate output
     #[error("Duplicate outputs")]
     DuplicateOutputs,
+    /// Maximum number of inputs exceeded
+    #[error("Maximum inputs exceeded: {actual} provided, max {max}")]
+    MaxInputsExceeded {
+        /// Actual number of inputs provided
+        actual: usize,
+        /// Maximum allowed inputs
+        max: usize,
+    },
+    /// Maximum number of outputs exceeded
+    #[error("Maximum outputs exceeded: {actual} provided, max {max}")]
+    MaxOutputsExceeded {
+        /// Actual number of outputs provided
+        actual: usize,
+        /// Maximum allowed outputs
+        max: usize,
+    },
     /// Multiple units provided
     #[error("Cannot have multiple units")]
     MultipleUnits,

+ 1 - 0
crates/cdk-integration-tests/src/bin/start_regtest_mints.rs

@@ -281,6 +281,7 @@ fn create_ldk_settings(
             logging: LoggingConfig::default(),
         },
         mint_info: cdk_mintd::config::MintInfo::default(),
+        limits: cdk_mintd::config::Limits::default(),
         ln: cdk_mintd::config::Ln {
             ln_backend: cdk_mintd::config::LnBackend::LdkNode,
             invoice_description: None,

+ 10 - 0
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -282,6 +282,10 @@ pub fn setup_tracing() {
 }
 
 pub async fn create_and_start_test_mint() -> Result<Mint> {
+    create_mint_with_limits(None).await
+}
+
+pub async fn create_mint_with_limits(limits: Option<(usize, usize)>) -> Result<Mint> {
     // Read environment variable to determine database type
     let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set");
 
@@ -330,6 +334,12 @@ pub async fn create_and_start_test_mint() -> Result<Mint> {
         .with_description("pure test mint".to_string())
         .with_urls(vec!["https://aaa".to_string()]);
 
+    if let Some((max_inputs, max_outputs)) = limits {
+        mint_builder = mint_builder.with_limits(max_inputs, max_outputs);
+    } else {
+        mint_builder = mint_builder.with_limits(2000, 2000);
+    }
+
     let quote_ttl = QuoteTTL::new(10000, 10000);
 
     let mint = mint_builder

+ 3 - 0
crates/cdk-integration-tests/src/shared.rs

@@ -217,6 +217,7 @@ pub fn create_fake_wallet_settings(
             enable_swagger_ui: None,
         },
         mint_info: cdk_mintd::config::MintInfo::default(),
+        limits: cdk_mintd::config::Limits::default(),
         ln: cdk_mintd::config::Ln {
             ln_backend: cdk_mintd::config::LnBackend::FakeWallet,
             invoice_description: None,
@@ -271,6 +272,7 @@ pub fn create_cln_settings(
             enable_swagger_ui: None,
         },
         mint_info: cdk_mintd::config::MintInfo::default(),
+        limits: cdk_mintd::config::Limits::default(),
         ln: cdk_mintd::config::Ln {
             ln_backend: cdk_mintd::config::LnBackend::Cln,
             invoice_description: None,
@@ -320,6 +322,7 @@ pub fn create_lnd_settings(
             enable_swagger_ui: None,
         },
         mint_info: cdk_mintd::config::MintInfo::default(),
+        limits: cdk_mintd::config::Limits::default(),
         ln: cdk_mintd::config::Ln {
             ln_backend: cdk_mintd::config::LnBackend::Lnd,
             invoice_description: None,

+ 262 - 0
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -748,6 +748,268 @@ async fn test_mint_enforce_fee() {
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_max_outputs_exceeded_mint() {
+    setup_tracing();
+    // Set max outputs to 5
+    let mint_bob = create_mint_with_limits(Some((100, 5)))
+        .await
+        .expect("Failed to create test mint");
+
+    let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
+        .await
+        .expect("Failed to create test wallet");
+
+    // Alice tries to mint 10 sats with split target 1 (requesting 10 outputs)
+    // This should fail because max_outputs is 5
+    let result = fund_wallet(
+        wallet_alice.clone(),
+        10,
+        Some(SplitTarget::Value(Amount::ONE)),
+    )
+    .await;
+
+    match result {
+        Ok(_) => panic!("Mint allowed exceeding max outputs"),
+        Err(err) => {
+            if let Some(cdk::Error::MaxOutputsExceeded { actual, max }) =
+                err.downcast_ref::<cdk::Error>()
+            {
+                // actual might be more than 10 depending on internal splitting logic, but certainly > 5
+                assert!(*actual >= 10);
+                assert_eq!(*max, 5);
+            } else {
+                panic!("Wrong error returned: {:?}", err);
+            }
+        }
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_max_inputs_exceeded_melt() {
+    setup_tracing();
+    // Set max inputs to 5
+    let mint_bob = create_mint_with_limits(Some((5, 100)))
+        .await
+        .expect("Failed to create test mint");
+
+    let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
+        .await
+        .expect("Failed to create test wallet");
+
+    // Alice gets 10 sats with small outputs to have enough proofs
+    fund_wallet(
+        wallet_alice.clone(),
+        10,
+        Some(SplitTarget::Value(Amount::ONE)),
+    )
+    .await
+    .expect("Failed to fund wallet");
+
+    let proofs = wallet_alice
+        .get_unspent_proofs()
+        .await
+        .expect("Could not get proofs");
+
+    // Use 6 proofs (limit is 5)
+    let six_proofs: Vec<_> = proofs.iter().take(6).cloned().collect();
+    assert_eq!(six_proofs.len(), 6);
+
+    let fake_invoice = create_fake_invoice(1000, "".to_string());
+    let melt_quote = wallet_alice
+        .melt_quote(PaymentMethod::BOLT11, fake_invoice.to_string(), None, None)
+        .await
+        .expect("Failed to create melt quote");
+
+    let melt_request = MeltRequest::new(melt_quote.id.parse().unwrap(), six_proofs, None);
+
+    match mint_bob.melt(&melt_request).await {
+        Ok(_) => panic!("Melt allowed exceeding max inputs"),
+        Err(err) => match err {
+            cdk::Error::MaxInputsExceeded { actual, max } => {
+                assert_eq!(actual, 6);
+                assert_eq!(max, 5);
+            }
+            _ => panic!("Wrong error returned: {:?}", err),
+        },
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_max_outputs_exceeded_melt() {
+    setup_tracing();
+    // Set max outputs to 20
+    let mint_bob = create_mint_with_limits(Some((100, 20)))
+        .await
+        .expect("Failed to create test mint");
+
+    let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
+        .await
+        .expect("Failed to create test wallet");
+
+    // Alice gets 100 sats
+    fund_wallet(wallet_alice.clone(), 100, None)
+        .await
+        .expect("Failed to fund wallet");
+
+    let proofs = wallet_alice
+        .get_unspent_proofs()
+        .await
+        .expect("Could not get proofs");
+
+    let fake_invoice = create_fake_invoice(1000, "".to_string()); // 1000 msat = 1 sat
+    let melt_quote = wallet_alice
+        .melt_quote(PaymentMethod::BOLT11, fake_invoice.to_string(), None, None)
+        .await
+        .expect("Failed to create melt quote");
+
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
+    let keyset_id = keys.id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
+
+    // Create 21 blinded messages for change (limit is 20)
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        21.into(),
+        &SplitTarget::Value(Amount::ONE),
+        &fee_and_amounts,
+    )
+    .unwrap();
+
+    let change_messages = preswap.blinded_messages();
+    assert!(change_messages.len() >= 21);
+    let excessive_change: Vec<_> = change_messages.into_iter().take(21).collect();
+
+    let melt_request = MeltRequest::new(
+        melt_quote.id.parse().unwrap(),
+        proofs,
+        Some(excessive_change),
+    );
+
+    match mint_bob.melt(&melt_request).await {
+        Ok(_) => panic!("Melt allowed exceeding max outputs"),
+        Err(err) => match err {
+            cdk::Error::MaxOutputsExceeded { actual, max } => {
+                assert_eq!(actual, 21);
+                assert_eq!(max, 20);
+            }
+            _ => panic!("Wrong error returned: {:?}", err),
+        },
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_max_inputs_exceeded() {
+    setup_tracing();
+    let mint_bob = create_mint_with_limits(Some((5, 100)))
+        .await
+        .expect("Failed to create test mint");
+
+    let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
+        .await
+        .expect("Failed to create test wallet");
+
+    // Alice gets 100 sats with small outputs to have enough proofs
+    fund_wallet(
+        wallet_alice.clone(),
+        100,
+        Some(SplitTarget::Value(Amount::ONE)),
+    )
+    .await
+    .expect("Failed to fund wallet");
+
+    let proofs = wallet_alice
+        .get_unspent_proofs()
+        .await
+        .expect("Could not get proofs");
+
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
+    let keyset_id = keys.id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
+
+    // Use 6 proofs (limit is 5)
+    let six_proofs: Vec<_> = proofs.iter().take(6).cloned().collect();
+    assert_eq!(six_proofs.len(), 6);
+
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        6.into(),
+        &SplitTarget::default(),
+        &fee_and_amounts,
+    )
+    .unwrap();
+
+    let swap_request = SwapRequest::new(six_proofs, preswap.blinded_messages());
+
+    match mint_bob.process_swap_request(swap_request).await {
+        Ok(_) => panic!("Swap allowed exceeding max inputs"),
+        Err(err) => match err {
+            cdk::Error::MaxInputsExceeded { actual, max } => {
+                assert_eq!(actual, 6);
+                assert_eq!(max, 5);
+            }
+            _ => panic!("Wrong error returned: {:?}", err),
+        },
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_mint_max_outputs_exceeded() {
+    setup_tracing();
+    // Set max outputs to 20
+    let mint_bob = create_mint_with_limits(Some((100, 20)))
+        .await
+        .expect("Failed to create test mint");
+
+    let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
+        .await
+        .expect("Failed to create test wallet");
+
+    // Alice gets 50 sats
+    fund_wallet(wallet_alice.clone(), 50, None)
+        .await
+        .expect("Failed to fund wallet");
+
+    let proofs = wallet_alice
+        .get_unspent_proofs()
+        .await
+        .expect("Could not get proofs");
+
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
+    let keyset_id = keys.id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
+
+    // Try to split into 21 outputs (limit is 20)
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        21.into(),
+        &SplitTarget::Value(Amount::ONE),
+        &fee_and_amounts,
+    )
+    .unwrap();
+
+    // Verify we generated enough messages
+    let messages = preswap.blinded_messages();
+    // We expect 21 messages because we asked for split target of 1 sat for 21 sats total.
+    assert!(messages.len() >= 21);
+
+    // Just take 21 messages to trigger the limit
+    let excessive_messages: Vec<_> = messages.into_iter().take(21).collect();
+
+    let swap_request = SwapRequest::new(proofs, excessive_messages);
+
+    match mint_bob.process_swap_request(swap_request).await {
+        Ok(_) => panic!("Swap allowed exceeding max outputs"),
+        Err(err) => match err {
+            cdk::Error::MaxOutputsExceeded { actual, max } => {
+                assert_eq!(actual, 21);
+                assert_eq!(max, 20);
+            }
+            _ => panic!("Wrong error returned: {:?}", err),
+        },
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_mint_change_with_fee_melt() {
     setup_tracing();
     let mint_bob = create_and_start_test_mint()

+ 7 - 0
crates/cdk-mintd/example.config.toml

@@ -202,3 +202,10 @@ max_delay_time = 3
 # swap = "blind"
 # restore = "blind"
 # check_proof_state = "none"
+
+# Transaction limits for DoS protection (optional, defaults shown)
+[limits]
+# Maximum number of inputs allowed per transaction (swap/melt)
+max_inputs = 1000
+# Maximum number of outputs allowed per transaction (mint/swap/melt)
+max_outputs = 1000

+ 31 - 0
crates/cdk-mintd/src/config.rs

@@ -559,6 +559,9 @@ pub struct Settings {
     pub info: Info,
     pub mint_info: MintInfo,
     pub ln: Ln,
+    /// Transaction limits for DoS protection
+    #[serde(default)]
+    pub limits: Limits,
     #[cfg(feature = "cln")]
     pub cln: Option<Cln>,
     #[cfg(feature = "lnbits")]
@@ -588,6 +591,34 @@ pub struct Prometheus {
     pub port: Option<u16>,
 }
 
+/// Transaction limits configuration
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Limits {
+    /// Maximum number of inputs allowed per transaction (swap/melt)
+    #[serde(default = "default_max_inputs")]
+    pub max_inputs: usize,
+    /// Maximum number of outputs allowed per transaction (mint/swap/melt)
+    #[serde(default = "default_max_outputs")]
+    pub max_outputs: usize,
+}
+
+impl Default for Limits {
+    fn default() -> Self {
+        Self {
+            max_inputs: 1000,
+            max_outputs: 1000,
+        }
+    }
+}
+
+fn default_max_inputs() -> usize {
+    1000
+}
+
+fn default_max_outputs() -> usize {
+    1000
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
 pub struct MintInfo {
     /// name of the mint and should be recognizable

+ 29 - 0
crates/cdk-mintd/src/env_vars/limits.rs

@@ -0,0 +1,29 @@
+//! Transaction limits environment variables
+
+use std::env;
+
+use crate::config::Limits;
+
+pub const ENV_MAX_INPUTS: &str = "CDK_MINTD_MAX_INPUTS";
+pub const ENV_MAX_OUTPUTS: &str = "CDK_MINTD_MAX_OUTPUTS";
+
+impl Limits {
+    /// Override limits with environment variables if set
+    pub fn from_env(&self) -> Self {
+        let mut limits = self.clone();
+
+        if let Ok(max_inputs_str) = env::var(ENV_MAX_INPUTS) {
+            if let Ok(max_inputs) = max_inputs_str.parse::<usize>() {
+                limits.max_inputs = max_inputs;
+            }
+        }
+
+        if let Ok(max_outputs_str) = env::var(ENV_MAX_OUTPUTS) {
+            if let Ok(max_outputs) = max_outputs_str.parse::<usize>() {
+                limits.max_outputs = max_outputs;
+            }
+        }
+
+        limits
+    }
+}

+ 3 - 0
crates/cdk-mintd/src/env_vars/mod.rs

@@ -7,6 +7,7 @@
 mod common;
 mod database;
 mod info;
+mod limits;
 mod ln;
 mod mint_info;
 
@@ -45,6 +46,7 @@ pub use fake_wallet::*;
 pub use grpc_processor::*;
 #[cfg(feature = "ldk-node")]
 pub use ldk_node::*;
+pub use limits::*;
 pub use ln::*;
 #[cfg(feature = "lnbits")]
 pub use lnbits::*;
@@ -94,6 +96,7 @@ impl Settings {
         self.info = self.info.clone().from_env();
         self.mint_info = self.mint_info.clone().from_env();
         self.ln = self.ln.clone().from_env();
+        self.limits = self.limits.clone().from_env();
 
         #[cfg(feature = "auth")]
         {

+ 4 - 0
crates/cdk-mintd/src/lib.rs

@@ -368,6 +368,10 @@ async fn configure_mint_builder(
     // Configure caching with payment methods
     let mint_builder = configure_cache(settings, mint_builder, &payment_methods);
 
+    // Configure transaction limits
+    let mint_builder =
+        mint_builder.with_limits(settings.limits.max_inputs, settings.limits.max_outputs);
+
     Ok(mint_builder)
 }
 

+ 15 - 0
crates/cdk/src/mint/builder.rs

@@ -38,6 +38,8 @@ pub struct MintBuilder {
     supported_units: HashMap<CurrencyUnit, (u64, u8)>,
     custom_paths: HashMap<CurrencyUnit, DerivationPath>,
     use_keyset_v2: Option<bool>,
+    max_inputs: usize,
+    max_outputs: usize,
 }
 
 impl std::fmt::Debug for MintBuilder {
@@ -74,6 +76,8 @@ impl MintBuilder {
             supported_units: HashMap::new(),
             custom_paths: HashMap::new(),
             use_keyset_v2: None,
+            max_inputs: 100,
+            max_outputs: 100,
         }
     }
 
@@ -253,6 +257,13 @@ impl MintBuilder {
         self
     }
 
+    /// Set transaction limits for DoS protection
+    pub fn with_limits(mut self, max_inputs: usize, max_outputs: usize) -> Self {
+        self.max_inputs = max_inputs;
+        self.max_outputs = max_outputs;
+        self
+    }
+
     /// Add payment processor
     pub async fn add_payment_processor(
         &mut self,
@@ -463,6 +474,8 @@ impl MintBuilder {
                 self.localstore,
                 auth_localstore,
                 self.payment_processors,
+                self.max_inputs,
+                self.max_outputs,
             )
             .await;
         }
@@ -471,6 +484,8 @@ impl MintBuilder {
             signatory,
             self.localstore,
             self.payment_processors,
+            self.max_inputs,
+            self.max_outputs,
         )
         .await
     }

+ 10 - 0
crates/cdk/src/mint/issue/mod.rs

@@ -674,6 +674,16 @@ impl Mint {
             }
         };
 
+        // Check max outputs limit
+        let outputs_count = mint_request.outputs.len();
+        if outputs_count > self.max_outputs {
+            tracing::warn!("Mint request exceeds max outputs limit: {} > {}", outputs_count, self.max_outputs);
+            return Err(Error::MaxOutputsExceeded {
+                actual: outputs_count,
+                max: self.max_outputs,
+            });
+        }
+
         // Get unit from the typed outputs amount
         let unit = outputs_amount.unit().clone();
         ensure_cdk!(unit == mint_quote.unit, Error::UnsupportedUnit);

+ 30 - 0
crates/cdk/src/mint/melt/melt_saga/mod.rs

@@ -199,6 +199,36 @@ impl MeltSaga<Initial> {
         } = input_verification;
         let input_unit = Some(input_amount.unit().clone());
 
+        // Check max inputs limit
+        let inputs_count = melt_request.inputs().len();
+        if inputs_count > self.mint.max_inputs {
+            tracing::warn!(
+                "Melt request exceeds max inputs limit: {} > {}",
+                inputs_count,
+                self.mint.max_inputs
+            );
+            return Err(Error::MaxInputsExceeded {
+                actual: inputs_count,
+                max: self.mint.max_inputs,
+            });
+        }
+
+        // Check max outputs limit (if change outputs are provided)
+        if let Some(outputs) = melt_request.outputs() {
+            let outputs_count = outputs.len();
+            if outputs_count > self.mint.max_outputs {
+                tracing::warn!(
+                    "Melt request exceeds max outputs limit: {} > {}",
+                    outputs_count,
+                    self.mint.max_outputs
+                );
+                return Err(Error::MaxOutputsExceeded {
+                    actual: outputs_count,
+                    max: self.mint.max_outputs,
+                });
+            }
+        }
+
         let mut tx = self.db.begin_transaction().await?;
 
         let mut quote =

+ 26 - 3
crates/cdk/src/mint/mod.rs

@@ -79,6 +79,10 @@ pub struct Mint {
     keysets: Arc<ArcSwap<Vec<SignatoryKeySet>>>,
     /// Background task management
     task_state: Arc<Mutex<TaskState>>,
+    /// Maximum number of inputs allowed per transaction
+    max_inputs: usize,
+    /// Maximum number of outputs allowed per transaction
+    max_outputs: usize,
 }
 
 impl std::fmt::Debug for Mint {
@@ -103,6 +107,8 @@ impl Mint {
         signatory: Arc<dyn Signatory + Send + Sync>,
         localstore: DynMintDatabase,
         payment_processors: HashMap<PaymentProcessorKey, DynMintPayment>,
+        max_inputs: usize,
+        max_outputs: usize,
     ) -> Result<Self, Error> {
         Self::new_internal(
             mint_info,
@@ -111,6 +117,8 @@ impl Mint {
             #[cfg(feature = "auth")]
             None,
             payment_processors,
+            max_inputs,
+            max_outputs,
         )
         .await
     }
@@ -123,6 +131,8 @@ impl Mint {
         localstore: DynMintDatabase,
         auth_localstore: DynMintAuthDatabase,
         payment_processors: HashMap<PaymentProcessorKey, DynMintPayment>,
+        max_inputs: usize,
+        max_outputs: usize,
     ) -> Result<Self, Error> {
         Self::new_internal(
             mint_info,
@@ -130,6 +140,8 @@ impl Mint {
             localstore,
             Some(auth_localstore),
             payment_processors,
+            max_inputs,
+            max_outputs,
         )
         .await
     }
@@ -142,6 +154,8 @@ impl Mint {
         localstore: DynMintDatabase,
         #[cfg(feature = "auth")] auth_localstore: Option<DynMintAuthDatabase>,
         payment_processors: HashMap<PaymentProcessorKey, DynMintPayment>,
+        max_inputs: usize,
+        max_outputs: usize,
     ) -> Result<Self, Error> {
         let keysets = signatory.keysets().await?;
         if !keysets
@@ -243,6 +257,8 @@ impl Mint {
             auth_localstore,
             keysets: Arc::new(ArcSwap::new(keysets.keysets.into())),
             task_state: Arc::new(Mutex::new(TaskState::default())),
+            max_inputs,
+            max_outputs,
         })
     }
 
@@ -1123,9 +1139,16 @@ mod tests {
                 .unwrap();
         }
 
-        Mint::new(MintInfo::default(), signatory, localstore, HashMap::new())
-            .await
-            .unwrap()
+        Mint::new(
+            MintInfo::default(),
+            signatory,
+            localstore,
+            HashMap::new(),
+            1000,
+            1000,
+        )
+        .await
+        .unwrap()
     }
 
     #[tokio::test]

+ 28 - 0
crates/cdk/src/mint/swap/mod.rs

@@ -39,6 +39,34 @@ impl Mint {
             ));
         }
 
+        // Check max inputs limit
+        let inputs_count = input_proofs.len();
+        if inputs_count > self.max_inputs {
+            tracing::warn!(
+                "Swap request exceeds max inputs limit: {} > {}",
+                inputs_count,
+                self.max_inputs
+            );
+            return Err(Error::MaxInputsExceeded {
+                actual: inputs_count,
+                max: self.max_inputs,
+            });
+        }
+
+        // Check max outputs limit
+        let outputs_count = swap_request.outputs().len();
+        if outputs_count > self.max_outputs {
+            tracing::warn!(
+                "Swap request exceeds max outputs limit: {} > {}",
+                outputs_count,
+                self.max_outputs
+            );
+            return Err(Error::MaxOutputsExceeded {
+                actual: outputs_count,
+                max: self.max_outputs,
+            });
+        }
+
         // We don't need to check P2PK or HTLC again. It has all been checked above
         // and the code doesn't reach here unless such verifications were satisfactory