From 4d8c40fd633dd11c7d73922dd97c98da61d92875 Mon Sep 17 00:00:00 2001 From: "S. Santos" Date: Mon, 8 Jul 2024 20:56:10 -0300 Subject: [PATCH] Add TA01 - 'signSecond not called' and return the pubnonce if the challenge is null --- clients/libs/rust/src/lib.rs | 8 + clients/tests/rust/src/main.rs | 2 + .../rust/src/ta01_sign_second_not_called.rs | 209 ++++++++++++++++++ docs/test_cases.md | 18 ++ server/src/database/mod.rs | 1 + server/src/database/sign.rs | 85 +++++++ server/src/endpoints/lightning_latch.rs | 2 +- server/src/endpoints/sign.rs | 85 ++----- 8 files changed, 343 insertions(+), 67 deletions(-) create mode 100644 clients/tests/rust/src/ta01_sign_second_not_called.rs create mode 100644 server/src/database/sign.rs diff --git a/clients/libs/rust/src/lib.rs b/clients/libs/rust/src/lib.rs index 66186bab..17004821 100644 --- a/clients/libs/rust/src/lib.rs +++ b/clients/libs/rust/src/lib.rs @@ -13,6 +13,14 @@ pub mod withdraw; pub use mercurylib::wallet::Wallet; pub use mercurylib::wallet::CoinStatus; +pub use mercurylib::wallet::Coin; +pub use mercurylib::wallet::BackupTx; +pub use mercurylib::wallet::Activity; + +pub use mercurylib::transfer::sender::{TransferSenderRequestPayload, TransferSenderResponsePayload, create_transfer_signature, create_transfer_update_msg}; +pub use mercurylib::transaction::{SignFirstRequestPayload, SignFirstResponsePayload, create_and_commit_nonces}; +pub use mercurylib::utils::get_blockheight; +pub use mercurylib::{validate_address, decode_transfer_address}; pub fn add(left: usize, right: usize) -> usize { left + right diff --git a/clients/tests/rust/src/main.rs b/clients/tests/rust/src/main.rs index cc1f19c3..87d30ee4 100644 --- a/clients/tests/rust/src/main.rs +++ b/clients/tests/rust/src/main.rs @@ -4,6 +4,7 @@ pub mod bitcoin_core; pub mod tb01_simple_transfer; pub mod tb02_transfer_address_reuse; pub mod tm01_sender_double_spends; +pub mod ta01_sign_second_not_called; use anyhow::{Result, Ok}; #[tokio::main(flavor = "current_thread")] @@ -12,6 +13,7 @@ async fn main() -> Result<()> { tb01_simple_transfer::execute().await?; tb02_transfer_address_reuse::execute().await?; tm01_sender_double_spends::execute().await?; + ta01_sign_second_not_called::execute().await?; Ok(()) } diff --git a/clients/tests/rust/src/ta01_sign_second_not_called.rs b/clients/tests/rust/src/ta01_sign_second_not_called.rs new file mode 100644 index 00000000..9567d3ea --- /dev/null +++ b/clients/tests/rust/src/ta01_sign_second_not_called.rs @@ -0,0 +1,209 @@ +use std::{env, process::Command, thread, time::Duration}; +use anyhow::{anyhow, Result, Ok}; +use mercuryrustlib::{client_config::ClientConfig, create_and_commit_nonces, decode_transfer_address, sqlite_manager::get_wallet, Coin, CoinStatus, SignFirstRequestPayload, SignFirstResponsePayload, TransferSenderRequestPayload, TransferSenderResponsePayload, Wallet}; + +use crate::{bitcoin_core, electrs}; + +/// This function gets the server public nonce from the statechain entity. +pub async fn sign_first(client_config: &ClientConfig, sign_first_request_payload: &SignFirstRequestPayload) -> Result { + + let endpoint = client_config.statechain_entity.clone(); + let path = "sign/first"; + + let client = client_config.get_reqwest_client()?; + let request = client.post(&format!("{}/{}", endpoint, path)); + + let value = request.json(&sign_first_request_payload).send().await?.text().await?; + + let sign_first_response_payload: SignFirstResponsePayload = serde_json::from_str(value.as_str())?; + + let mut server_pubnonce_hex = sign_first_response_payload.server_pubnonce.to_string(); + + if server_pubnonce_hex.starts_with("0x") { + server_pubnonce_hex = server_pubnonce_hex[2..].to_string(); + } + + Ok(server_pubnonce_hex) +} + +pub async fn new_transaction_only_sign_first( + client_config: &ClientConfig, + coin: &mut Coin) -> Result<()> { + + let coin_nonce = create_and_commit_nonces(&coin)?; + coin.secret_nonce = Some(coin_nonce.secret_nonce); + coin.public_nonce = Some(coin_nonce.public_nonce); + coin.blinding_factor = Some(coin_nonce.blinding_factor); + + let _ = sign_first(&client_config, &coin_nonce.sign_first_request_payload).await?; + + Ok(()) +} + +pub async fn execute_only_sign_first( + client_config: &ClientConfig, + recipient_address: &str, + wallet_name: &str, + statechain_id: &str, + batch_id: Option) -> Result<()> +{ + + let mut wallet = get_wallet(&client_config.pool, &wallet_name).await?; + + let coin = wallet.coins + .iter_mut() + .filter(|tx| tx.statechain_id == Some(statechain_id.to_string())) // Filter coins with the specified statechain_id + .min_by_key(|tx| tx.locktime.unwrap_or(u32::MAX)); // Find the one with the lowest locktime + + if coin.is_none() { + return Err(anyhow!("No coins associated with this statechain ID were found")); + } + + let coin = coin.unwrap(); + + let statechain_id = coin.statechain_id.as_ref().unwrap(); + let signed_statechain_id = coin.signed_statechain_id.as_ref().unwrap(); + + let (_, _, recipient_auth_pubkey) = decode_transfer_address(recipient_address)?; + let _ = get_new_x1(&client_config, statechain_id, signed_statechain_id, &recipient_auth_pubkey.to_string(), batch_id).await?; + + let _ = new_transaction_only_sign_first(client_config, coin).await?; + + Ok(()) + +} + +async fn get_new_x1(client_config: &ClientConfig, statechain_id: &str, signed_statechain_id: &str, recipient_auth_pubkey: &str, batch_id: Option) -> Result { + + let endpoint = client_config.statechain_entity.clone(); + let path = "transfer/sender"; + + let client = client_config.get_reqwest_client()?; + let request = client.post(&format!("{}/{}", endpoint, path)); + + let transfer_sender_request_payload = TransferSenderRequestPayload { + statechain_id: statechain_id.to_string(), + auth_sig: signed_statechain_id.to_string(), + new_user_auth_key: recipient_auth_pubkey.to_string(), + batch_id, + }; + + let value = match request.json(&transfer_sender_request_payload).send().await { + std::result::Result::Ok(response) => { + + let status = response.status(); + let text = response.text().await.unwrap_or("Unexpected error".to_string()); + + if status.is_success() { + text + } else { + return Err(anyhow::anyhow!(format!("status: {}, error: {}", status, text))); + } + }, + Err(err) => { + return Err(anyhow::anyhow!(format!("status: {}, error: {}", err.status().unwrap(),err.to_string()))); + }, + }; + + let response: TransferSenderResponsePayload = serde_json::from_str(value.as_str()).expect(&format!("failed to parse: {}", value.as_str())); + + Ok(response.x1) +} + +async fn ta01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) -> Result<()> { + + let amount = 1000; + + let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + + let address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; + + let _ = bitcoin_core::sendtoaddress(amount, &address)?; + + let core_wallet_address = bitcoin_core::getnewaddress()?; + let remaining_blocks = client_config.confirmation_target; + let _ = bitcoin_core::generatetoaddress(remaining_blocks, &core_wallet_address)?; + + // It appears that Electrs takes a few seconds to index the transaction + let mut is_tx_indexed = false; + + while !is_tx_indexed { + is_tx_indexed = electrs::check_address(client_config, &address, amount).await?; + thread::sleep(Duration::from_secs(1)); + } + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(address.clone())).unwrap(); + + assert!(new_coin.status == CoinStatus::CONFIRMED); + + let statechain_id = new_coin.statechain_id.as_ref().unwrap(); + + let batch_id = None; + + let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; + + execute_only_sign_first( + &client_config, + &wallet2_transfer_adress, + &wallet1.name, + &statechain_id, + batch_id).await?; + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let batch_id = None; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, batch_id).await; + + assert!(result.is_ok()); + + let received_statechain_ids = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet2.name).await?; + + assert!(received_statechain_ids.contains(&statechain_id.to_string())); + assert!(received_statechain_ids.len() == 1); + + let wallet2 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet2.name).await?; + + let new_coin = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string())).unwrap(); + + assert!(new_coin.status == CoinStatus::CONFIRMED); + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string())).unwrap(); + + assert!(new_coin.status == CoinStatus::TRANSFERRED); + + Ok(()) +} + +pub async fn execute() -> Result<()> { + + let _ = Command::new("rm").arg("wallet.db").arg("wallet.db-shm").arg("wallet.db-wal").output().expect("failed to execute process"); + + env::set_var("ML_NETWORK", "regtest"); + + let client_config = mercuryrustlib::client_config::load().await; + + let wallet1 = mercuryrustlib::wallet::create_wallet( + "wallet1", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet1).await?; + + let wallet2 = mercuryrustlib::wallet::create_wallet( + "wallet2", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet2).await?; + + ta01(&client_config, &wallet1, &wallet2).await?; + + println!("TA01 - 'SignSecond not called' tested successfully"); + + Ok(()) +} diff --git a/docs/test_cases.md b/docs/test_cases.md index f51ce8d7..770fc63d 100644 --- a/docs/test_cases.md +++ b/docs/test_cases.md @@ -60,6 +60,24 @@ 25. Generate blocks enough to confirm the withdrawal (according to the client's Settings.toml) 26. Confirm that the coin status changed to `WITHDRAWN` status in wallet 1 +## Alternative Workflow + +### TA01 - SignSecond not called + +01. Create wallet 1 and 2 +02. Create a token +03. Generate wallet 1 deposit address with this token +04. Send funds to the address of the new coin +05. Generate blocks enough to confirm the coin (according to the client's Settings.toml) +06. Wait for Electrs to index this deposit transaction +07. Confirm wallet 1 has a coin +08. Wallet 2 generates a new transfer address. +09. Wallet 1 calls `transfer/send` and `sign/first` but not `sign/second` using the coin' statechain id and the transfer address of wallet 2 +10. Using the same parameters, wallet 1 then calls `transfer-send` command, which includes the complete process (transfer/send`, `sign/first`, `sign/second` and `transfer/update`) +11. Wallet 2 executes `transfer-receive` +12. Confirm that the coin status changed to `TRANSFERRED` status in wallet 1 +13. Confirm that the coin status changed to `CONFIRMED` status in wallet 2 + ## Malicious Workflow ### TM01 - Sender Double Spends diff --git a/server/src/database/mod.rs b/server/src/database/mod.rs index 8569b8ab..b3d939ad 100644 --- a/server/src/database/mod.rs +++ b/server/src/database/mod.rs @@ -4,3 +4,4 @@ pub mod transfer; pub mod deposit; pub mod utils; pub mod lightning_latch; +pub mod sign; diff --git a/server/src/database/sign.rs b/server/src/database/sign.rs new file mode 100644 index 00000000..1e27c50e --- /dev/null +++ b/server/src/database/sign.rs @@ -0,0 +1,85 @@ +use sqlx::Row; + +pub async fn get_server_pubnonce_from_null_challenge(pool: &sqlx::PgPool, statechain_id: &str) -> Option { + + let query = "SELECT server_pubnonce \ + FROM statechain_signature_data \ + WHERE statechain_id = $1 \ + AND challenge is NULL \ + ORDER BY created_at ASC"; + + let row = sqlx::query(query) + .bind(statechain_id) + .fetch_optional(pool) + .await + .unwrap(); + + if row.is_none() + { + return None; + } + + let row = row.unwrap(); + + let server_pubnonce: String = row.get(0); + + Some(server_pubnonce) +} + +pub async fn insert_new_signature_data(pool: &sqlx::PgPool, server_pubnonce: &str, statechain_id: &str) { + + let mut transaction = pool.begin().await.unwrap(); + + // FOR UPDATE is used to lock the row for the duration of the transaction + // It is not allowed with aggregate functions (MAX in this case), so we need to wrap it in a subquery + let max_tx_k_query = "\ + SELECT COALESCE(MAX(tx_n), 0) \ + FROM (\ + SELECT * \ + FROM statechain_signature_data \ + WHERE statechain_id = $1 FOR UPDATE) AS result"; + + let row = sqlx::query(max_tx_k_query) + .bind(statechain_id) + .fetch_one(&mut *transaction) + .await + .unwrap(); + + let mut new_tx_n = row.get::(0); + new_tx_n = new_tx_n + 1; + + let query = "\ + INSERT INTO statechain_signature_data \ + (server_pubnonce, statechain_id, tx_n) \ + VALUES ($1, $2, $3)"; + + let _ = sqlx::query(query) + .bind(server_pubnonce) + .bind(statechain_id) + .bind(new_tx_n) + .execute(&mut *transaction) + .await + .unwrap(); + + transaction.commit().await.unwrap(); +} + +pub async fn update_signature_data_challenge(pool: &sqlx::PgPool, server_pub_nonce: &str, challenge: &str, statechain_id: &str) { + + println!("server_pub_nonce: {}", server_pub_nonce); + println!("challenge: {}", challenge); + println!("statechain_id: {}", statechain_id); + + let query = "\ + UPDATE statechain_signature_data \ + SET challenge = $1 \ + WHERE statechain_id = $2 AND server_pubnonce= $3"; + + let _ = sqlx::query(query) + .bind(challenge) + .bind(statechain_id) + .bind(server_pub_nonce) + .execute(pool) + .await + .unwrap(); +} diff --git a/server/src/endpoints/lightning_latch.rs b/server/src/endpoints/lightning_latch.rs index c9623860..41df1ee6 100644 --- a/server/src/endpoints/lightning_latch.rs +++ b/server/src/endpoints/lightning_latch.rs @@ -6,7 +6,7 @@ use hex::encode; use mercurylib::transfer::sender::{PaymentHashRequestPayload, PaymentHashResponsePayload, TransferPreimageRequestPayload, TransferPreimageResponsePayload}; use rand::Rng; use rocket::{State, serde::json::Json, response::status, http::Status}; -use secp256k1_zkp::{PublicKey, XOnlyPublicKey}; +use secp256k1_zkp::PublicKey; use serde_json::{json, Value}; use crate::server::StateChainEntity; diff --git a/server/src/endpoints/sign.rs b/server/src/endpoints/sign.rs index c0824cbe..c98b6d63 100644 --- a/server/src/endpoints/sign.rs +++ b/server/src/endpoints/sign.rs @@ -1,49 +1,11 @@ use mercurylib::transaction::SignFirstRequestPayload; -use rocket::{State, serde::json::Json, response::status, http::Status}; +use rocket::{http::Status, response::status, serde::json::Json, State}; use secp256k1_zkp::musig::MusigSession; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sqlx::Row; -use crate::server::StateChainEntity; -pub async fn insert_new_signature_data(pool: &sqlx::PgPool, server_pubnonce: &str, statechain_id: &str) { - - let mut transaction = pool.begin().await.unwrap(); - - // FOR UPDATE is used to lock the row for the duration of the transaction - // It is not allowed with aggregate functions (MAX in this case), so we need to wrap it in a subquery - let max_tx_k_query = "\ - SELECT COALESCE(MAX(tx_n), 0) \ - FROM (\ - SELECT * \ - FROM statechain_signature_data \ - WHERE statechain_id = $1 FOR UPDATE) AS result"; - - let row = sqlx::query(max_tx_k_query) - .bind(statechain_id) - .fetch_one(&mut *transaction) - .await - .unwrap(); - - let mut new_tx_n = row.get::(0); - new_tx_n = new_tx_n + 1; - - let query = "\ - INSERT INTO statechain_signature_data \ - (server_pubnonce, statechain_id, tx_n) \ - VALUES ($1, $2, $3)"; - - let _ = sqlx::query(query) - .bind(server_pubnonce) - .bind(statechain_id) - .bind(new_tx_n) - .execute(&mut *transaction) - .await - .unwrap(); - - transaction.commit().await.unwrap(); -} +use crate::server::StateChainEntity; #[post("/sign/first", format = "json", data = "")] pub async fn sign_first(statechain_entity: &State, sign_first_request_payload: Json) -> status::Custom> { @@ -87,6 +49,21 @@ pub async fn sign_first(statechain_entity: &State, sign_first_ return status::Custom(Status::InternalServerError, Json(response_body)); } + // This situation should not happen, as this state is only possible if the client has called signFirst, but not signSecond + // In this case, the server should have already stored server_pubnonce in the database and the challenge is still null because the client did not call signSecond + let server_pubnonce_hex = crate::database::sign::get_server_pubnonce_from_null_challenge(&statechain_entity.pool, &statechain_id).await; + + if server_pubnonce_hex.is_some() { + + let response = mercurylib::transaction::SignFirstResponsePayload { + server_pubnonce: server_pubnonce_hex.unwrap(), + }; + + let response_body = json!(response); + + return status::Custom(Status::Ok, Json(response_body)); + } + let value = match request.json(&sign_first_request_payload.0).send().await { Ok(response) => { let text = response.text().await.unwrap(); @@ -112,37 +89,13 @@ pub async fn sign_first(statechain_entity: &State, sign_first_ server_pubnonce_hex = server_pubnonce_hex[2..].to_string(); } - insert_new_signature_data(&statechain_entity.pool, &server_pubnonce_hex, &statechain_id,).await; + crate::database::sign::insert_new_signature_data(&statechain_entity.pool, &server_pubnonce_hex, &statechain_id,).await; let response_body = json!(response); -/* let response_body = json!({ - "server_pubnonce": hex::encode(server_pub_nonce.serialize()), - }); */ - return status::Custom(Status::Ok, Json(response_body)); } -pub async fn update_signature_data_challenge(pool: &sqlx::PgPool, server_pub_nonce: &str, challenge: &str, statechain_id: &str) { - - println!("server_pub_nonce: {}", server_pub_nonce); - println!("challenge: {}", challenge); - println!("statechain_id: {}", statechain_id); - - let query = "\ - UPDATE statechain_signature_data \ - SET challenge = $1 \ - WHERE statechain_id = $2 AND server_pubnonce= $3"; - - let _ = sqlx::query(query) - .bind(challenge) - .bind(statechain_id) - .bind(server_pub_nonce) - .execute(pool) - .await - .unwrap(); -} - #[post("/sign/second", format = "json", data = "")] pub async fn sign_second (statechain_entity: &State, partial_signature_request_payload: Json) -> status::Custom> { @@ -194,7 +147,7 @@ pub async fn sign_second (statechain_entity: &State, partial_s let challenge = session.get_challenge_from_session(); let challenge_str = hex::encode(challenge); - update_signature_data_challenge(&statechain_entity.pool, &server_pub_nonce, &challenge_str, &statechain_id).await; + crate::database::sign::update_signature_data_challenge(&statechain_entity.pool, &server_pub_nonce, &challenge_str, &statechain_id).await; let value = match request.json(&partial_signature_request_payload).send().await { Ok(response) => {