Skip to content

Commit cbac0e7

Browse files
committed
Let ChannelSigner set to_local script pubkey
This allows the `to_local` output to easily be changed according to the features of the channel, or the evolution of the LN specification. `to_local` could even be set to completely arbitrary scripts if compatibility with the formal LN spec is not required. Builders of `CommitmentTransaction` now ask a `ChannelSigner` for the appropriate `to_local` script pubkey to use, and then pass it to the `CommitmentTransaction` constructor. External signers now provide the expected `to_local` script pubkey to the `verify` call of `CommitmentTransaction`.
1 parent e12c3e8 commit cbac0e7

File tree

6 files changed

+109
-39
lines changed

6 files changed

+109
-39
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3398,13 +3398,18 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
33983398
&broadcaster_keys, &countersignatory_keys, &self.onchain_tx_handler.secp_ctx);
33993399
let channel_parameters =
34003400
&self.onchain_tx_handler.channel_transaction_parameters.as_counterparty_broadcastable();
3401+
let to_broadcaster_spk = self.onchain_tx_handler.signer.get_revokeable_spk(false, commitment_number, their_per_commitment_point, &self.onchain_tx_handler.secp_ctx);
3402+
let to_broadcaster_txout = TxOut {
3403+
script_pubkey: to_broadcaster_spk,
3404+
value: Amount::from_sat(to_broadcaster_value),
3405+
};
34013406
let counterparty_txout = TxOut {
34023407
script_pubkey: self.counterparty_payment_script.clone(),
34033408
value: Amount::from_sat(to_countersignatory_value),
34043409
};
34053410

34063411
CommitmentTransaction::new_with_auxiliary_htlc_data(commitment_number,
3407-
to_broadcaster_value, counterparty_txout, broadcaster_funding_key,
3412+
to_broadcaster_txout, counterparty_txout, broadcaster_funding_key,
34083413
countersignatory_funding_key, keys, feerate_per_kw, &mut nondust_htlcs,
34093414
channel_parameters)
34103415
}
@@ -3507,15 +3512,11 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
35073512
let secret = self.get_secret(commitment_number).unwrap();
35083513
let per_commitment_key = ignore_error!(SecretKey::from_slice(&secret));
35093514
let per_commitment_point = PublicKey::from_secret_key(&self.onchain_tx_handler.secp_ctx, &per_commitment_key);
3510-
let revocation_pubkey = RevocationKey::from_basepoint(&self.onchain_tx_handler.secp_ctx, &self.holder_revocation_basepoint, &per_commitment_point,);
3511-
let delayed_key = DelayedPaymentKey::from_basepoint(&self.onchain_tx_handler.secp_ctx, &self.counterparty_commitment_params.counterparty_delayed_payment_base_key, &PublicKey::from_secret_key(&self.onchain_tx_handler.secp_ctx, &per_commitment_key));
3512-
3513-
let revokeable_redeemscript = chan_utils::get_revokeable_redeemscript(&revocation_pubkey, self.counterparty_commitment_params.on_counterparty_tx_csv, &delayed_key);
3514-
let revokeable_p2wsh = revokeable_redeemscript.to_p2wsh();
3515+
let revokeable_spk = self.onchain_tx_handler.signer.get_revokeable_spk(false, commitment_number, &per_commitment_point, &self.onchain_tx_handler.secp_ctx);
35153516

