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

feat: support for stealth addresses in one-sided transactions #4310

Merged
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
55d3e59
reworked `scan_outputs_for_one_sided_payments()` to handle both types…
agubarev Jul 14, 2022
a5030ed
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 14, 2022
a81d353
bugfix
agubarev Jul 14, 2022
30ce16e
Merge remote-tracking branch 'origin/stealth_addresses_clean' into st…
agubarev Jul 14, 2022
90a6517
Merge remote-tracking branch 'upstream/development' into stealth_addr…
agubarev Jul 15, 2022
3fad93d
fix
agubarev Jul 15, 2022
5d69b5d
Merge remote-tracking branch 'origin/stealth_addresses_clean' into st…
agubarev Jul 15, 2022
bc89132
fix
agubarev Jul 15, 2022
a683f45
Merge branch 'tari-project:development' into stealth_addresses_clean
agubarev Jul 18, 2022
1a65870
small refactor before merging with another PR
agubarev Jul 18, 2022
31715a0
reverted merge
agubarev Jul 19, 2022
a063105
refactored `scan_outputs_for_one_sided_payments()`
agubarev Jul 20, 2022
bbc8030
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 20, 2022
9667354
Merge branch 'tari-project:development' into stealth_addresses_clean
agubarev Jul 20, 2022
11273b5
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 20, 2022
e8dcd3c
ffi: added helper functions needed to test stealth addresses in cucumber
agubarev Jul 21, 2022
f2dd1c2
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 21, 2022
3e4ee3a
fix
agubarev Jul 21, 2022
20bb419
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 21, 2022
0bf1c02
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 21, 2022
b5d8081
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 21, 2022
652e0e3
cucumber test for one-sided stealth transactions
agubarev Jul 21, 2022
31573ae
cucumber test for one-sided stealth transactions
agubarev Jul 21, 2022
59b02f0
Merge remote-tracking branch 'origin/stealth_addresses_clean' into st…
agubarev Jul 21, 2022
e282c26
cleanup
agubarev Jul 22, 2022
909026f
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 22, 2022
b5b20b5
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 27, 2022
3f25f56
Merge branch 'development' into stealth_addresses_clean
agubarev Jul 27, 2022
32d5577
fix: refactored code to use the new hashing API approach
agubarev Jul 28, 2022
db8302e
Merge branch 'development' into stealth_addresses_clean
stringhandler Jul 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 166 additions & 81 deletions base_layer/wallet/src/output_manager_service/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ use tari_core::{
use tari_crypto::{
commitment::HomomorphicCommitmentFactory,
errors::RangeProofError,
hash::blake2::Blake256,
hashing::{DomainSeparatedHasher, GenericHashDomain},
keys::{DiffieHellmanSharedSecret, PublicKey as PublicKeyTrait, SecretKey},
ristretto::RistrettoSecretKey,
};
use tari_script::{inputs, script, TariScript};
use tari_script::{inputs, script, Opcode, TariScript};
use tari_service_framework::reply_channel;
use tari_shutdown::ShutdownSignal;
use tari_utilities::{hex::Hex, ByteArray};
Expand Down Expand Up @@ -1902,98 +1905,180 @@ where
Ok(())
}

