Skip to content

Commit

Permalink
add one-time borrowing example
Browse files Browse the repository at this point in the history
  • Loading branch information
biryukovmaxim committed Oct 15, 2024
1 parent 2892374 commit c86881f
Showing 1 changed file with 238 additions and 29 deletions.
267 changes: 238 additions & 29 deletions crypto/txscript/examples/kip-10.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use kaspa_addresses::{Address, Prefix, Version};
use kaspa_consensus_core::{
hashing::{
sighash::{calc_schnorr_signature_hash, SigHashReusedValues},
Expand All @@ -14,35 +15,39 @@ use kaspa_txscript::{
OpCheckSig, OpCheckSigVerify, OpDup, OpElse, OpEndIf, OpEqualVerify, OpFalse, OpGreaterThanOrEqual, OpIf, OpInputAmount,
OpInputSpk, OpOutputAmount, OpOutputSpk, OpSub, OpTrue,
},
pay_to_script_hash_script,
pay_to_address_script, pay_to_script_hash_script,
script_builder::{ScriptBuilder, ScriptBuilderResult},
TxScriptEngine,
};
use kaspa_txscript_errors::TxScriptError::{EvalFalse, VerifyError};
use rand::thread_rng;
use secp256k1::Keypair;

/// Main function to execute the Kaspa transaction script example.
/// Main function to execute all Kaspa transaction script scenarios.
///
/// # Returns
///
/// * `ScriptBuilderResult<()>` - Result of script builder operations.
/// * `ScriptBuilderResult<()>` - Result of script builder operations for all scenarios.
fn main() -> ScriptBuilderResult<()> {
threshold_scenario()?;
threshold_scenario_limited_one_time()?;
shared_secret_scenario()?;
Ok(())
}

/// # Kaspa Transaction Script Example
/// # Standard Threshold Scenario
///
/// This example demonstrates the use of custom opcodes and script execution within the Kaspa blockchain ecosystem.
/// There are two main scenarios:
/// This scenario demonstrates the use of custom opcodes and script execution within the Kaspa blockchain ecosystem.
/// There are two main cases:
///
/// 1. **Owner scenario:** The script checks if the input is used by the owner and verifies the owner's signature.
/// 2. **Borrower scenario:** The script allows the input to be consumed if the output with the same index has a value of input + threshold and goes to the P2SH of the script itself. This scenario also includes a check where the threshold is not reached.

/// 1. **Owner case:** The script checks if the input is used by the owner and verifies the owner's signature.
/// 2. **Borrower case:** The script allows the input to be consumed if the output with the same index has a value of input + threshold and goes to the P2SH of the script itself.
///
/// # Returns
///
/// * `ScriptBuilderResult<()>` - Result of script builder operations for this scenario.
fn threshold_scenario() -> ScriptBuilderResult<()> {
println!("\nrun threshold scenario");
println!("\n[STANDARD] Running standard threshold scenario");
// Create a new key pair for the owner
let owner = Keypair::new(secp256k1::SECP256K1, &mut thread_rng());

Expand Down Expand Up @@ -101,7 +106,7 @@ fn threshold_scenario() -> ScriptBuilderResult<()> {

// Check owner branch
{
println!("check owner branch in threshold scenario");
println!("[STANDARD] Checking owner branch");
let mut tx = MutableTransaction::with_entries(tx.clone(), vec![utxo_entry.clone()]);
let sig_hash = calc_schnorr_signature_hash(&tx.as_verifiable(), 0, SIG_HASH_ALL, &mut reused_values);
let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap();
Expand All @@ -124,50 +129,251 @@ fn threshold_scenario() -> ScriptBuilderResult<()> {
TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Ok(()));
println!("owner branch in threshold scenario successes");
println!("[STANDARD] Owner branch execution successful");
}

// Check borrower branch
{
println!("check borrower branch in threshold scenario");
println!("[STANDARD] Checking borrower branch");
tx.inputs[0].signature_script = ScriptBuilder::new().add_op(OpFalse)?.add_data(&script)?.drain();
let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]);
let mut vm =
TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Ok(()));
println!("borrower branch in threshold scenario successes");
println!("[STANDARD] Borrower branch execution successful");
}

