2021-05-04 02:59:44 -04:00
|
|
|
#![allow(non_snake_case)]
|
|
|
|
|
|
|
|
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
|
2021-05-11 07:14:13 -04:00
|
|
|
use curve25519_dalek::edwards::CompressedEdwardsY;
|
2021-05-04 02:59:44 -04:00
|
|
|
use curve25519_dalek::scalar::Scalar;
|
|
|
|
use hash_edwards_to_edwards::hash_point_to_point;
|
2021-05-09 23:21:40 -04:00
|
|
|
use monero::blockdata::transaction::{ExtraField, KeyImage, SubField, TxOutTarget};
|
2021-05-04 02:59:44 -04:00
|
|
|
use monero::cryptonote::hash::Hashable;
|
2021-05-07 00:53:32 -04:00
|
|
|
use monero::cryptonote::onetime_key::{KeyGenerator, MONERO_MUL_FACTOR};
|
2021-05-09 23:21:40 -04:00
|
|
|
use monero::util::key::H;
|
2021-05-04 03:58:54 -04:00
|
|
|
use monero::util::ringct::{EcdhInfo, RctSig, RctSigBase, RctSigPrunable, RctType};
|
2021-05-09 23:21:40 -04:00
|
|
|
use monero::{
|
|
|
|
PrivateKey, PublicKey, Transaction, TransactionPrefix, TxIn, TxOut, VarInt, ViewPair,
|
|
|
|
};
|
|
|
|
use monero_harness::Monero;
|
2021-05-04 02:59:44 -04:00
|
|
|
use monero_rpc::monerod::{GetOutputsOut, MonerodRpc};
|
2021-05-09 23:21:40 -04:00
|
|
|
use monero_wallet::MonerodClientExt;
|
2021-05-11 03:47:59 -04:00
|
|
|
use rand::{Rng, SeedableRng};
|
2021-05-04 02:59:44 -04:00
|
|
|
use std::convert::TryInto;
|
2021-05-03 23:37:07 -04:00
|
|
|
use std::iter;
|
2021-05-09 23:21:40 -04:00
|
|
|
use testcontainers::clients::Cli;
|
2021-05-03 23:37:07 -04:00
|
|
|
|
2021-05-10 03:25:58 -04:00
|
|
|
#[tokio::test]
|
|
|
|
async fn monerod_integration_test() {
|
|
|
|
let mut rng = rand::rngs::StdRng::from_seed([0u8; 32]);
|
2021-05-03 23:37:07 -04:00
|
|
|
|
2021-05-10 03:25:58 -04:00
|
|
|
let cli = Cli::default();
|
2021-05-09 23:21:40 -04:00
|
|
|
let (monero, _monerod_container, _monero_wallet_rpc_containers) =
|
|
|
|
Monero::new(&cli, vec![]).await.unwrap();
|
|
|
|
|
2021-05-11 03:47:59 -04:00
|
|
|
let signing_key = curve25519_dalek::scalar::Scalar::random(&mut rng);
|
2021-05-10 03:25:58 -04:00
|
|
|
let lock_kp = monero::KeyPair {
|
|
|
|
view: monero::PrivateKey::from_scalar(curve25519_dalek::scalar::Scalar::random(&mut rng)),
|
2021-05-11 03:47:59 -04:00
|
|
|
spend: monero::PrivateKey::from_scalar(signing_key),
|
2021-05-10 03:25:58 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
let lock_amount = 1_000_000_000_000;
|
|
|
|
let fee = 400_000_000;
|
|
|
|
let spend_amount = lock_amount - fee;
|
2021-05-09 23:21:40 -04:00
|
|
|
|
2021-05-10 03:25:58 -04:00
|
|
|
let lock_address = monero::Address::from_keypair(monero::Network::Mainnet, &lock_kp);
|
|
|
|
|
|
|
|
dbg!(lock_address.to_string()); // 45BcRKAHaA4b5A9SdamF2f1w7zk1mKkBPhaqVoDWzuAtMoSAytzm5A6b2fE6ruupkAFmStrQzdojUExt96mR3oiiSKp8Exf
|
|
|
|
|
|
|
|
monero.init_miner().await.unwrap();
|
2021-05-09 23:21:40 -04:00
|
|
|
let wallet = monero.wallet("miner").expect("wallet to exist");
|
|
|
|
|
|
|
|
let transfer = wallet
|
2021-05-10 03:25:58 -04:00
|
|
|
.transfer(&lock_address.to_string(), lock_amount)
|
2021-05-09 23:21:40 -04:00
|
|
|
.await
|
|
|
|
.expect("lock to succeed");
|
|
|
|
|
2021-05-10 03:25:58 -04:00
|
|
|
let client = monero.monerod().client();
|
|
|
|
|
2021-05-09 23:21:40 -04:00
|
|
|
let miner_address = wallet
|
|
|
|
.address()
|
|
|
|
.await
|
|
|
|
.expect("miner address to exist")
|
|
|
|
.address;
|
2021-05-10 03:25:58 -04:00
|
|
|
client
|
2021-05-09 23:21:40 -04:00
|
|
|
.generateblocks(10, miner_address)
|
|
|
|
.await
|
|
|
|
.expect("can generate blocks");
|
|
|
|
|
2021-05-10 03:25:58 -04:00
|
|
|
let lock_tx = transfer.tx_hash.parse().unwrap();
|
2021-05-03 23:37:07 -04:00
|
|
|
|
2021-05-06 03:54:50 -04:00
|
|
|
let o_indexes_response = client.get_o_indexes(lock_tx).await.unwrap();
|
|
|
|
|
2021-05-09 23:21:40 -04:00
|
|
|
let transaction = client
|
|
|
|
.get_transactions(&[lock_tx])
|
|
|
|
.await
|
|
|
|
.unwrap()
|
|
|
|
.pop()
|
|
|
|
.unwrap();
|
2021-05-03 23:37:07 -04:00
|
|
|
|
2021-05-07 00:53:32 -04:00
|
|
|
dbg!(&transaction.prefix.inputs);
|
|
|
|
|
2021-05-06 22:12:24 -04:00
|
|
|
let viewpair = ViewPair::from(&lock_kp);
|
|
|
|
|
2021-05-09 23:21:40 -04:00
|
|
|
let our_output = transaction
|
|
|
|
.check_outputs(&viewpair, 0..1, 0..1)
|
|
|
|
.expect("to have outputs in this transaction")
|
|
|
|
.pop()
|
|
|
|
.expect("to own at least one output");
|
2021-05-06 22:12:24 -04:00
|
|
|
let actual_lock_amount = transaction.get_amount(&viewpair, &our_output).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(actual_lock_amount, lock_amount);
|
2021-05-06 03:54:50 -04:00
|
|
|
|
|
|
|
let real_key_offset = o_indexes_response.o_indexes[our_output.index];
|
2021-05-03 23:37:07 -04:00
|
|
|
|
2021-05-04 03:58:54 -04:00
|
|
|
let (lower, upper) = client.calculate_key_offset_boundaries().await.unwrap();
|
2021-05-03 23:37:07 -04:00
|
|
|
|
|
|
|
let mut key_offsets = Vec::with_capacity(11);
|
|
|
|
key_offsets.push(VarInt(real_key_offset));
|
|
|
|
|
2021-05-05 23:39:05 -04:00
|
|
|
for _ in 0..10 {
|
2021-05-03 23:37:07 -04:00
|
|
|
loop {
|
|
|
|
let decoy_offset = VarInt(rng.gen_range(lower.0, upper.0));
|
|
|
|
|
|
|
|
if key_offsets.contains(&decoy_offset) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
key_offsets.push(decoy_offset);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-07 01:25:38 -04:00
|
|
|
dbg!(&key_offsets);
|
|
|
|
|
2021-05-04 02:59:44 -04:00
|
|
|
let response = client
|
|
|
|
.get_outs(
|
|
|
|
key_offsets
|
|
|
|
.iter()
|
|
|
|
.map(|offset| GetOutputsOut {
|
|
|
|
amount: 0,
|
|
|
|
index: offset.0,
|
|
|
|
})
|
|
|
|
.collect(),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2021-05-07 00:53:32 -04:00
|
|
|
|
|
|
|
dbg!(&response);
|
|
|
|
|
2021-05-04 02:59:44 -04:00
|
|
|
let ring = response
|
|
|
|
.outs
|
|
|
|
.iter()
|
|
|
|
.map(|out| out.key.point.decompress().unwrap())
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.try_into()
|
|
|
|
.unwrap();
|
2021-05-03 23:37:07 -04:00
|
|
|
|
2021-05-04 02:59:44 -04:00
|
|
|
key_offsets.sort();
|
2021-05-03 23:37:07 -04:00
|
|
|
|
|
|
|
let relative_key_offsets = to_relative_offsets(&key_offsets);
|
|
|
|
|
2021-05-07 00:53:32 -04:00
|
|
|
dbg!(&relative_key_offsets);
|
|
|
|
|
2021-05-05 23:39:05 -04:00
|
|
|
let target_address = "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".parse::<monero::Address>().unwrap();
|
|
|
|
|
2021-05-07 01:25:38 -04:00
|
|
|
let ecdh_key_0 = PrivateKey::random(&mut rng);
|
|
|
|
let (ecdh_info_0, out_blinding_0) = EcdhInfo::new_bulletproof(spend_amount, ecdh_key_0.scalar);
|
2021-05-09 23:21:40 -04:00
|
|
|
|
2021-05-07 01:25:38 -04:00
|
|
|
let ecdh_key_1 = PrivateKey::random(&mut rng);
|
|
|
|
let (ecdh_info_1, out_blinding_1) = EcdhInfo::new_bulletproof(spend_amount, ecdh_key_1.scalar);
|
2021-05-09 23:21:40 -04:00
|
|
|
|
2021-05-11 07:21:07 -04:00
|
|
|
let (bulletproof, out_pk) = monero::make_bulletproof(
|
|
|
|
&mut rng,
|
|
|
|
&[spend_amount, 0],
|
|
|
|
&[out_blinding_0, out_blinding_1],
|
|
|
|
)
|
2021-05-09 23:21:40 -04:00
|
|
|
.unwrap();
|
2021-05-06 03:54:50 -04:00
|
|
|
|
2021-05-05 23:39:05 -04:00
|
|
|
let k_image = {
|
|
|
|
let k = lock_kp.spend.scalar;
|
|
|
|
let K = ViewPair::from(&lock_kp).spend.point;
|
2021-05-04 03:58:54 -04:00
|
|
|
|
2021-05-05 23:39:05 -04:00
|
|
|
let k_image = k * hash_point_to_point(K.decompress().unwrap());
|
2021-05-09 23:21:40 -04:00
|
|
|
KeyImage {
|
|
|
|
image: monero::cryptonote::hash::Hash(k_image.compress().to_bytes()),
|
|
|
|
}
|
2021-05-05 23:39:05 -04:00
|
|
|
};
|
2021-05-04 03:58:54 -04:00
|
|
|
|
2021-05-04 02:59:44 -04:00
|
|
|
let prefix = TransactionPrefix {
|
|
|
|
version: VarInt(2),
|
|
|
|
unlock_time: Default::default(),
|
|
|
|
inputs: vec![TxIn::ToKey {
|
|
|
|
amount: VarInt(0),
|
|
|
|
key_offsets: relative_key_offsets,
|
2021-05-05 23:39:05 -04:00
|
|
|
k_image,
|
2021-05-04 02:59:44 -04:00
|
|
|
}],
|
2021-05-09 23:21:40 -04:00
|
|
|
outputs: vec![
|
|
|
|
TxOut {
|
|
|
|
amount: VarInt(0),
|
|
|
|
target: TxOutTarget::ToKey {
|
|
|
|
key: KeyGenerator::from_random(
|
|
|
|
target_address.public_view,
|
|
|
|
target_address.public_spend,
|
|
|
|
ecdh_key_0,
|
|
|
|
)
|
|
|
|
.one_time_key(0), // TODO: This must be the output index
|
|
|
|
},
|
2021-05-04 03:58:54 -04:00
|
|
|
},
|
2021-05-09 23:21:40 -04:00
|
|
|
TxOut {
|
|
|
|
amount: VarInt(0),
|
|
|
|
target: TxOutTarget::ToKey {
|
|
|
|
key: KeyGenerator::from_random(
|
|
|
|
target_address.public_view,
|
|
|
|
target_address.public_spend,
|
|
|
|
ecdh_key_1,
|
|
|
|
)
|
2021-05-07 03:48:07 -04:00
|
|
|
.one_time_key(1), // TODO: This must be the output index
|
2021-05-09 23:21:40 -04:00
|
|
|
},
|
2021-05-07 01:25:38 -04:00
|
|
|
},
|
2021-05-09 23:21:40 -04:00
|
|
|
],
|
|
|
|
extra: ExtraField(vec![
|
|
|
|
SubField::TxPublicKey(PublicKey::from_private_key(&ecdh_key_0)),
|
|
|
|
SubField::TxPublicKey(PublicKey::from_private_key(&ecdh_key_1)),
|
|
|
|
]),
|
2021-05-04 02:59:44 -04:00
|
|
|
};
|
|
|
|
|
2021-05-09 23:21:40 -04:00
|
|
|
// assert_eq!(prefix.hash(),
|
|
|
|
// "c3ded4d1a8cddd4f76c09b63edff4e312e759b3afc46beda4e1fd75c9c68d997".parse().
|
|
|
|
// unwrap());
|
2021-05-07 00:53:32 -04:00
|
|
|
|
2021-05-11 03:47:59 -04:00
|
|
|
let signing_key = signing_key
|
2021-05-09 23:21:40 -04:00
|
|
|
+ KeyGenerator::from_key(&viewpair, our_output.tx_pubkey)
|
|
|
|
.get_rvn_scalar(our_output.index)
|
|
|
|
.scalar;
|
2021-05-07 03:48:07 -04:00
|
|
|
|
2021-05-10 05:28:00 -04:00
|
|
|
let commitment_ring = response
|
|
|
|
.outs
|
|
|
|
.iter()
|
|
|
|
.map(|out| CompressedEdwardsY(out.mask.key).decompress().unwrap())
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.try_into()
|
|
|
|
.unwrap();
|
2021-05-07 00:53:32 -04:00
|
|
|
|
2021-05-09 23:21:40 -04:00
|
|
|
let out_pk = out_pk
|
|
|
|
.into_iter()
|
|
|
|
.map(|p| (p.decompress().unwrap() * Scalar::from(MONERO_MUL_FACTOR)).compress())
|
|
|
|
.collect::<Vec<_>>();
|
2021-05-07 00:53:32 -04:00
|
|
|
|
2021-05-06 22:12:24 -04:00
|
|
|
let fee_key = Scalar::from(fee) * H.point.decompress().unwrap();
|
|
|
|
|
2021-05-07 01:25:38 -04:00
|
|
|
let pseudo_out = fee_key + out_pk[0].decompress().unwrap() + out_pk[1].decompress().unwrap();
|
2021-05-06 22:12:24 -04:00
|
|
|
|
2021-05-10 05:28:00 -04:00
|
|
|
let (_, real_commitment_blinder) = transaction.clone().rct_signatures.sig.unwrap().ecdh_info
|
|
|
|
[our_output.index]
|
|
|
|
.open_commitment(&viewpair, &our_output.tx_pubkey, our_output.index);
|
|
|
|
|
2021-05-11 03:47:59 -04:00
|
|
|
let H_p_pk = hash_point_to_point(signing_key * ED25519_BASEPOINT_POINT);
|
|
|
|
let alpha = Scalar::random(&mut rng);
|
|
|
|
|
|
|
|
let sig = monero_adaptor::clsag::sign(
|
2021-05-10 05:28:00 -04:00
|
|
|
&prefix.hash().to_bytes(),
|
2021-05-11 03:47:59 -04:00
|
|
|
signing_key,
|
|
|
|
H_p_pk,
|
|
|
|
alpha,
|
|
|
|
&ring,
|
|
|
|
&commitment_ring,
|
|
|
|
random_array(|| Scalar::random(&mut rng)),
|
2021-05-11 10:42:30 -04:00
|
|
|
real_commitment_blinder - (out_blinding_0 + out_blinding_1), // * Scalar::from(MONERO_MUL_FACTOR), TODO DOESN'T VERIFY WITH THIS
|
2021-05-11 03:47:59 -04:00
|
|
|
pseudo_out,
|
|
|
|
alpha * ED25519_BASEPOINT_POINT,
|
|
|
|
alpha * H_p_pk,
|
|
|
|
signing_key * H_p_pk,
|
2021-05-10 05:28:00 -04:00
|
|
|
);
|
2021-05-11 10:42:30 -04:00
|
|
|
assert!(monero_adaptor::clsag::verify(
|
|
|
|
&sig,
|
|
|
|
&prefix.hash().to_bytes(),
|
|
|
|
&ring,
|
|
|
|
&commitment_ring,
|
|
|
|
pseudo_out
|
|
|
|
));
|
2021-05-10 05:28:00 -04:00
|
|
|
|
2021-05-11 07:14:13 -04:00
|
|
|
sig.responses.iter().enumerate().for_each(|(i, res)| {
|
|
|
|
println!(
|
|
|
|
r#"epee::string_tools::hex_to_pod("{}", clsag.s[{}]);"#,
|
|
|
|
hex::encode(res.as_bytes()),
|
|
|
|
i
|
|
|
|
);
|
|
|
|
});
|
2021-05-11 10:42:30 -04:00
|
|
|
println!(
|
|
|
|
r#"epee::string_tools::hex_to_pod("{}", clsag.c1);"#,
|
|
|
|
hex::encode(sig.h_0.as_bytes())
|
|
|
|
);
|
|
|
|
println!(
|
|
|
|
r#"epee::string_tools::hex_to_pod("{}", clsag.D);"#,
|
|
|
|
hex::encode(sig.D.compress().as_bytes())
|
|
|
|
);
|
|
|
|
println!(
|
|
|
|
r#"epee::string_tools::hex_to_pod("{}", clsag.I);"#,
|
|
|
|
hex::encode(sig.I.compress().to_bytes())
|
|
|
|
);
|
|
|
|
println!(
|
|
|
|
r#"epee::string_tools::hex_to_pod("{}", msg);"#,
|
|
|
|
hex::encode(&prefix.hash().to_bytes())
|
|
|
|
);
|
2021-05-11 07:14:13 -04:00
|
|
|
|
2021-05-11 10:42:30 -04:00
|
|
|
ring.iter()
|
|
|
|
.zip(commitment_ring.iter())
|
|
|
|
.enumerate()
|
|
|
|
.for_each(|(i, (pk, c))| {
|
|
|
|
println!(
|
|
|
|
r#"epee::string_tools::hex_to_pod("{}", pubs[{}].dest);"#,
|
|
|
|
hex::encode(&pk.compress().to_bytes()),
|
|
|
|
i
|
|
|
|
);
|
|
|
|
println!(
|
|
|
|
r#"epee::string_tools::hex_to_pod("{}", pubs[{}].mask);"#,
|
|
|
|
hex::encode(&c.compress().to_bytes()),
|
|
|
|
i
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
println!(
|
|
|
|
r#"epee::string_tools::hex_to_pod("{}", Cout);"#,
|
|
|
|
hex::encode(pseudo_out.compress().to_bytes())
|
|
|
|
);
|
2021-05-11 07:14:13 -04:00
|
|
|
|
2021-05-06 22:12:24 -04:00
|
|
|
let out_pk = out_pk
|
|
|
|
.iter()
|
|
|
|
.map(|c| monero::util::ringct::CtKey {
|
|
|
|
mask: monero::util::ringct::Key { key: c.to_bytes() },
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
2021-05-05 23:39:05 -04:00
|
|
|
|
2021-05-03 23:37:07 -04:00
|
|
|
let transaction = Transaction {
|
2021-05-04 02:59:44 -04:00
|
|
|
prefix,
|
|
|
|
signatures: Vec::new(),
|
|
|
|
rct_signatures: RctSig {
|
|
|
|
sig: Some(RctSigBase {
|
|
|
|
rct_type: RctType::Clsag,
|
|
|
|
txn_fee: VarInt(fee),
|
|
|
|
pseudo_outs: Vec::new(),
|
2021-05-07 01:25:38 -04:00
|
|
|
ecdh_info: vec![ecdh_info_0, ecdh_info_1],
|
2021-05-04 02:59:44 -04:00
|
|
|
out_pk,
|
|
|
|
}),
|
|
|
|
p: Some(RctSigPrunable {
|
|
|
|
range_sigs: Vec::new(),
|
|
|
|
bulletproofs: vec![bulletproof],
|
|
|
|
MGs: Vec::new(),
|
|
|
|
Clsags: vec![sig.into()],
|
2021-05-09 23:21:40 -04:00
|
|
|
pseudo_outs: vec![monero::util::ringct::Key {
|
|
|
|
key: pseudo_out.compress().0,
|
|
|
|
}],
|
2021-05-04 02:59:44 -04:00
|
|
|
}),
|
2021-05-03 23:37:07 -04:00
|
|
|
},
|
|
|
|
};
|
2021-05-04 03:58:54 -04:00
|
|
|
|
2021-05-06 03:25:53 -04:00
|
|
|
client.send_raw_transaction(transaction).await.unwrap();
|
2021-05-03 23:37:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn to_relative_offsets(offsets: &[VarInt]) -> Vec<VarInt> {
|
|
|
|
let vals = offsets.iter();
|
|
|
|
let next_vals = offsets.iter().skip(1);
|
|
|
|
|
2021-05-04 02:59:44 -04:00
|
|
|
let diffs = vals
|
|
|
|
.zip(next_vals)
|
|
|
|
.map(|(cur, next)| VarInt(next.0 - cur.0));
|
2021-05-03 23:37:07 -04:00
|
|
|
iter::once(offsets[0].clone()).chain(diffs).collect()
|
|
|
|
}
|
|
|
|
|
2021-05-11 03:47:59 -04:00
|
|
|
fn random_array<T: Default + Copy, const N: usize>(rng: impl FnMut() -> T) -> [T; N] {
|
|
|
|
let mut ring = [T::default(); N];
|
|
|
|
ring[..].fill_with(rng);
|
2021-05-04 02:59:44 -04:00
|
|
|
|
2021-05-11 03:47:59 -04:00
|
|
|
ring
|
2021-05-04 02:59:44 -04:00
|
|
|
}
|
|
|
|
|
2021-05-03 23:37:07 -04:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn calculate_relative_key_offsets() {
|
|
|
|
let key_offsets = [
|
|
|
|
VarInt(78),
|
|
|
|
VarInt(81),
|
|
|
|
VarInt(91),
|
|
|
|
VarInt(91),
|
|
|
|
VarInt(96),
|
|
|
|
VarInt(98),
|
|
|
|
VarInt(101),
|
|
|
|
VarInt(112),
|
|
|
|
VarInt(113),
|
|
|
|
VarInt(114),
|
|
|
|
VarInt(117),
|
|
|
|
];
|
|
|
|
|
|
|
|
let relative_offsets = to_relative_offsets(&key_offsets);
|
|
|
|
|
2021-05-11 07:21:07 -04:00
|
|
|
assert_eq!(
|
|
|
|
&relative_offsets,
|
|
|
|
&[
|
|
|
|
VarInt(78),
|
|
|
|
VarInt(3),
|
|
|
|
VarInt(10),
|
|
|
|
VarInt(0),
|
|
|
|
VarInt(5),
|
|
|
|
VarInt(2),
|
|
|
|
VarInt(3),
|
|
|
|
VarInt(11),
|
|
|
|
VarInt(1),
|
|
|
|
VarInt(1),
|
|
|
|
VarInt(3),
|
|
|
|
]
|
|
|
|
)
|
2021-05-03 23:37:07 -04:00
|
|
|
}
|
2021-05-03 21:46:58 -04:00
|
|
|
}
|