Skip to content

Commit

Permalink
Add TA01 - 'signSecond not called' and return the pubnonce if the cha…
Browse files Browse the repository at this point in the history
…llenge is null
  • Loading branch information
ssantos21 committed Jul 9, 2024
1 parent 77fa2e0 commit 4d8c40f
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 67 deletions.
8 changes: 8 additions & 0 deletions clients/libs/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions clients/tests/rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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(())
}
209 changes: 209 additions & 0 deletions clients/tests/rust/src/ta01_sign_second_not_called.rs
Original file line number Diff line number Diff line change
@@ -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<String> {

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<String>) -> 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<String>) -> Result<String> {

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(())
}
18 changes: 18 additions & 0 deletions docs/test_cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions server/src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod transfer;
pub mod deposit;
pub mod utils;
pub mod lightning_latch;
pub mod sign;
85 changes: 85 additions & 0 deletions server/src/database/sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use sqlx::Row;

pub async fn get_server_pubnonce_from_null_challenge(pool: &sqlx::PgPool, statechain_id: &str) -> Option<String> {

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::<i32, _>(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();
}
2 changes: 1 addition & 1 deletion server/src/endpoints/lightning_latch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 4d8c40f

Please sign in to comment.