// Check borrower branch with threshold not reached
{
println!("check borrower branch in threshold scenario with underflow");
println!("[STANDARD] Checking borrower branch with threshold not reached");
// Less than threshold
tx.outputs[0].value -= 1;
let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]);
let mut vm =
TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Err(EvalFalse));
println!("borrower branch in threshold scenario with underflow failed! all good");
println!("[STANDARD] Borrower branch with threshold not reached failed as expected");
}

println!("[STANDARD] Standard threshold scenario completed successfully");
Ok(())
}

/// Generate a script for the one-time borrowing scenario
///
/// This function creates a script that allows for one-time borrowing with a threshold,
/// or spending by the owner at any time.
///
/// # Arguments
///
/// * `owner` - The public key of the owner
/// * `threshold` - The threshold amount that must be met for borrowing
///
/// # Returns
///
/// * The generated script as a vector of bytes
fn generate_one_time_script(owner: &Keypair, threshold: i64) -> ScriptBuilderResult<Vec<u8>> {
let p2pk =
pay_to_address_script(&Address::new(Prefix::Mainnet, Version::PubKey, owner.x_only_public_key().0.serialize().as_slice()));
let p2pk_as_vec = {
let version = p2pk.version.to_be_bytes();
let script = p2pk.script();
let mut v = Vec::with_capacity(version.len() + script.len());
v.extend_from_slice(&version);
v.extend_from_slice(script);
v
};

let mut builder = ScriptBuilder::new();
let script = builder
// Owner branch
.add_op(OpIf)?
.add_data(owner.x_only_public_key().0.serialize().as_slice())?
.add_op(OpCheckSig)?
// Borrower branch
.add_op(OpElse)?
.add_data(&p2pk_as_vec)?
.add_ops(&[OpOutputSpk, OpEqualVerify, OpOutputAmount])?
.add_i64(threshold)?
.add_ops(&[OpSub, OpInputAmount, OpGreaterThanOrEqual])?
.add_op(OpEndIf)?
.drain();

Ok(script)
}

/// # Threshold Scenario with Limited One-Time Borrowing
///
/// This function demonstrates a modified version of the threshold scenario where borrowing
/// is limited to a single occurrence. The key difference from the standard threshold scenario
/// is that the output goes to a Pay-to-Public-Key (P2PK) address instead of a Pay-to-Script-Hash (P2SH)
/// address of the script itself.
///
/// ## Key Features:
/// 1. **One-Time Borrowing:** The borrower can only use this mechanism once, as the funds are
/// sent to a regular P2PK address instead of back to the script.
/// 2. **Owner Access:** The owner retains the ability to spend the funds at any time using their private key.
/// 3. **Threshold Mechanism:** The borrower must still meet the threshold requirement to spend the funds.
/// 4. **Output Validation:** Ensures the output goes to the correct address.
///
/// ## Scenarios Tested:
/// 1. **Owner Spending:** Verifies that the owner can spend the funds using their signature.
/// 2. **Borrower Spending:** Checks if the borrower can spend when meeting the threshold and
/// sending to the correct P2PK address.
/// 3. **Invalid Borrower Attempt (Threshold):** Ensures the script fails if the borrower doesn't meet the threshold.
/// 4. **Invalid Borrower Attempt (Wrong Output):** Ensures the script fails if the output goes to an incorrect address.
///
/// # Returns
///
/// * `ScriptBuilderResult<()>` - Result of script builder operations for this scenario.
fn threshold_scenario_limited_one_time() -> ScriptBuilderResult<()> {
println!("\n[ONE-TIME] Running threshold one-time scenario");
// Create a new key pair for the owner
let owner = Keypair::new(secp256k1::SECP256K1, &mut thread_rng());

// Set a threshold value for comparison
let threshold: i64 = 100;

// Generate the one-time script
let script = generate_one_time_script(&owner, threshold)?;
let p2pk = pay_to_address_script(&Address::new(Prefix::Mainnet, Version::PubKey, owner.x_only_public_key().0.serialize().as_slice()));

// Initialize a cache for signature verification
let sig_cache = Cache::new(10_000);

// Prepare to reuse values for signature hashing
let mut reused_values = SigHashReusedValues::new();

// Generate the script public key
let spk = pay_to_script_hash_script(&script);

// Define the input value
let input_value = 1000000000;

// Create a transaction output
let output = TransactionOutput { value: 1000000000 + threshold as u64, script_public_key: p2pk.clone() };

// Create a UTXO entry for the input
let utxo_entry = UtxoEntry::new(input_value, spk, 0, false);

// Create a transaction input
let input = TransactionInput {
previous_outpoint: TransactionOutpoint {
transaction_id: TransactionId::from_bytes([
0xc9, 0x97, 0xa5, 0xe5, 0x6e, 0x10, 0x42, 0x02, 0xfa, 0x20, 0x9c, 0x6a, 0x85, 0x2d, 0xd9, 0x06, 0x60, 0xa2, 0x0b,
0x2d, 0x9c, 0x35, 0x24, 0x23, 0xed, 0xce, 0x25, 0x85, 0x7f, 0xcd, 0x37, 0x04,
]),
index: 0,
},
signature_script: ScriptBuilder::new().add_data(&script)?.drain(),
sequence: 4294967295,
sig_op_count: 0,
};

// Create a transaction with the input and output
let mut tx = Transaction::new(1, vec![input.clone()], vec![output.clone()], 0, Default::default(), 0, vec![]);

// Check owner branch
{
println!("[ONE-TIME] Checking owner branch");
let mut tx = MutableTransaction::with_entries(tx.clone(), vec![utxo_entry.clone()]);
let sig_hash = calc_schnorr_signature_hash(&tx.as_verifiable(), 0, SIG_HASH_ALL, &mut reused_values);
let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap();

let sig = owner.sign_schnorr(msg);
let mut signature = Vec::new();
signature.extend_from_slice(sig.as_ref().as_slice());
signature.push(SIG_HASH_ALL.to_u8());

let mut builder = ScriptBuilder::new();
builder.add_data(&signature)?;
builder.add_op(OpTrue)?;
builder.add_data(&script)?;
{
tx.tx.inputs[0].signature_script = builder.drain();
}

let tx = tx.as_verifiable();
let mut vm =
TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Ok(()));
println!("[ONE-TIME] Owner branch execution successful");
}

