use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::Cursor;
use uuid::Uuid;
use crate::grin_core::consensus::valid_header_version;
use crate::grin_core::core::HeaderVersion;
use crate::grin_keychain::{Identifier, Keychain};
use crate::grin_util::secp::key::SecretKey;
use crate::grin_util::secp::pedersen;
use crate::grin_util::Mutex;
use crate::internal::{selection, updater};
use crate::slate::Slate;
use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend};
use crate::util::OnionV3Address;
use crate::InitTxArgs;
use crate::{address, Error};
use ed25519_dalek::Keypair as DalekKeypair;
use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::SecretKey as DalekSecretKey;
use ed25519_dalek::Signature as DalekSignature;
use ed25519_dalek::{Signer, Verifier};
use grin_core::core::FeeFields;
lazy_static! {
static ref SLATE_COUNTER: Mutex<u8> = Mutex::new(0);
}
pub fn new_tx_slate<'a, T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
is_invoice: bool,
num_participants: u8,
use_test_rng: bool,
ttl_blocks: Option<u64>,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let current_height = wallet.w2n_client().get_chain_tip()?.0;
let mut slate = Slate::blank(num_participants, is_invoice);
if let Some(b) = ttl_blocks {
slate.ttl_cutoff_height = current_height + b;
}
if use_test_rng {
{
let sc = SLATE_COUNTER.lock();
let bytes = [4, 54, 67, 12, 43, 2, 98, 76, 32, 50, 87, 5, 1, 33, 43, *sc];
slate.id = Uuid::from_slice(&bytes).unwrap();
}
*SLATE_COUNTER.lock() += 1;
}
slate.amount = amount;
if valid_header_version(current_height, HeaderVersion(1)) {
slate.version_info.block_header_version = 1;
}
if valid_header_version(current_height, HeaderVersion(2)) {
slate.version_info.block_header_version = 2;
}
if valid_header_version(current_height, HeaderVersion(3)) {
slate.version_info.block_header_version = 3;
}
slate.kernel_features = 0;
Ok(slate)
}
pub fn estimate_send_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
amount: u64,
amount_includes_fee: bool,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
) -> Result<
(
u64, // total
u64, // fee
),
Error,
>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let current_height = wallet.w2n_client().get_chain_tip()?.0;
updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?;
let (_coins, total, _amount, fee) = selection::select_coins_and_fee(
wallet,
amount,
amount_includes_fee,
current_height,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
parent_key_id,
)?;
Ok((total, fee))
}
pub fn add_inputs_to_slate<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
is_initiator: bool,
use_test_rng: bool,
amount_includes_fee: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?;
let mut context = selection::build_send_tx(
wallet,
&wallet.keychain(keychain_mask)?,
keychain_mask,
slate,
current_height,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
None,
parent_key_id.clone(),
use_test_rng,
is_initiator,
amount_includes_fee,
)?;
slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?;
context.initial_sec_key = context.sec_key.clone();
if !is_initiator {
slate.fill_round_2(
&wallet.keychain(keychain_mask)?,
&context.sec_key,
&context.sec_nonce,
)?;
}
Ok(context)
}
pub fn add_output_to_slate<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
parent_key_id: &Identifier,
is_initiator: bool,
use_test_rng: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let keychain = wallet.keychain(keychain_mask)?;
let (_, mut context, mut tx) = selection::build_recipient_output(
wallet,
keychain_mask,
slate,
current_height,
parent_key_id.clone(),
use_test_rng,
is_initiator,
)?;
slate.fill_round_1(&keychain, &mut context)?;
context.initial_sec_key = context.sec_key.clone();
if !is_initiator {
slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?;
let mut batch = wallet.batch(keychain_mask)?;
tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?);
batch.save_tx_log_entry(tx.clone(), &parent_key_id)?;
batch.commit()?;
}
Ok(context)
}
pub fn create_late_lock_context<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
init_tx_args: &InitTxArgs,
parent_key_id: &Identifier,
use_test_rng: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?;
let (_coins, _total, _amount, fee) = selection::select_coins_and_fee(
wallet,
init_tx_args.amount,
init_tx_args.amount_includes_fee.unwrap_or(false),
current_height,
init_tx_args.minimum_confirmations,
init_tx_args.max_outputs as usize,
init_tx_args.num_change_outputs as usize,
init_tx_args.selection_strategy_is_use_all,
&parent_key_id,
)?;
slate.fee_fields = FeeFields::new(0, fee)?;
let keychain = wallet.keychain(keychain_mask)?;
let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true);
context.fee = Some(slate.fee_fields);
context.amount = slate.amount;
context.late_lock_args = Some(init_tx_args.clone());
slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?;
Ok(context)
}
pub fn complete_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
context: &Context,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let (sec_key, sec_nonce) = {
if context.initial_sec_key != context.sec_key
&& context.initial_sec_nonce != context.sec_nonce
{
(
context.initial_sec_key.clone(),
context.initial_sec_nonce.clone(),
)
} else {
(context.sec_key.clone(), context.sec_nonce.clone())
}
};
slate.fill_round_2(&wallet.keychain(keychain_mask)?, &sec_key, &sec_nonce)?;
trace!("Slate to finalize is: {}", slate);
slate.finalize(&wallet.keychain(keychain_mask)?)?;
Ok(())
}
pub fn cancel_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
parent_key_id: &Identifier,
tx_id: Option<u32>,
tx_slate_id: Option<Uuid>,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut tx_id_string = String::new();
if let Some(tx_id) = tx_id {
tx_id_string = tx_id.to_string();
} else if let Some(tx_slate_id) = tx_slate_id {
tx_id_string = tx_slate_id.to_string();
}
let tx_vec = updater::retrieve_txs(
wallet,
tx_id,
tx_slate_id,
None,
Some(&parent_key_id),
false,
)?;
if tx_vec.len() != 1 {
return Err(Error::TransactionDoesntExist(tx_id_string));
}
let tx = tx_vec[0].clone();
match tx.tx_type {
TxLogEntryType::TxSent | TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {}
_ => return Err(Error::TransactionNotCancellable(tx_id_string)),
}
if tx.confirmed {
return Err(Error::TransactionNotCancellable(tx_id_string));
}
let res = updater::retrieve_outputs(
wallet,
keychain_mask,
false,
Some(tx.id),
Some(&parent_key_id),
)?;
let outputs = res.iter().map(|m| m.output.clone()).collect();
updater::cancel_tx_and_outputs(wallet, keychain_mask, tx, outputs, parent_key_id)?;
Ok(())
}
pub fn update_stored_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
context: &Context,
slate: &Slate,
is_invoiced: bool,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, None, false)?;
let mut tx = None;
for t in tx_vec {
if t.tx_type == TxLogEntryType::TxSent && !is_invoiced {
tx = Some(t);
break;
}
if t.tx_type == TxLogEntryType::TxReceived && is_invoiced {
tx = Some(t);
break;
}
}
let mut tx = match tx {
Some(t) => t,
None => return Err(Error::TransactionDoesntExist(slate.id.to_string())),
};
let parent_key = tx.parent_key_id.clone();
{
let keychain = wallet.keychain(keychain_mask)?;
tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?);
}
if let Some(ref p) = slate.clone().payment_proof {
let derivation_index = match context.payment_proof_derivation_index {
Some(i) => i,
None => 0,
};
let keychain = wallet.keychain(keychain_mask)?;
let parent_key_id = wallet.parent_key_id();
let excess = slate.calc_excess(keychain.secp())?;
let sender_key =
address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
let sig =
create_payment_proof_signature(slate.amount, &excess, p.sender_address, sender_key)?;
tx.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address,
receiver_signature: p.receiver_signature,
sender_address_path: derivation_index,
sender_address: sender_address.to_ed25519()?,
sender_signature: Some(sig),
})
}
wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), slate.tx_or_err()?)?;
let mut batch = wallet.batch(keychain_mask)?;
batch.save_tx_log_entry(tx, &parent_key)?;
batch.commit()?;
Ok(())
}
pub fn payment_proof_message(
amount: u64,
kernel_commitment: &pedersen::Commitment,
sender_address: DalekPublicKey,
) -> Result<Vec<u8>, Error> {
let mut msg = Vec::new();
msg.write_u64::<BigEndian>(amount)?;
msg.append(&mut kernel_commitment.0.to_vec());
msg.append(&mut sender_address.to_bytes().to_vec());
Ok(msg)
}
pub fn _decode_payment_proof_message(
msg: &[u8],
) -> Result<(u64, pedersen::Commitment, DalekPublicKey), Error> {
let mut rdr = Cursor::new(msg);
let amount = rdr.read_u64::<BigEndian>()?;
let mut commit_bytes = [0u8; 33];
for i in 0..33 {
commit_bytes[i] = rdr.read_u8()?;
}
let mut sender_address_bytes = [0u8; 32];
for i in 0..32 {
sender_address_bytes[i] = rdr.read_u8()?;
}
Ok((
amount,
pedersen::Commitment::from_vec(commit_bytes.to_vec()),
DalekPublicKey::from_bytes(&sender_address_bytes).unwrap(),
))
}
pub fn create_payment_proof_signature(
amount: u64,
kernel_commitment: &pedersen::Commitment,
sender_address: DalekPublicKey,
sec_key: SecretKey,
) -> Result<DalekSignature, Error> {
let msg = payment_proof_message(amount, kernel_commitment, sender_address)?;
let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) {
Ok(k) => k,
Err(e) => {
return Err(Error::ED25519Key(format!("{}", e)));
}
};
let pub_key: DalekPublicKey = (&d_skey).into();
let keypair = DalekKeypair {
public: pub_key,
secret: d_skey,
};
Ok(keypair.sign(&msg))
}
pub fn verify_slate_payment_proof<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
parent_key_id: &Identifier,
context: &Context,
slate: &Slate,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let tx_vec = updater::retrieve_txs(
wallet,
None,
Some(slate.id),
None,
Some(parent_key_id),
false,
)?;
if tx_vec.is_empty() {
return Err(Error::PaymentProof(
"TxLogEntry with original proof info not found (is account correct?)".to_owned(),
));
}
let orig_proof_info = tx_vec[0].clone().payment_proof;
if orig_proof_info.is_some() && slate.payment_proof.is_none() {
return Err(Error::PaymentProof(
"Expected Payment Proof for this Transaction is not present".to_owned(),
));
}
if let Some(ref p) = slate.clone().payment_proof {
let orig_proof_info = match orig_proof_info {
Some(p) => p.clone(),
None => {
return Err(Error::PaymentProof(
"Original proof info not stored in tx".to_owned(),
));
}
};
let keychain = wallet.keychain(keychain_mask)?;
let index = match context.payment_proof_derivation_index {
Some(i) => i,
None => {
return Err(Error::PaymentProof(
"Payment proof derivation index required".to_owned(),
));
}
};
let orig_sender_sk =
address::address_from_derivation_path(&keychain, parent_key_id, index)?;
let orig_sender_address = OnionV3Address::from_private(&orig_sender_sk.0)?;
if p.sender_address != orig_sender_address.to_ed25519()? {
return Err(Error::PaymentProof(
"Sender address on slate does not match original sender address".to_owned(),
));
}
if orig_proof_info.receiver_address != p.receiver_address {
return Err(Error::PaymentProof(
"Recipient address on slate does not match original recipient address".to_owned(),
));
}
let msg = payment_proof_message(
slate.amount,
&slate.calc_excess(&keychain.secp())?,
orig_sender_address.to_ed25519()?,
)?;
let sig = match p.receiver_signature {
Some(s) => s,
None => {
return Err(Error::PaymentProof(
"Recipient did not provide requested proof signature".to_owned(),
));
}
};
if p.receiver_address.verify(&msg, &sig).is_err() {
return Err(Error::PaymentProof("Invalid proof signature".to_owned()));
};
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use rand::rngs::mock::StepRng;
use crate::grin_core::core::{FeeFields, KernelFeatures};
use crate::grin_core::libtx::{build, ProofBuilder};
use crate::grin_keychain::{
BlindSum, BlindingFactor, ExtKeychain, ExtKeychainPath, Keychain, SwitchCommitmentType,
};
use crate::grin_util::{secp, static_secp_instance};
#[test]
fn output_commitment_equals_input_commitment_on_spend() {
let keychain = ExtKeychain::from_random_seed(false).unwrap();
let builder = ProofBuilder::new(&keychain);
let key_id1 = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier();
let tx1 = build::transaction(
KernelFeatures::Plain {
fee: FeeFields::zero(),
},
&[build::output(105, key_id1.clone())],
&keychain,
&builder,
)
.unwrap();
let tx2 = build::transaction(
KernelFeatures::Plain {
fee: FeeFields::zero(),
},
&[build::input(105, key_id1.clone())],
&keychain,
&builder,
)
.unwrap();
let inputs: Vec<_> = tx2.inputs().into();
assert_eq!(tx1.outputs()[0].commitment(), inputs[0].commitment());
}
#[test]
fn payment_proof_construction() {
let secp_inst = static_secp_instance();
let secp = secp_inst.lock();
let mut test_rng = StepRng::new(1_234_567_890_u64, 1);
let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng);
let d_skey = DalekSecretKey::from_bytes(&sec_key.0).unwrap();
let address: DalekPublicKey = (&d_skey).into();
let kernel_excess = {
ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier();
let keychain = ExtKeychain::from_random_seed(true).unwrap();
let switch = SwitchCommitmentType::Regular;
let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
let id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0);
let skey1 = keychain.derive_key(0, &id1, switch).unwrap();
let skey2 = keychain.derive_key(0, &id2, switch).unwrap();
let blinding_factor = keychain
.blind_sum(
&BlindSum::new()
.sub_blinding_factor(BlindingFactor::from_secret_key(skey1))
.add_blinding_factor(BlindingFactor::from_secret_key(skey2)),
)
.unwrap();
keychain
.secp()
.commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap())
.unwrap()
};
let amount = 1_234_567_890_u64;
let msg = payment_proof_message(amount, &kernel_excess, address).unwrap();
println!("payment proof message is (len {}): {:?}", msg.len(), msg);
let decoded = _decode_payment_proof_message(&msg).unwrap();
assert_eq!(decoded.0, amount);
assert_eq!(decoded.1, kernel_excess);
assert_eq!(decoded.2, address);
let sig = create_payment_proof_signature(amount, &kernel_excess, address, sec_key).unwrap();
assert!(address.verify(&msg, &sig).is_ok());
}
}