/// Attempt to scan and then rewind all of the given transaction outputs into unblinded outputs based on known
/// pubkeys
// Scanning outputs addressed to this wallet
fn scan_outputs_for_one_sided_payments(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn scan_outputs_for_one_sided_payments(
/// Attempt to scan and then rewind all of the given transaction outputs into unblinded outputs based on known
/// pubkeys
fn scan_outputs_for_one_sided_payments(

&mut self,
outputs: Vec<TransactionOutput>,
) -> Result<Vec<RecoveredOutput>, OutputManagerError> {
let known_one_sided_payment_scripts: Vec<KnownOneSidedPaymentScript> =
self.resources.db.get_all_known_one_sided_payment_scripts()?;
// TODO: use MultiKey
// NOTE: known keys is a list consisting of an actual and deprecated wallet keys
let known_keys = self.resources.db.get_all_known_one_sided_payment_scripts()?;

let wallet_sk = self.node_identity.secret_key().clone();
let wallet_pk = self.node_identity.public_key();

let mut scanned_outputs = vec![];

let mut rewound_outputs: Vec<RecoveredOutput> = Vec::new();
for output in outputs {
let position = known_one_sided_payment_scripts
.iter()
.position(|known_one_sided_script| known_one_sided_script.script == output.script);
if let Some(i) = position {
let spending_key = PrivateKey::from_bytes(
CommsPublicKey::shared_secret(
&known_one_sided_payment_scripts[i].private_key,
&output.sender_offset_public_key,
match output.script.as_slice() {
// ----------------------------------------------------------------------------
// simple one-sided address
[Opcode::PushPubKey(scanned_pk)] => {
match known_keys
.iter()
.find(|x| &PublicKey::from_secret_key(&x.private_key) == scanned_pk.as_ref())
{
// none of the keys match, skipping
None => continue,

// match found
Some(matched_key) => {
match PrivateKey::from_bytes(
CommsPublicKey::shared_secret(
&matched_key.private_key,
&output.sender_offset_public_key,
)
.as_bytes(),
) {
Ok(spending_sk) => {
scanned_outputs.push((output.clone(), matched_key.private_key.clone(), spending_sk))
},
Err(e) => {
error!(
target: LOG_TARGET,
"failed to derive private key from DH shared secret (simple one-sided): {:?}",
e
);
continue;
},
}
},
}
},

// ----------------------------------------------------------------------------
// one-sided stealth address
// NOTE: Extracting the nonce R and a spending (public aka scan_key) key from the script
// NOTE: [RFC 203 on Stealth Addresses](https://rfc.tari.com/RFC-0203_StealthAddresses.html)
[Opcode::PushPubKey(nonce), Opcode::Drop, Opcode::PushPubKey(scanned_pk)] => {
// computing shared secret
let shared_secret = PrivateKey::from_bytes(
DomainSeparatedHasher::<Blake256, GenericHashDomain>::new("com.tari.stealth_address")
.chain(PublicKey::shared_secret(&wallet_sk, nonce.as_ref()).as_bytes())
.finalize()
.as_ref(),
)
.as_bytes(),
)?;
let rewind_blinding_key = PrivateKey::from_bytes(&hash_secret_key(&spending_key))?;
let encryption_key = PrivateKey::from_bytes(&hash_secret_key(&rewind_blinding_key))?;
let committed_value =
EncryptedValue::decrypt_value(&encryption_key, &output.commitment, &output.encrypted_value);
if let Ok(committed_value) = committed_value {
let blinding_factor =
output.recover_mask(&self.resources.factories.range_proof, &rewind_blinding_key)?;
if output.verify_mask(
&self.resources.factories.range_proof,
&blinding_factor,
committed_value.into(),
)? {
let rewound_output = UnblindedOutput::new(
output.version,
committed_value,
blinding_factor.clone(),
output.features,
known_one_sided_payment_scripts[i].script.clone(),
known_one_sided_payment_scripts[i].input.clone(),
known_one_sided_payment_scripts[i].private_key.clone(),
output.sender_offset_public_key,
output.metadata_signature,
known_one_sided_payment_scripts[i].script_lock_height,
output.covenant,
output.encrypted_value,
output.minimum_value_promise,
);
.unwrap();

let db_output = DbUnblindedOutput::rewindable_from_unblinded_output(
rewound_output.clone(),
&self.resources.factories,
&RewindData {
rewind_blinding_key,
encryption_key,
},
None,
Some(&output.proof),
)?;

let output_hex = output.commitment.to_hex();
let tx_id = TxId::new_random();

match self.resources.db.add_unspent_output_with_tx_id(tx_id, db_output) {
Ok(_) => {
rewound_outputs.push(RecoveredOutput {
output: rewound_output,
tx_id,
});
},
Err(OutputManagerStorageError::DuplicateOutput) => {
warn!(
target: LOG_TARGET,
"Attempt to add scanned output {} that already exists. Ignoring the output.",
output_hex
);
},
Err(err) => {
return Err(err.into());
},
}
trace!(
target: LOG_TARGET,
"One-sided payment Output {} with value {} recovered",
output_hex,
committed_value,
);
// matching spending (public) keys
if &(PublicKey::from_secret_key(&shared_secret) + wallet_pk) != scanned_pk.as_ref() {
continue;
}

match PrivateKey::from_bytes(
CommsPublicKey::shared_secret(&wallet_sk, &output.sender_offset_public_key).as_bytes(),
) {
Ok(spending_sk) => {
scanned_outputs.push((output.clone(), wallet_sk.clone() + shared_secret, spending_sk))
},
Err(e) => {
error!(
target: LOG_TARGET,
"failed to derive private key from DH shared secret (stealth one-sided): {:?}", e
);
continue;
},
}
},

_ => {},
}
}

self.import_onesided_outputs(scanned_outputs)
}

// Imports scanned outputs into the wallet
fn import_onesided_outputs(
&self,
scanned_outputs: Vec<(TransactionOutput, PrivateKey, RistrettoSecretKey)>,
) -> Result<Vec<RecoveredOutput>, OutputManagerError> {
let mut rewound_outputs = Vec::with_capacity(scanned_outputs.len());

for (output, script_private_key, spending_sk) in scanned_outputs {
let rewind_blinding_key = PrivateKey::from_bytes(&hash_secret_key(&spending_sk))?;
let encryption_key = PrivateKey::from_bytes(&hash_secret_key(&rewind_blinding_key))?;
let committed_value =
EncryptedValue::decrypt_value(&encryption_key, &output.commitment, &output.encrypted_value);

if let Ok(committed_value) = committed_value {
let blinding_factor =
output.recover_mask(&self.resources.factories.range_proof, &rewind_blinding_key)?;

if output.verify_mask(
&self.resources.factories.range_proof,
&blinding_factor,
committed_value.into(),
)? {
let rewound_output = UnblindedOutput::new(
output.version,
committed_value,
blinding_factor.clone(),
output.features,
output.script,
tari_script::ExecutionStack::new(vec![]),
script_private_key,
output.sender_offset_public_key,
output.metadata_signature,
0,
output.covenant,
output.encrypted_value,
output.minimum_value_promise,
);

let db_output = DbUnblindedOutput::rewindable_from_unblinded_output(
rewound_output.clone(),
&self.resources.factories,
&RewindData {
rewind_blinding_key,
encryption_key,
},
None,
Some(&output.proof),
)?;

let output_hex = output.commitment.to_hex();
let tx_id = TxId::new_random();

match self.resources.db.add_unspent_output_with_tx_id(tx_id, db_output) {
Ok(_) => {
trace!(
target: LOG_TARGET,
"One-sided payment Output {} with value {} recovered",
output_hex,
committed_value,
);

rewound_outputs.push(RecoveredOutput {
output: rewound_output,
tx_id,
})
},
Err(OutputManagerStorageError::DuplicateOutput) => {
warn!(
target: LOG_TARGET,
"Attempt to add scanned output {} that already exists. Ignoring the output.",
output_hex
);
},
Err(err) => {
return Err(err.into());
},
}
}
}
}

Ok(rewound_outputs)
}

Expand Down
58 changes: 58 additions & 0 deletions base_layer/wallet/src/transaction_service/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2501,3 +2501,61 @@ pub struct TransactionSendResult {
pub tx_id: TxId,
pub transaction_status: TransactionStatus,
}

#[cfg(test)]
mod tests {
use tari_crypto::{
hash::blake2::Blake256,
hashing::{DomainSeparatedHasher, GenericHashDomain},
ristretto::RistrettoSecretKey,
};
use tari_script::Opcode;

use super::*;

#[test]
fn test_stealth_addresses() {
// recipient's keys
let (a, big_a) = PublicKey::random_keypair(&mut OsRng);
let (b, big_b) = PublicKey::random_keypair(&mut OsRng);

// Sender generates a random nonce key-pair: R=r⋅G
let (r, big_r) = PublicKey::random_keypair(&mut OsRng);

// Sender calculates a ECDH shared secret: c=H(r⋅a⋅G)=H(a⋅R)=H(r⋅A),
// where H(⋅) is a cryptographic hash function
let c = DomainSeparatedHasher::<Blake256, GenericHashDomain>::new("stealth_test")
.chain(PublicKey::shared_secret(&r, &big_a).as_bytes())
.finalize();

// using spending key `Ks=c⋅G+B` as the last public key in the one-sided payment script
let sender_spending_key =
PublicKey::from_secret_key(&RistrettoSecretKey::from_bytes(c.as_ref()).unwrap()) + big_b.clone();

let script = script!(PushPubKey(Box::new(big_r)) Drop PushPubKey(Box::new(sender_spending_key.clone())));

// ----------------------------------------------------------------------------
// imitating the receiving end, scanning and extraction

// Extracting the nonce R and a spending key from the script
if let [Opcode::PushPubKey(big_r), Opcode::Drop, Opcode::PushPubKey(provided_spending_key)] = script.as_slice()
{
// calculating Ks with the provided R nonce from the script
let c = DomainSeparatedHasher::<Blake256, GenericHashDomain>::new("stealth_test")
.chain(PublicKey::shared_secret(&a, big_r).as_bytes())
.finalize();

// computing a spending key `Ks=(c+b)G` for comparison
let receiver_spending_key =
PublicKey::from_secret_key(&(RistrettoSecretKey::from_bytes(c.as_ref()).unwrap() + b));

// computing a scanning key `Ks=cG+B` for comparison
let scanning_key = PublicKey::from_secret_key(&RistrettoSecretKey::from_bytes(c.as_ref()).unwrap()) + big_b;

assert_eq!(provided_spending_key.as_ref(), &sender_spending_key);
assert_eq!(receiver_spending_key, sender_spending_key);
assert_eq!(scanning_key, sender_spending_key);
assert_eq!(scanning_key, receiver_spending_key);
}
}
}
Loading