// Check borrower branch
{
println!("[ONE-TIME] Checking borrower branch");
tx.inputs[0].signature_script = ScriptBuilder::new().add_op(OpFalse)?.add_data(&script)?.drain();
let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]);
let mut vm =
TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Ok(()));
println!("[ONE-TIME] Borrower branch execution successful");
}

// Check borrower branch with threshold not reached
{
println!("[ONE-TIME] Checking borrower branch with threshold not reached");
// Less than threshold
tx.outputs[0].value -= 1;
let tx = PopulatedTransaction::new(&tx, vec![utxo_entry.clone()]);
let mut vm =
TxScriptEngine::from_transaction_input(&tx, &tx.tx.inputs[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Err(EvalFalse));
println!("[ONE-TIME] Borrower branch with threshold not reached failed as expected");
}

// Check borrower branch with output going to wrong address
{
println!("[ONE-TIME] Checking borrower branch with output going to wrong address");
// Create a new key pair for a different address
let wrong_recipient = Keypair::new(secp256k1::SECP256K1, &mut thread_rng());
let wrong_p2pk = pay_to_address_script(&Address::new(Prefix::Mainnet, Version::PubKey, wrong_recipient.x_only_public_key().0.serialize().as_slice()));

// Create a new transaction with the wrong output address
let mut wrong_tx = tx.clone();
wrong_tx.outputs[0].script_public_key = wrong_p2pk;
wrong_tx.inputs[0].signature_script = ScriptBuilder::new().add_op(OpFalse)?.add_data(&script)?.drain();

let wrong_tx = PopulatedTransaction::new(&wrong_tx, vec![utxo_entry.clone()]);
let mut vm =
TxScriptEngine::from_transaction_input(&wrong_tx, &wrong_tx.tx.inputs[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Err(VerifyError));
println!("[ONE-TIME] Borrower branch with output going to wrong address failed as expected");
}

println!("[ONE-TIME] Threshold one-time scenario completed successfully");
Ok(())
}

/// # Shared Secret Scenario
///
/// This scenario demonstrates the use of a shared secret within the Kaspa blockchain ecosystem.
/// Instead of using a threshold value, it checks the shared secret and the signature associated with it.
/// There are three main sub-scenarios:
///
/// 1. **Owner scenario:** The script checks if the input is used by the owner and verifies the owner's signature.
/// 2. **Borrower scenario with shared secret:** The script allows the input to be consumed if the shared secret is verified.
/// 3. **Borrower scenario with incorrect secret:** The script fails if the borrower uses an incorrect secret.
/// ## Key Features:
/// 1. **Owner Access:** The owner can spend funds at any time using their signature.
/// 2. **Shared Secret:** A separate keypair is used as a shared secret for borrower access.
/// 3. **Borrower Verification:** The borrower must provide the correct shared secret signature to spend.
///
/// ## Scenarios Tested:
/// 1. **Owner Spending:** Verifies that the owner can spend the funds using their signature.
/// 2. **Borrower with Correct Secret:** Checks if the borrower can spend when providing the correct shared secret.
/// 3. **Borrower with Incorrect Secret:** Ensures the script fails if the borrower uses an incorrect secret.
///
/// # Returns
///
/// * `ScriptBuilderResult<()>` - Result of script builder operations for this scenario.
fn shared_secret_scenario() -> ScriptBuilderResult<()> {
println!("\nrun shared secret scenario");
println!("\n[SHARED-SECRET] Running shared secret scenario");

// Create a new key pair for the owner
// Create key pairs for the owner, shared secret, and a potential borrower
let owner = Keypair::new(secp256k1::SECP256K1, &mut thread_rng());
let shared_secret_kp = Keypair::new(secp256k1::SECP256K1, &mut thread_rng());
let borrower_kp = Keypair::new(secp256k1::SECP256K1, &mut thread_rng());
Expand Down Expand Up @@ -234,9 +440,10 @@ fn shared_secret_scenario() -> ScriptBuilderResult<()> {
signature.push(SIG_HASH_ALL.to_u8());
(tx, signature, reused_values)
};

// Check owner branch
{
println!("check owner branch in shared_secret_scenario");
println!("[SHARED-SECRET] Checking owner branch");
let (mut tx, signature, mut reused_values) = sign(owner);
let mut builder = ScriptBuilder::new();
builder.add_data(&signature)?;
Expand All @@ -251,12 +458,12 @@ fn shared_secret_scenario() -> ScriptBuilderResult<()> {
TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Ok(()));
println!("owner scenario in shared_secret_scenario successes");
println!("[SHARED-SECRET] Owner branch execution successful");
}

// Check borrower branch
// Check borrower branch with correct shared secret
{
println!("check borrower branch in shared_secret_scenario");
println!("[SHARED-SECRET] Checking borrower branch with correct shared secret");
let (mut tx, signature, mut reused_values) = sign(shared_secret_kp);
builder.add_data(&signature)?;
builder.add_data(shared_secret_kp.x_only_public_key().0.serialize().as_slice())?;
Expand All @@ -271,11 +478,12 @@ fn shared_secret_scenario() -> ScriptBuilderResult<()> {
TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Ok(()));
println!("borrower scenario successes in shared_secret_scenario");
println!("[SHARED-SECRET] Borrower branch with correct shared secret execution successful");
}

// Check borrower branch with borrower signature
// Check borrower branch with incorrect secret
{
println!("[SHARED-SECRET] Checking borrower branch with incorrect secret");
let (mut tx, signature, mut reused_values) = sign(borrower_kp);
builder.add_data(&signature)?;
builder.add_data(borrower_kp.x_only_public_key().0.serialize().as_slice())?;
Expand All @@ -290,8 +498,9 @@ fn shared_secret_scenario() -> ScriptBuilderResult<()> {
TxScriptEngine::from_transaction_input(&tx, &tx.inputs()[0], 0, &utxo_entry, &mut reused_values, &sig_cache, true)
.expect("Script creation failed");
assert_eq!(vm.execute(), Err(VerifyError));
println!("borrower scenario in shared_secret_scenario with wrong secret signature failed! all good");
println!("[SHARED-SECRET] Borrower branch with incorrect secret failed as expected");
}

println!("[SHARED-SECRET] Shared secret scenario completed successfully");
Ok(())
}

0 comments on commit c86881f

Please sign in to comment.