35163517
// First, process non-htlc outputs (to_holder & to_counterparty)
35173518
for (idx, outp) in tx.output.iter().enumerate() {
3518-
if outp.script_pubkey == revokeable_p2wsh {
3519+
if outp.script_pubkey == revokeable_spk {
35193520
let revk_outp = RevokedOutput::build(per_commitment_point, self.counterparty_commitment_params.counterparty_delayed_payment_base_key, self.counterparty_commitment_params.counterparty_htlc_base_key, per_commitment_key, outp.value, self.counterparty_commitment_params.on_counterparty_tx_csv, self.onchain_tx_handler.channel_type_features().supports_anchors_zero_fee_htlc_tx());
35203521
let justice_package = PackageTemplate::build_package(
35213522
commitment_txid, idx as u32,

lightning/src/ln/chan_utils.rs

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,12 +1137,17 @@ impl HolderCommitmentTransaction {
11371137
for _ in 0..htlcs.len() {
11381138
counterparty_htlc_sigs.push(dummy_sig);
11391139
}
1140+
let broadcaster_payment_script = signer.get_revokeable_spk(false, 0, &keys.per_commitment_point, &secp_ctx);
1141+
let broadcaster_txout = TxOut {
1142+
script_pubkey: broadcaster_payment_script,
1143+
value: Amount::ZERO,
1144+
};
11401145
let counterparty_payment_script = signer.get_counterparty_payment_script(true);
11411146
let counterparty_txout = TxOut {
11421147
script_pubkey: counterparty_payment_script,
11431148
value: Amount::ZERO,
11441149
};
1145-
let inner = CommitmentTransaction::new_with_auxiliary_htlc_data(0, 0, counterparty_txout, dummy_key.clone(), dummy_key.clone(), keys, 0, htlcs, &channel_parameters.as_counterparty_broadcastable());
1150+
let inner = CommitmentTransaction::new_with_auxiliary_htlc_data(0, broadcaster_txout, counterparty_txout, dummy_key.clone(), dummy_key.clone(), keys, 0, htlcs, &channel_parameters.as_counterparty_broadcastable());
11461151
htlcs.sort_by_key(|htlc| htlc.0.transaction_output_index);
11471152
HolderCommitmentTransaction {
11481153
inner,
@@ -1452,12 +1457,12 @@ impl CommitmentTransaction {
14521457
/// Only include HTLCs that are above the dust limit for the channel.
14531458
///
14541459
/// This is not exported to bindings users due to the generic though we likely should expose a version without
1455-
pub fn new_with_auxiliary_htlc_data<T>(commitment_number: u64, to_broadcaster_value_sat: u64, to_countersignatory_txout: TxOut, broadcaster_funding_key: PublicKey, countersignatory_funding_key: PublicKey, keys: TxCreationKeys, feerate_per_kw: u32, htlcs_with_aux: &mut Vec<(HTLCOutputInCommitment, T)>, channel_parameters: &DirectedChannelTransactionParameters) -> CommitmentTransaction {
1456-
let to_broadcaster_value_sat = Amount::from_sat(to_broadcaster_value_sat);
1460+
pub fn new_with_auxiliary_htlc_data<T>(commitment_number: u64, to_broadcaster_txout: TxOut, to_countersignatory_txout: TxOut, broadcaster_funding_key: PublicKey, countersignatory_funding_key: PublicKey, keys: TxCreationKeys, feerate_per_kw: u32, htlcs_with_aux: &mut Vec<(HTLCOutputInCommitment, T)>, channel_parameters: &DirectedChannelTransactionParameters) -> CommitmentTransaction {
1461+
let to_broadcaster_value_sat = to_broadcaster_txout.value;
14571462
let to_countersignatory_value_sat = to_countersignatory_txout.value;
14581463

14591464
// Sort outputs and populate output indices while keeping track of the auxiliary data
1460-
let (outputs, htlcs) = Self::internal_build_outputs(&keys, to_broadcaster_value_sat, to_countersignatory_txout, htlcs_with_aux, channel_parameters, &broadcaster_funding_key, &countersignatory_funding_key).unwrap();
1465+
let (outputs, htlcs) = Self::internal_build_outputs(&keys, to_broadcaster_txout, to_countersignatory_txout, htlcs_with_aux, channel_parameters, &broadcaster_funding_key, &countersignatory_funding_key).unwrap();
14611466

14621467
let (obscured_commitment_transaction_number, txins) = Self::internal_build_inputs(commitment_number, channel_parameters);
14631468
let transaction = Self::make_transaction(obscured_commitment_transaction_number, txins, outputs);
@@ -1486,15 +1491,19 @@ impl CommitmentTransaction {
14861491
self
14871492
}
14881493

1489-
fn internal_rebuild_transaction(&self, keys: &TxCreationKeys, channel_parameters: &DirectedChannelTransactionParameters, broadcaster_funding_key: &PublicKey, countersignatory_funding_key: &PublicKey, to_countersignatory_spk: ScriptBuf) -> Result<BuiltCommitmentTransaction, ()> {
1494+
fn internal_rebuild_transaction(&self, keys: &TxCreationKeys, channel_parameters: &DirectedChannelTransactionParameters, broadcaster_funding_key: &PublicKey, countersignatory_funding_key: &PublicKey, to_broadcaster_spk: ScriptBuf, to_countersignatory_spk: ScriptBuf) -> Result<BuiltCommitmentTransaction, ()> {
14901495
let (obscured_commitment_transaction_number, txins) = Self::internal_build_inputs(self.commitment_number, channel_parameters);
14911496

1497+
let to_broadcaster_txout = TxOut {
1498+
script_pubkey: to_broadcaster_spk,
1499+
value: self.to_broadcaster_value_sat,
1500+
};
14921501
let to_countersignatory_txout = TxOut {
14931502
script_pubkey: to_countersignatory_spk,
14941503
value: self.to_countersignatory_value_sat,
14951504
};
14961505
let mut htlcs_with_aux = self.htlcs.iter().map(|h| (h.clone(), ())).collect();
1497-
let (outputs, _) = Self::internal_build_outputs(keys, self.to_broadcaster_value_sat, to_countersignatory_txout, &mut htlcs_with_aux, channel_parameters, broadcaster_funding_key, countersignatory_funding_key)?;
1506+
let (outputs, _) = Self::internal_build_outputs(keys, to_broadcaster_txout, to_countersignatory_txout, &mut htlcs_with_aux, channel_parameters, broadcaster_funding_key, countersignatory_funding_key)?;
14981507

14991508
let transaction = Self::make_transaction(obscured_commitment_transaction_number, txins, outputs);
15001509
let txid = transaction.compute_txid();
@@ -1518,9 +1527,7 @@ impl CommitmentTransaction {
15181527
// - initial sorting of outputs / HTLCs in the constructor, in which case T is auxiliary data the
15191528
// caller needs to have sorted together with the HTLCs so it can keep track of the output index
15201529
// - building of a bitcoin transaction during a verify() call, in which case T is just ()
1521-
fn internal_build_outputs<T>(keys: &TxCreationKeys, to_broadcaster_value_sat: Amount, to_countersignatory_txout: TxOut, htlcs_with_aux: &mut Vec<(HTLCOutputInCommitment, T)>, channel_parameters: &DirectedChannelTransactionParameters, broadcaster_funding_key: &PublicKey, countersignatory_funding_key: &PublicKey) -> Result<(Vec<TxOut>, Vec<HTLCOutputInCommitment>), ()> {
1522-
let contest_delay = channel_parameters.contest_delay();
1523-
1530+
fn internal_build_outputs<T>(keys: &TxCreationKeys, to_broadcaster_txout: TxOut, to_countersignatory_txout: TxOut, htlcs_with_aux: &mut Vec<(HTLCOutputInCommitment, T)>, channel_parameters: &DirectedChannelTransactionParameters, broadcaster_funding_key: &PublicKey, countersignatory_funding_key: &PublicKey) -> Result<(Vec<TxOut>, Vec<HTLCOutputInCommitment>), ()> {
15241531
let mut txouts: Vec<(TxOut, Option<&mut HTLCOutputInCommitment>)> = Vec::new();
15251532

15261533
if to_countersignatory_txout.value > Amount::ZERO {
@@ -1530,23 +1537,15 @@ impl CommitmentTransaction {
15301537
))
15311538
}
15321539

1533-
if to_broadcaster_value_sat > Amount::ZERO {
1534-
let redeem_script = get_revokeable_redeemscript(
1535-
&keys.revocation_key,
1536-
contest_delay,
1537-
&keys.broadcaster_delayed_payment_key,
1538-
);
1540+
if to_broadcaster_txout.value > Amount::ZERO {
15391541
txouts.push((
1540-
TxOut {
1541-
script_pubkey: redeem_script.to_p2wsh(),
1542-
value: to_broadcaster_value_sat,
1543-
},
1542+
to_broadcaster_txout.clone(),
15441543
None,
15451544
));
15461545
}
15471546

15481547
if channel_parameters.channel_type_features().supports_anchors_zero_fee_htlc_tx() {
1549-
if to_broadcaster_value_sat > Amount::ZERO || !htlcs_with_aux.is_empty() {
1548+
if to_broadcaster_txout.value > Amount::ZERO || !htlcs_with_aux.is_empty() {
15501549
let anchor_script = get_anchor_redeemscript(broadcaster_funding_key);
15511550
txouts.push((
15521551
TxOut {
@@ -1682,14 +1681,14 @@ impl CommitmentTransaction {
16821681
///
16831682
/// An external validating signer must call this method before signing
16841683
/// or using the built transaction.
1685-
pub fn verify<T: secp256k1::Signing + secp256k1::Verification>(&self, channel_parameters: &DirectedChannelTransactionParameters, broadcaster_keys: &ChannelPublicKeys, countersignatory_keys: &ChannelPublicKeys, secp_ctx: &Secp256k1<T>, to_countersignatory_spk: ScriptBuf) -> Result<TrustedCommitmentTransaction, ()> {
1684+
pub fn verify<T: secp256k1::Signing + secp256k1::Verification>(&self, channel_parameters: &DirectedChannelTransactionParameters, broadcaster_keys: &ChannelPublicKeys, countersignatory_keys: &ChannelPublicKeys, secp_ctx: &Secp256k1<T>, to_broadcaster_spk: ScriptBuf, to_countersignatory_spk: ScriptBuf) -> Result<TrustedCommitmentTransaction, ()> {
16861685
// This is the only field of the key cache that we trust
16871686
let per_commitment_point = self.keys.per_commitment_point;
16881687
let keys = TxCreationKeys::from_channel_static_keys(&per_commitment_point, broadcaster_keys, countersignatory_keys, secp_ctx);
16891688
if keys != self.keys {
16901689
return Err(());
16911690
}
1692-
let tx = self.internal_rebuild_transaction(&keys, channel_parameters, &broadcaster_keys.funding_pubkey, &countersignatory_keys.funding_pubkey, to_countersignatory_spk)?;
1691+
let tx = self.internal_rebuild_transaction(&keys, channel_parameters, &broadcaster_keys.funding_pubkey, &countersignatory_keys.funding_pubkey, to_broadcaster_spk, to_countersignatory_spk)?;
16931692
if self.built.transaction != tx.transaction || self.built.txid != tx.txid {
16941693
return Err(());
16951694
}
@@ -1897,7 +1896,7 @@ mod tests {
18971896
use super::{CounterpartyCommitmentSecrets, ChannelPublicKeys};
18981897
use crate::chain;
18991898
use crate::ln::chan_utils::{get_htlc_redeemscript, get_to_countersignatory_with_anchors_redeemscript, CommitmentTransaction, TxCreationKeys, ChannelTransactionParameters, CounterpartyChannelTransactionParameters, HTLCOutputInCommitment};
1900-
use bitcoin::secp256k1::{PublicKey, SecretKey, Secp256k1};
1899+
use bitcoin::secp256k1::{self, PublicKey, SecretKey, Secp256k1};
19011900
use crate::util::test_utils;
19021901
use crate::sign::{ChannelSigner, SignerProvider};
19031902
use bitcoin::{Amount, TxOut, Network, Txid, ScriptBuf, CompressedPublicKey};
@@ -1921,6 +1920,7 @@ mod tests {
19211920
channel_parameters: ChannelTransactionParameters,
19221921
counterparty_pubkeys: ChannelPublicKeys,
19231922
signer: TestChannelSigner,
1923+
secp_ctx: Secp256k1::<secp256k1::All>,
19241924
}
19251925

19261926
impl TestCommitmentTxBuilder {
@@ -1957,18 +1957,24 @@ mod tests {
19571957
channel_parameters,
19581958
counterparty_pubkeys,
19591959
signer,
1960+
secp_ctx,
19601961
}
19611962
}
19621963

19631964
fn build(&mut self, to_broadcaster_sats: u64, to_countersignatory_sats: u64) -> CommitmentTransaction {
1965+
let broadcaster_payment_script = self.signer.get_revokeable_spk(true, self.commitment_number, &self.keys.per_commitment_point, &self.secp_ctx);
1966+
let broadcaster_txout = TxOut {
1967+
script_pubkey: broadcaster_payment_script,
1968+
value: Amount::from_sat(to_broadcaster_sats),
1969+
};
19641970
let counterparty_payment_script = self.signer.get_counterparty_payment_script(false);
19651971
let counterparty_txout = TxOut {
19661972
script_pubkey: counterparty_payment_script,
19671973
value: Amount::from_sat(to_countersignatory_sats),
19681974
};
19691975
CommitmentTransaction::new_with_auxiliary_htlc_data(
19701976
self.commitment_number,
1971-
to_broadcaster_sats,
1977+
broadcaster_txout,
19721978
counterparty_txout,
19731979
self.holder_funding_pubkey.clone(),
19741980
self.counterparty_funding_pubkey.clone(),

lightning/src/ln/channel.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3164,13 +3164,18 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
31643164
let channel_parameters =
31653165
if local { self.channel_transaction_parameters.as_holder_broadcastable() }
31663166
else { self.channel_transaction_parameters.as_counterparty_broadcastable() };
3167+
let broadcaster_payment_script = self.holder_signer.as_ref().get_revokeable_spk(local, commitment_number, &keys.per_commitment_point, &self.secp_ctx);
3168+
let broadcaster_txout = TxOut {
3169+
script_pubkey: broadcaster_payment_script,
3170+
value: Amount::from_sat(value_to_a as u64),
3171+
};
31673172
let counterparty_payment_script = self.holder_signer.as_ref().get_counterparty_payment_script(!local);
31683173
let counterparty_txout = TxOut {
31693174
script_pubkey: counterparty_payment_script,
31703175
value: Amount::from_sat(value_to_b as u64),
31713176
};
31723177
let tx = CommitmentTransaction::new_with_auxiliary_htlc_data(commitment_number,
3173-
value_to_a as u64,
3178+
broadcaster_txout,
31743179
counterparty_txout,
31753180
funding_pubkey_a,
31763181
funding_pubkey_b,

lightning/src/ln/functional_tests.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,11 @@ fn test_update_fee_that_funder_cannot_afford() {
766766
|phase| if let ChannelPhase::Funded(chan) = phase { Some(chan) } else { None }
767767
).flatten().unwrap();
768768
let local_chan_signer = local_chan.get_signer();
769+
let broadcaster_payment_script = local_chan_signer.as_ref().get_revokeable_spk(false, INITIAL_COMMITMENT_NUMBER - 1, &commit_tx_keys.per_commitment_point, &secp_ctx);
770+
let broadcaster_txout = TxOut {
771+
script_pubkey: broadcaster_payment_script,
772+
value: Amount::from_sat(push_sats),
773+
};
769774
let counterparty_payment_script = local_chan_signer.as_ref().get_counterparty_payment_script(true);
770775
let counterparty_txout = TxOut {
771776
script_pubkey: counterparty_payment_script,
@@ -774,7 +779,7 @@ fn test_update_fee_that_funder_cannot_afford() {
774779
let mut htlcs: Vec<(HTLCOutputInCommitment, ())> = vec![];
775780
let commitment_tx = CommitmentTransaction::new_with_auxiliary_htlc_data(
776781
INITIAL_COMMITMENT_NUMBER - 1,
777-
push_sats,
782+
broadcaster_txout,
778783
counterparty_txout,
779784
local_funding, remote_funding,
780785
commit_tx_keys.clone(),
@@ -1522,14 +1527,19 @@ fn test_fee_spike_violation_fails_htlc() {
15221527
|phase| if let ChannelPhase::Funded(chan) = phase { Some(chan) } else { None }
15231528
).flatten().unwrap();
15241529
let local_chan_signer = local_chan.get_signer();
1530+
let broadcaster_payment_script = local_chan_signer.as_ref().get_revokeable_spk(false, commitment_number, &commit_tx_keys.per_commitment_point, &secp_ctx);
1531+
let broadcaster_txout = TxOut {
1532+
script_pubkey: broadcaster_payment_script,
1533+
value: Amount::from_sat(95000),
1534+
};
15251535
let counterparty_payment_script = local_chan_signer.as_ref().get_counterparty_payment_script(true);
15261536
let counterparty_txout = TxOut {
15271537
script_pubkey: counterparty_payment_script,
15281538
value: Amount::from_sat(local_chan_balance),
15291539
};
15301540
let commitment_tx = CommitmentTransaction::new_with_auxiliary_htlc_data(
15311541
commitment_number,
1532-
95000,
1542+
broadcaster_txout,
15331543
counterparty_txout,
15341544
local_funding, remote_funding,
15351545
commit_tx_keys.clone(),

lightning/src/sign/mod.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ use crate::crypto::chacha20::ChaCha20;
6464
use crate::io::{self, Error};
6565
use crate::ln::msgs::DecodeError;
6666
use crate::prelude::*;
67+
use crate::sign::chan_utils::TxCreationKeys;
6768
use crate::sign::ecdsa::EcdsaChannelSigner;
6869
#[cfg(taproot)]
6970
use crate::sign::taproot::TaprootChannelSigner;
@@ -796,13 +797,28 @@ pub trait ChannelSigner {
796797
/// channel_parameters.is_populated() MUST be true.
797798
fn provide_channel_parameters(&mut self, channel_parameters: &ChannelTransactionParameters);
798799

799-
/// Returns the scriptpubkey that should be placed in the `to_remote` output of commitment
800-
/// transactions. Assumes the signer has already been given the channel parameters via
800+
/// Returns the script pubkey that should be placed in the `to_remote` output of commitment
801+
/// transactions.
802+
///
803+
/// Assumes the signer has already been given the channel parameters via
801804
/// `provide_channel_parameters`.
802805
///
803-
/// If `to_self` is set, return the `to_remote` script pubkey for the counterparty's commitment
806+
/// If `to_self` is set, return the `to_remote` script pubkey for the remote party's commitment
804807
/// transaction, otherwise, for the local party's.
805808
fn get_counterparty_payment_script(&self, to_self: bool) -> ScriptBuf;
809+
810+
/// Returns the script pubkey that should be placed in the `to_local` output of commitment
811+
/// transactions, and in the output of second level HTLC transactions.
812+
///
813+
/// Assumes the signer has already been given the channel parameters via
814+
/// `provide_channel_parameters`.
815+
///
816+
/// If `to_self` is set, return the revokeable script pubkey for local party's
817+
/// commitment / htlc transaction, otherwise, for the remote party's.
818+
fn get_revokeable_spk(
819+
&self, to_self: bool, commitment_number: u64, per_commitment_point: &PublicKey,
820+
secp_ctx: &Secp256k1<secp256k1::All>,
821+
) -> ScriptBuf;
806822
}
807823

808824
/// Specifies the recipient of an invoice.
@@ -1405,6 +1421,30 @@ impl ChannelSigner for InMemorySigner {
14051421
let payment_point = &params.countersignatory_pubkeys().payment_point;
14061422
get_counterparty_payment_script(params.channel_type_features(), payment_point)
14071423
}
1424+
1425+
fn get_revokeable_spk(
1426+
&self, to_self: bool, _commitment_number: u64, per_commitment_point: &PublicKey,
1427+
secp_ctx: &Secp256k1<secp256k1::All>,
1428+
) -> ScriptBuf {
1429+
let params = if to_self {
1430+
self.channel_parameters.as_ref().unwrap().as_holder_broadcastable()
1431+
} else {
1432+
self.channel_parameters.as_ref().unwrap().as_counterparty_broadcastable()
1433+
};
1434+
let contest_delay = params.contest_delay();
1435+
let keys = TxCreationKeys::from_channel_static_keys(
1436+
per_commitment_point,
1437+
params.broadcaster_pubkeys(),
1438+
params.countersignatory_pubkeys(),
1439+
secp_ctx,
1440+
);
1441+
get_revokeable_redeemscript(
1442+
&keys.revocation_key,
1443+
contest_delay,
1444+
&keys.broadcaster_delayed_payment_key,
1445+
)
1446+
.to_p2wsh()
1447+
}
14081448
}
14091449

14101450
const MISSING_PARAMS_ERR: &'static str =

0 commit comments

Comments
 (0)