diff --git a/checksums.txt b/checksums.txt index 32b3959..34febc9 100644 --- a/checksums.txt +++ b/checksums.txt @@ -1,4 +1,4 @@ -43f698790b28c433607bc5a2a2b9cbe5ed86ed43a753d014c9a0670f9274ce8d build/release/commitment-lock -41de10902949fd46cb07107068b57e32300fb3659311716c34f3392ca82d7fde build/release/commitment-lock.debug +a7762721ef239b15adbdddca8d772db6e1c001d9f81e23ccc74e1392329931d7 build/release/commitment-lock +ecf31c4c024d5f7339f56e19f0e7c2bdce6188b6c069752faefa5313f7ae534d build/release/commitment-lock.debug 831f164aad1509db5483dd50c50bf4ed5cb6601f941600287ffdb5e79a29cf9e build/release/funding-lock 291bdd4a48783ce75012a6e21935090806014d33bf6c52b1b2374e8feeaa91f0 build/release/funding-lock.debug diff --git a/contracts/commitment-lock/src/main.rs b/contracts/commitment-lock/src/main.rs index ac03e6e..0640dce 100644 --- a/contracts/commitment-lock/src/main.rs +++ b/contracts/commitment-lock/src/main.rs @@ -79,11 +79,47 @@ pub fn program_entry() -> i8 { // a placeholder for empty witness args, to resolve the issue of xudt compatibility const EMPTY_WITNESS_ARGS: [u8; 16] = [16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0]; -// HTLC script length: 1 (htlc_type) + 16 (payment_amount) + 20 (payment_hash) + 20 (remote_htlc_pubkey_hash) + 20 (local_htlc_pubkey_hash) + 8 (htlc_expiry) = 85 -const HTLC_SCRIPT_LEN: usize = 85; +// HTLC script length: 1 (htlc_type) + 16 (payment_amount) + payment_hash + 20 +// (remote_htlc_pubkey_hash) + 20 (local_htlc_pubkey_hash) + 8 (htlc_expiry) const SIGNATURE_LEN: usize = 65; const PREIMAGE_LEN: usize = 32; +// feature bitmap bit0: full 32-byte payment hash committed on-chain +const FEATURE_ONCHAIN_FULL_PAYMENT_HASH: u8 = 0b0000_0001; + +#[derive(Copy, Clone)] +struct HtlcLayout { + htlc_script_len: usize, + payment_hash_len: usize, +} + +const HTLC_LAYOUT_LEGACY: HtlcLayout = HtlcLayout { + htlc_script_len: 85, + payment_hash_len: 20, +}; + +const HTLC_LAYOUT_V1: HtlcLayout = HtlcLayout { + htlc_script_len: 97, // 1 (htlc_type) + 16 (payment_amount) + 32 (payment_hash) + // + 20 (remote_htlc_pubkey_hash) + 20 (local_htlc_pubkey_hash) + 8 (htlc_expiry) + payment_hash_len: 32, +}; + +fn resolve_htlc_layout(args: &[u8]) -> Result { + match args.len() { + 57 => Ok(HTLC_LAYOUT_LEGACY), + 58 if args[57] == FEATURE_ONCHAIN_FULL_PAYMENT_HASH => Ok(HTLC_LAYOUT_V1), + _ => Err(Error::ArgsLenError), + } +} + +fn preimage_matches(hash_type: PaymentHashType, committed_hash: &[u8], preimage: &[u8]) -> bool { + let digest = match hash_type { + PaymentHashType::Blake2b => blake2b_256(preimage), + PaymentHashType::Sha256 => Sha256::digest(preimage).into(), + }; + committed_hash == &digest[..committed_hash.len()] +} + enum HtlcType { Offered, Received, @@ -94,11 +130,21 @@ enum PaymentHashType { Sha256, } -struct Htlc<'a>(&'a [u8]); +struct Htlc<'a> { + data: &'a [u8], + payment_hash_len: usize, +} impl<'a> Htlc<'a> { + fn new(data: &'a [u8], layout: HtlcLayout) -> Self { + Self { + data, + payment_hash_len: layout.payment_hash_len, + } + } + fn htlc_type(&self) -> HtlcType { - if self.0[0] & 0b00000001 == 0 { + if self.data[0] & 0b00000001 == 0 { HtlcType::Offered } else { HtlcType::Received @@ -106,7 +152,7 @@ impl<'a> Htlc<'a> { } fn payment_hash_type(&self) -> PaymentHashType { - if (self.0[0] >> 1) & 0b0000001 == 0 { + if (self.data[0] >> 1) & 0b0000001 == 0 { PaymentHashType::Blake2b } else { PaymentHashType::Sha256 @@ -114,23 +160,26 @@ impl<'a> Htlc<'a> { } fn payment_amount(&self) -> u128 { - u128::from_le_bytes(self.0[1..17].try_into().unwrap()) + u128::from_le_bytes(self.data[1..17].try_into().unwrap()) } fn payment_hash(&self) -> &'a [u8] { - &self.0[17..37] + &self.data[17..17 + self.payment_hash_len] } fn remote_htlc_pubkey_hash(&self) -> [u8; 20] { - self.0[37..57].try_into().unwrap() + let off = 17 + self.payment_hash_len; + self.data[off..off + 20].try_into().unwrap() } fn local_htlc_pubkey_hash(&self) -> [u8; 20] { - self.0[57..77].try_into().unwrap() + let off = 17 + self.payment_hash_len + 20; + self.data[off..off + 20].try_into().unwrap() } fn htlc_expiry(&self) -> u64 { - u64::from_le_bytes(self.0[77..85].try_into().unwrap()) + let off = 17 + self.payment_hash_len + 40; + u64::from_le_bytes(self.data[off..off + 8].try_into().unwrap()) } } @@ -169,9 +218,7 @@ fn auth() -> Result<(), Error> { let script = load_script()?; let args: Bytes = script.args().unpack(); - if args.len() != 57 { - return Err(Error::ArgsLenError); - } + let layout = resolve_htlc_layout(args.as_ref())?; let is_first_settlement = args[56] == 0x00; let mut witness = load_witness(0, Source::GroupInput)?; @@ -231,8 +278,8 @@ fn auth() -> Result<(), Error> { } else { // settlement unlock process let pending_htlc_count = witness[0] as usize; - // 1 (pending_htlc_count) + pending_htlc_count * HTLC_SCRIPT_LEN - let pending_htlcs_len = 1 + pending_htlc_count * HTLC_SCRIPT_LEN; + // 1 (pending_htlc_count) + pending_htlc_count * HtlcLayout::htlc_script_len + let pending_htlcs_len = 1 + pending_htlc_count * layout.htlc_script_len; // settlement_remote_pubkey_hash + settlement_remote_amount + settlement_local_pubkey_hash + settlement_local_amount let settlement_script_len = pending_htlcs_len + 72; if witness.len() < settlement_script_len { @@ -297,11 +344,11 @@ fn auth() -> Result<(), Error> { let mut signatures_to_verify = Vec::new(); for (i, htlc_script) in witness[1..pending_htlcs_len] - .chunks(HTLC_SCRIPT_LEN) + .chunks(layout.htlc_script_len) .enumerate() { if !settlements.is_empty() && settlements[0].unlock_type() == i as u8 { - let htlc = Htlc(htlc_script); + let htlc = Htlc::new(htlc_script, layout); match htlc.htlc_type() { HtlcType::Offered => { if raw_since_value == 0 { @@ -311,14 +358,11 @@ fn auth() -> Result<(), Error> { } // when input since is 0, it means the unlock logic is for remote_htlc pubkey and preimage let preimage = settlements[0].preimage(); - if match htlc.payment_hash_type() { - PaymentHashType::Blake2b => { - htlc.payment_hash() != &blake2b_256(preimage)[0..20] - } - PaymentHashType::Sha256 => { - htlc.payment_hash() != &Sha256::digest(preimage)[0..20] - } - } { + if !preimage_matches( + htlc.payment_hash_type(), + htlc.payment_hash(), + preimage, + ) { return Err(Error::PreimageError); } new_amount = new_amount.saturating_sub(htlc.payment_amount()); @@ -353,14 +397,11 @@ fn auth() -> Result<(), Error> { } // when input since is 0, it means the unlock logic is for local_htlc pubkey and preimage let preimage = settlements[0].preimage(); - if match htlc.payment_hash_type() { - PaymentHashType::Blake2b => { - htlc.payment_hash() != &blake2b_256(preimage)[0..20] - } - PaymentHashType::Sha256 => { - htlc.payment_hash() != &Sha256::digest(preimage)[0..20] - } - } { + if !preimage_matches( + htlc.payment_hash_type(), + htlc.payment_hash(), + preimage, + ) { return Err(Error::PreimageError); } new_amount = new_amount.saturating_sub(htlc.payment_amount()); @@ -463,13 +504,22 @@ fn auth() -> Result<(), Error> { // verify the first output cell's lock script and capacity are correct if new_amount > 0 && !two_parties_all_settled { let output_lock = load_cell_lock(0, Source::Output)?; + // propagate the layout version into the derived cell: v1 cells + // (58-byte args) keep their features byte at [57], legacy cells + // stay at 57 bytes; otherwise a v1 partial settlement would + // produce a 57-byte derived cell and brick the remaining v1 + // pending HTLCs + let features_byte: &[u8] = if args.len() == 58 { &args[57..58] } else { &[] }; let expected_lock_args = [ &args[0..36], blake2b_256(new_settlement_script.concat())[0..20].as_ref(), &[0x01], // 0x01 means new cell is created for subsequent commitment cell unlock + features_byte, ] .concat() .pack(); + // keep the original combined check so legacy failure codes stay + // identical (OutputLockError) if output_lock.code_hash() != script.code_hash() || output_lock.hash_type() != script.hash_type() || output_lock.args() != expected_lock_args diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 4fba48e..1e98c58 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -929,7 +929,10 @@ fn test_commitment_lock_with_two_pending_htlcs() { .build(); let outputs = vec![ CellOutput::new_builder() - .capacity(((local_amount + payment_amount1 + payment_amount2) as u64).pack()) + .capacity( + ((local_amount + 5 * BYTE_SHANNONS as u128 + 8 * BYTE_SHANNONS as u128) as u64) + .pack(), + ) .lock(new_lock_script.clone()) .build(), ]; @@ -1480,3 +1483,1772 @@ fn test_commitment_lock_with_two_pending_htlcs_and_sudt() { .expect_err("empty preimage should fail"); assert!(err.to_string().contains("#22")); // PreimageError } + +// v1 HTLC entry layout: 1 (htlc_type) + 16 (payment_amount) + 32 (payment_hash) +// + 20 (remote_htlc_pubkey_hash) + 20 (local_htlc_pubkey_hash) + 8 (htlc_expiry) = 97 +fn build_htlc_entry_v1( + htlc_type: u8, + payment_amount: u128, + payment_hash: &[u8], + remote_key_hash: &[u8], + local_key_hash: &[u8], + expiry_since: u64, +) -> Vec { + let mut vec = Vec::new(); + vec.push(htlc_type); + vec.extend_from_slice(&payment_amount.to_le_bytes()); + vec.extend_from_slice(payment_hash); + vec.extend_from_slice(remote_key_hash); + vec.extend_from_slice(local_key_hash); + vec.extend_from_slice(&expiry_since.to_le_bytes()); + assert_eq!(vec.len(), 97); + vec +} + +#[test] +fn v1_settlement_with_preimage_unlock_succeeds() { + // deploy contract + let mut context = Context::default(); + let loader = Loader::default(); + let commitment_lock_bin = loader.load_binary("commitment-lock"); + let auth_bin = loader.load_binary("../../deps/auth"); + let commitment_lock_out_point = context.deploy_cell(commitment_lock_bin); + let auth_out_point = context.deploy_cell(auth_bin); + let always_success_out_point = context.deploy_cell(ALWAYS_SUCCESS.clone()); + + // prepare script + let (_sec_key_1, _sec_key_2, key_agg_ctx) = generate_multisig_keys(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + let x_only_pubkey = aggregated_pubkey.x_only_public_key().0.serialize(); + let pubkey_hash = blake2b_256(x_only_pubkey); + let delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(10, 1, 2), false); // 10.5 epoch =~ 42 hours + let half_delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(5, 0, 1), false); // 5 epoch + let commitment_tx_version = 42u64; + + let mut generator = Generator::new(); + let remote_settlement_key = generator.gen_keypair(); + let remote_amount = (400 * BYTE_SHANNONS) as u128; + let local_settlement_key = generator.gen_keypair(); + let local_amount = (600 * BYTE_SHANNONS) as u128; + + let remote_htlc_key1 = generator.gen_keypair(); + let remote_htlc_key2 = generator.gen_keypair(); + let local_htlc_key1 = generator.gen_keypair(); + let local_htlc_key2 = generator.gen_keypair(); + let preimage1 = [42u8; 32]; + let preimage2 = [24u8; 32]; + let payment_amount1 = 5 * BYTE_SHANNONS as u128; + let payment_amount2 = 8 * BYTE_SHANNONS as u128; + // timeout after 2024-04-01 01:00:00 + let expiry1 = Since::from_timestamp(1711976400, true).unwrap(); + // timeout after 2024-04-02 01:00:00 + let expiry2 = Since::from_timestamp(1712062800, true).unwrap(); + + let htlc1_entry = build_htlc_entry_v1( + 0b00000000, + payment_amount1, + &blake2b_256(preimage1), + &blake2b_256(remote_htlc_key1.1.serialize())[0..20], + &blake2b_256(local_htlc_key1.1.serialize())[0..20], + expiry1.as_u64(), + ); + let htlc2_entry = build_htlc_entry_v1( + 0b00000011, + payment_amount2, + &Sha256::digest(preimage2), + &blake2b_256(remote_htlc_key2.1.serialize())[0..20], + &blake2b_256(local_htlc_key2.1.serialize())[0..20], + expiry2.as_u64(), + ); + + let pending_htlcs = [[2].to_vec(), htlc1_entry.clone(), htlc2_entry.clone()].concat(); + + let two_party_settlement = [ + blake2b_256(remote_settlement_key.1.serialize())[0..20].to_vec(), + remote_amount.to_le_bytes().to_vec(), + blake2b_256(local_settlement_key.1.serialize())[0..20].to_vec(), + local_amount.to_le_bytes().to_vec(), + ] + .concat(); + + let settlement_script = [pending_htlcs.clone(), two_party_settlement.clone()].concat(); + + let mut args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&settlement_script)[0..20], + &[0x00], + ] + .concat(); + // v1: args gain one byte (57 -> 58), [57] = feature bitmap, bit0 = ONCHAIN_FULL_PAYMENT_HASH + args.push(0x01); + + let lock_script = context + .build_script(&commitment_lock_out_point, args.clone().into()) + .expect("script"); + let always_success_script = context + .build_script(&always_success_out_point, Bytes::new()) + .expect("script"); + + // prepare cell deps + let commitment_lock_dep = CellDep::new_builder() + .out_point(commitment_lock_out_point) + .build(); + let auth_dep = CellDep::new_builder().out_point(auth_out_point).build(); + let always_success_dep = CellDep::new_builder() + .out_point(always_success_out_point) + .build(); + let cell_deps = vec![commitment_lock_dep, auth_dep, always_success_dep].pack(); + + // prepare cells + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity( + ((local_amount + remote_amount + payment_amount1 + payment_amount2) as u64).pack(), + ) + .lock(lock_script.clone()) + .build(), + Bytes::new(), + ); + let delay_input_out_point = context.create_cell( + CellOutput::new_builder() + .lock(always_success_script) + .build(), + Bytes::new(), + ); + + // build transaction with remote_htlc_pubkey unlock offered pending htlc 1 + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + + let new_pending_htlcs = [[1].to_vec(), htlc2_entry.clone()].concat(); + + let new_settlement_script = [new_pending_htlcs.clone(), two_party_settlement.clone()].concat(); + let mut new_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(new_settlement_script)[0..20], + &[0x01], + ] + .concat(); + // propagate the v1 features byte into the derived cell args + new_args.push(0x01); + let new_lock_script = lock_script + .clone() + .as_builder() + .args(new_args.pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount2) as u64).pack()) + .lock(new_lock_script.clone()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with remote_htlc_key1 + let message: [u8; 32] = compute_tx_message(&tx); + + let signature = remote_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock with remote_htlc_key1 and preimage + signature.clone(), + preimage1.to_vec(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("pass verification"); + println!("consume cycles: {}", cycles); + + // sign with remote_htlc_key1 and wrong preimage should fail + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock with remote_htlc_key1 and preimage + signature.clone(), + preimage2.to_vec(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + + // run + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("wrong preimage should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#22")); // PreimageError + + // sign with remote_htlc_pubkey and empty preimage should fail + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x00].to_vec(), // unlock with remote_htlc_key1 and no preimage + signature.clone(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + + // run + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("empty preimage should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#22")); // PreimageError + + // build transaction with local_htlc_pubkey unlock offered pending htlc 1 + let since = Since::from_timestamp(1711976400 + 1000, true).unwrap(); + + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(delay_epoch.as_u64().pack()) + .build(); + let delay_epoch_input = CellInput::new_builder() + .previous_output(delay_input_out_point.clone()) + .since(since.as_u64().pack()) + .build(); + let inputs = vec![input, delay_epoch_input.clone()]; + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount2) as u64).pack()) + .lock(new_lock_script.clone()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs.clone()) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with local_htlc_pubkey + let message: [u8; 32] = compute_tx_message(&tx); + + let signature = local_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x00].to_vec(), // unlock with local_htlc_key1 and no preimage + signature.clone(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("pass verification"); + println!("consume cycles: {}", cycles); + + // sign with local_htlc_pubkey and none-expired since should fail + let since = Since::from_timestamp(1711976400 - 1000, true).unwrap(); + + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![ + input, + delay_epoch_input + .clone() + .as_builder() + .since(since.as_u64().pack()) + .build(), + ]; + + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with local_htlc_pubkey + let message: [u8; 32] = compute_tx_message(&tx); + + let signature = local_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x00].to_vec(), // unlock with local_htlc_key1 and no preimage + signature.clone(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("none-expired since should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#11")); // InvalidExpiry + + // build transaction with remote_htlc_pubkey2 unlock received pending htlc 2 + let since = Since::from_timestamp(1712062800 + 1000, true).unwrap(); + let input = CellInput::new_builder() + .since(delay_epoch.as_u64().pack()) + .previous_output(input_out_point.clone()) + .build(); + let inputs = vec![ + input, + delay_epoch_input + .clone() + .as_builder() + .since(since.as_u64().pack()) + .build(), + ]; + + let new_pending_htlcs = [[1].to_vec(), htlc1_entry.clone()].concat(); + let new_settlement_script = [new_pending_htlcs.clone(), two_party_settlement.clone()].concat(); + + let mut new_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(new_settlement_script)[0..20], + &[0x01], + ] + .concat(); + // propagate the v1 features byte into the derived cell args + new_args.push(0x01); + let new_lock_script = lock_script + .clone() + .as_builder() + .args(new_args.pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount1) as u64).pack()) + .lock(new_lock_script.clone()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with remote_htlc_pubkey2 + let message: [u8; 32] = compute_tx_message(&tx); + + let signature = remote_htlc_key2 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x01, 0x00].to_vec(), // unlock with remote_htlc_pubkey2 and no preimage + signature.clone(), + ] + .concat(); + + let tx = tx.as_advanced_builder().witness(witness.pack()).build(); + + // run + let cycles = context + .verify_tx(&tx, MAX_CYCLES) + .expect("pass verification"); + println!("consume cycles: {}", cycles); + + // build transaction with local_htlc_pubkey2 unlock received pending htlc 2 + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(half_delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount1) as u64).pack()) + .lock(new_lock_script.clone()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with local_htlc_key2 + let message: [u8; 32] = compute_tx_message(&tx); + + let signature = local_htlc_key2 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x01, 0x01].to_vec(), // unlock with local_htlc_key2 and preimage + signature.clone(), + preimage2.to_vec(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("pass verification"); + println!("consume cycles: {}", cycles); + + // sign with local_htlc_key2 and wrong preimage should fail + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x01, 0x01].to_vec(), // unlock with local_htlc_key2 and preimage + signature.clone(), + preimage1.to_vec(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("wrong preimage should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#22")); // PreimageError + + // sign with local_htlc_key2 and empty preimage should fail + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x01, 0x00].to_vec(), // unlock with local_htlc_key2 and no preimage + signature, + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("empty preimage should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#22")); // PreimageError + + // test with settlement unlock logic (remote settlement key) + let new_two_party_settlement = [ + [0u8; 36].to_vec(), + blake2b_256(local_settlement_key.1.serialize())[0..20].to_vec(), + local_amount.to_le_bytes().to_vec(), + ] + .concat(); + + let new_settlement_script = [pending_htlcs.clone(), new_two_party_settlement.clone()].concat(); + let mut new_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&new_settlement_script)[0..20], + &[0x01], + ] + .concat(); + // propagate the v1 features byte into the derived cell args + new_args.push(0x01); + + let mut new_lock_script = lock_script + .clone() + .as_builder() + .args(new_args.pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity( + ((local_amount + 5 * BYTE_SHANNONS as u128 + 8 * BYTE_SHANNONS as u128) as u64) + .pack(), + ) + .lock(new_lock_script.clone()) + .build(), + ]; + + let outputs_data = [Bytes::new()]; + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs.clone()) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with remote_settlement_key + let message: [u8; 32] = compute_tx_message(&tx); + + let signature = remote_settlement_key + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + vec![0xFE, 0x00], // unlock with remote settlement key, + signature.clone(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("pass verification"); + println!("consume cycles: {}", cycles); + + // test with batch unlock logic (remote settlement key + remote htlc key1) + let new_two_party_settlement = [ + [0u8; 36].to_vec(), + blake2b_256(local_settlement_key.1.serialize())[0..20].to_vec(), + local_amount.to_le_bytes().to_vec(), + ] + .concat(); + + let new_pending_htlcs = [[1].to_vec(), htlc2_entry.clone()].concat(); + let new_settlement_script = [new_pending_htlcs.clone(), new_two_party_settlement].concat(); + let mut new_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&new_settlement_script)[0..20], + &[0x01], + ] + .concat(); + // propagate the v1 features byte into the derived cell args + new_args.push(0x01); + + new_lock_script = lock_script + .clone() + .as_builder() + .args(new_args.pack()) + .build(); + + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + payment_amount2) as u64).pack()) + .lock(new_lock_script.clone()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs.clone()) + .outputs_data(outputs_data.pack()) + .build(); + // sign with remote_settlement_key and remote_htlc_key1 + let message: [u8; 32] = compute_tx_message(&tx); + + let signature1 = remote_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + + let signature2 = remote_settlement_key + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x02], + settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock with remote_htlc_key1 and preimage + signature1.clone(), + preimage1.to_vec(), + [0xFE, 0x00].to_vec(), // unlock with remote settlement + signature2.clone(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("pass verification"); + println!("consume cycles: {}", cycles); +} + +// v1 regression test for the prefix-truncation attack: the committed on-chain +// hash contains the 20-byte prefix of hash(preimage) plus 12 junk bytes; the +// legacy contract would accept, the v1 contract must reject with PreimageError + +#[test] +fn v1_settlement_rejects_prefix_only_preimage() { + // deploy contract + let mut context = Context::default(); + let loader = Loader::default(); + let commitment_lock_bin = loader.load_binary("commitment-lock"); + let auth_bin = loader.load_binary("../../deps/auth"); + let commitment_lock_out_point = context.deploy_cell(commitment_lock_bin); + let auth_out_point = context.deploy_cell(auth_bin); + let _always_success_out_point = context.deploy_cell(ALWAYS_SUCCESS.clone()); + + // prepare script + let (_sec_key_1, _sec_key_2, key_agg_ctx) = generate_multisig_keys(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + let x_only_pubkey = aggregated_pubkey.x_only_public_key().0.serialize(); + let pubkey_hash = blake2b_256(x_only_pubkey); + let delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(10, 1, 2), false); // 10.5 epoch =~ 42 hours + let commitment_tx_version = 42u64; + + let mut generator = Generator::new(); + let remote_settlement_key = generator.gen_keypair(); + let remote_amount = (400 * BYTE_SHANNONS) as u128; + let local_settlement_key = generator.gen_keypair(); + let local_amount = (600 * BYTE_SHANNONS) as u128; + + let remote_htlc_key1 = generator.gen_keypair(); + let local_htlc_key1 = generator.gen_keypair(); + let preimage1 = [42u8; 32]; + let payment_amount1 = 5 * BYTE_SHANNONS as u128; + // timeout after 2024-04-01 01:00:00 + let expiry1 = Since::from_timestamp(1711976400, true).unwrap(); + + // committed hash: valid 20-byte prefix of hash(preimage1) followed by 12 + // attacker-controlled bytes + let mut committed_payment_hash = blake2b_256(preimage1)[0..20].to_vec(); + committed_payment_hash.extend_from_slice(&[0xAA; 12]); + + let htlc1_entry = build_htlc_entry_v1( + 0b00000000, + payment_amount1, + &committed_payment_hash, + &blake2b_256(remote_htlc_key1.1.serialize())[0..20], + &blake2b_256(local_htlc_key1.1.serialize())[0..20], + expiry1.as_u64(), + ); + + let pending_htlcs = [[1].to_vec(), htlc1_entry.clone()].concat(); + + let two_party_settlement = [ + blake2b_256(remote_settlement_key.1.serialize())[0..20].to_vec(), + remote_amount.to_le_bytes().to_vec(), + blake2b_256(local_settlement_key.1.serialize())[0..20].to_vec(), + local_amount.to_le_bytes().to_vec(), + ] + .concat(); + + let settlement_script = [pending_htlcs.clone(), two_party_settlement.clone()].concat(); + + let mut args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&settlement_script)[0..20], + &[0x00], + ] + .concat(); + args.push(0x01); + + let lock_script = context + .build_script(&commitment_lock_out_point, args.clone().into()) + .expect("script"); + + // prepare cell deps + let commitment_lock_dep = CellDep::new_builder() + .out_point(commitment_lock_out_point) + .build(); + let auth_dep = CellDep::new_builder().out_point(auth_out_point).build(); + let always_success_dep = CellDep::new_builder() + .out_point(context.deploy_cell(ALWAYS_SUCCESS.clone())) + .build(); + let cell_deps = vec![commitment_lock_dep, auth_dep, always_success_dep].pack(); + + // prepare cells + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount1) as u64).pack()) + .lock(lock_script.clone()) + .build(), + Bytes::new(), + ); + + // build transaction with remote_htlc_pubkey unlock offered pending htlc 1 + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + + let new_settlement_script = [[0].to_vec(), two_party_settlement.clone()].concat(); + let new_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(new_settlement_script)[0..20], + &[0x01], + ] + .concat(); + let new_lock_script = lock_script + .clone() + .as_builder() + .args(new_args.pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount) as u64).pack()) + .lock(new_lock_script.clone()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with remote_htlc_key1 and the correct preimage must still fail, + // because the committed hash is not the full hash of the preimage + let message: [u8; 32] = compute_tx_message(&tx); + + let signature = remote_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock with remote_htlc_key1 and preimage + signature.clone(), + preimage1.to_vec(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + + // run + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("prefix-only payment hash should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#22")); // PreimageError +} + +// v1 feature bitmap with unknown bit1 set must be rejected with ArgsLenError +#[test] +fn v1_args_with_unknown_flag_bits_rejected() { + // deploy contract + let mut context = Context::default(); + let loader = Loader::default(); + let commitment_lock_bin = loader.load_binary("commitment-lock"); + let auth_bin = loader.load_binary("../../deps/auth"); + let commitment_lock_out_point = context.deploy_cell(commitment_lock_bin); + let auth_out_point = context.deploy_cell(auth_bin); + + // prepare script + let (_sec_key_1, _sec_key_2, key_agg_ctx) = generate_multisig_keys(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + let x_only_pubkey = aggregated_pubkey.x_only_public_key().0.serialize(); + let pubkey_hash = blake2b_256(x_only_pubkey); + let delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(10, 1, 2), false); + let commitment_tx_version = 42u64; + + let settlement_script = [[0x00u8; 97].to_vec(), [0x00u8; 72].to_vec()].concat(); + + let mut args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&settlement_script)[0..20], + &[0x00], + ] + .concat(); + // unknown feature bit1 set + args.push(0x03); + + let lock_script = context + .build_script(&commitment_lock_out_point, args.clone().into()) + .expect("script"); + + // prepare cell deps + let commitment_lock_dep = CellDep::new_builder() + .out_point(commitment_lock_out_point) + .build(); + let auth_dep = CellDep::new_builder().out_point(auth_out_point).build(); + let cell_deps = vec![commitment_lock_dep, auth_dep].pack(); + + // prepare cells and transaction + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity((1000u64).pack()) + .lock(lock_script.clone()) + .build(), + Bytes::new(), + ); + let input = CellInput::new_builder() + .previous_output(input_out_point) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity((1000u64).pack()) + .lock(Script::new_builder().build()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps) + .input(input) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .witness( + [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x00].to_vec(), + [0u8; 65].to_vec(), + ] + .concat() + .pack(), + ) + .build(); + + // run + let error = context + .verify_tx(&tx, MAX_CYCLES) + .expect_err("unknown feature bits should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#12")); // ArgsLenError +} + +// 58-byte args with a zero feature bitmap must NOT fall through to the legacy +// layout; they must be rejected with ArgsLenError +#[test] +fn v1_zero_mask_rejected() { + // deploy contract + let mut context = Context::default(); + let loader = Loader::default(); + let commitment_lock_bin = loader.load_binary("commitment-lock"); + let auth_bin = loader.load_binary("../../deps/auth"); + let commitment_lock_out_point = context.deploy_cell(commitment_lock_bin); + let auth_out_point = context.deploy_cell(auth_bin); + + // prepare script + let (_sec_key_1, _sec_key_2, key_agg_ctx) = generate_multisig_keys(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + let x_only_pubkey = aggregated_pubkey.x_only_public_key().0.serialize(); + let pubkey_hash = blake2b_256(x_only_pubkey); + let delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(10, 1, 2), false); + let commitment_tx_version = 42u64; + + let settlement_script = [[0x00u8; 97].to_vec(), [0x00u8; 72].to_vec()].concat(); + + let mut args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&settlement_script)[0..20], + &[0x00], + ] + .concat(); + // zero feature bitmap on 58 bytes is explicitly invalid + args.push(0x00); + + let lock_script = context + .build_script(&commitment_lock_out_point, args.clone().into()) + .expect("script"); + + // prepare cell deps + let commitment_lock_dep = CellDep::new_builder() + .out_point(commitment_lock_out_point) + .build(); + let auth_dep = CellDep::new_builder().out_point(auth_out_point).build(); + let cell_deps = vec![commitment_lock_dep, auth_dep].pack(); + + // prepare cells and transaction + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity((1000u64).pack()) + .lock(lock_script.clone()) + .build(), + Bytes::new(), + ); + let input = CellInput::new_builder() + .previous_output(input_out_point) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity((1000u64).pack()) + .lock(Script::new_builder().build()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps) + .input(input) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .witness( + [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x00].to_vec(), + [0u8; 65].to_vec(), + ] + .concat() + .pack(), + ) + .build(); + + // run + let error = context + .verify_tx(&tx, MAX_CYCLES) + .expect_err("zero feature mask should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#12")); // ArgsLenError +} + +// The args claiming v1 (flag byte at [57]) while the witness and the +// committed snapshot hash use legacy 85-byte HTLC entries: the parser derives +// the witness lengths from the v1 layout, so the snapshot hash check runs over +// a longer slice than the committed script bytes and must reject with +// WitnessHashError. +#[test] +fn v1_settlement_rejects_legacy_htlc_entries() { + // deploy contract + let mut context = Context::default(); + let loader = Loader::default(); + let commitment_lock_bin = loader.load_binary("commitment-lock"); + let auth_bin = loader.load_binary("../../deps/auth"); + let commitment_lock_out_point = context.deploy_cell(commitment_lock_bin); + let auth_out_point = context.deploy_cell(auth_bin); + + // prepare script + let (_sec_key_1, _sec_key_2, key_agg_ctx) = generate_multisig_keys(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + let x_only_pubkey = aggregated_pubkey.x_only_public_key().0.serialize(); + let pubkey_hash = blake2b_256(x_only_pubkey); + let delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(10, 1, 2), false); + let commitment_tx_version = 42u64; + + let mut generator = Generator::new(); + let remote_settlement_key = generator.gen_keypair(); + let remote_amount = (400 * BYTE_SHANNONS) as u128; + let local_settlement_key = generator.gen_keypair(); + let local_amount = (600 * BYTE_SHANNONS) as u128; + + let remote_htlc_key1 = generator.gen_keypair(); + let local_htlc_key1 = generator.gen_keypair(); + let preimage1 = [42u8; 32]; + let payment_amount1 = 5 * BYTE_SHANNONS as u128; + // timeout after 2024-04-01 01:00:00 + let expiry1 = Since::from_timestamp(1711976400, true).unwrap(); + + // legacy 85-byte HTLC entry committed at commitment time + let legacy_htlc_entry = [ + [0b00000000].to_vec(), + payment_amount1.to_le_bytes().to_vec(), + blake2b_256(preimage1)[0..20].to_vec(), + blake2b_256(remote_htlc_key1.1.serialize())[0..20].to_vec(), + blake2b_256(local_htlc_key1.1.serialize())[0..20].to_vec(), + expiry1.as_u64().to_le_bytes().to_vec(), + ] + .concat(); + + let pending_htlcs = [[1].to_vec(), legacy_htlc_entry.clone()].concat(); + let two_party_settlement = [ + blake2b_256(remote_settlement_key.1.serialize())[0..20].to_vec(), + remote_amount.to_le_bytes().to_vec(), + blake2b_256(local_settlement_key.1.serialize())[0..20].to_vec(), + local_amount.to_le_bytes().to_vec(), + ] + .concat(); + + let settlement_script = [pending_htlcs.clone(), two_party_settlement.clone()].concat(); + + let mut args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&settlement_script)[0..20], + &[0x00], + ] + .concat(); + // the fingerprint hash at [36..56] does not cover this flag byte, so the + // commitment is otherwise a valid legacy one; the args now claim v1 + args.push(0x01); + + let lock_script = context + .build_script(&commitment_lock_out_point, args.clone().into()) + .expect("script"); + + // prepare cell deps + let commitment_lock_dep = CellDep::new_builder() + .out_point(commitment_lock_out_point) + .build(); + let auth_dep = CellDep::new_builder().out_point(auth_out_point).build(); + let cell_deps = vec![commitment_lock_dep, auth_dep].pack(); + + // prepare cells + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount1) as u64).pack()) + .lock(lock_script.clone()) + .build(), + Bytes::new(), + ); + let input = CellInput::new_builder() + .previous_output(input_out_point) + .since(delay_epoch.as_u64().pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount) as u64).pack()) + .lock(Script::new_builder().build()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps) + .inputs(vec![input]) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with remote_htlc_key1 + let message: [u8; 32] = compute_tx_message(&tx); + let signature = remote_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock offered htlc1 with preimage + signature.clone(), + preimage1.to_vec(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + + // run + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("legacy witness entries with v1 args should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#15")); // WitnessHashError +} + +// The args staying legacy (57 bytes, committed snapshot hash over +// legacy-summed 85-byte entries) while the witness carries v1 97-byte HTLC +// entries: the legacy arithmetic hashes a different slice than committed and +// must reject with WitnessHashError. +#[test] +fn legacy_settlement_rejects_v1_htlc_entries() { + // deploy contract + let mut context = Context::default(); + let loader = Loader::default(); + let commitment_lock_bin = loader.load_binary("commitment-lock"); + let auth_bin = loader.load_binary("../../deps/auth"); + let commitment_lock_out_point = context.deploy_cell(commitment_lock_bin); + let auth_out_point = context.deploy_cell(auth_bin); + + // prepare script + let (_sec_key_1, _sec_key_2, key_agg_ctx) = generate_multisig_keys(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + let x_only_pubkey = aggregated_pubkey.x_only_public_key().0.serialize(); + let pubkey_hash = blake2b_256(x_only_pubkey); + let delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(10, 1, 2), false); + let commitment_tx_version = 42u64; + + let mut generator = Generator::new(); + let remote_settlement_key = generator.gen_keypair(); + let remote_amount = (400 * BYTE_SHANNONS) as u128; + let local_settlement_key = generator.gen_keypair(); + let local_amount = (600 * BYTE_SHANNONS) as u128; + + let remote_htlc_key1 = generator.gen_keypair(); + let local_htlc_key1 = generator.gen_keypair(); + let preimage1 = [42u8; 32]; + let payment_amount1 = 5 * BYTE_SHANNONS as u128; + // timeout after 2024-04-01 01:00:00 + let expiry1 = Since::from_timestamp(1711976400, true).unwrap(); + + // v1 97-byte HTLC entry placed in the witness + let v1_htlc_entry = build_htlc_entry_v1( + 0b00000000, + payment_amount1, + &blake2b_256(preimage1), + &blake2b_256(remote_htlc_key1.1.serialize())[0..20], + &blake2b_256(local_htlc_key1.1.serialize())[0..20], + expiry1.as_u64(), + ); + // the legacy-summed (85-byte) version of the same HTLC committed in args + let legacy_htlc_entry = [ + [0b00000000].to_vec(), + payment_amount1.to_le_bytes().to_vec(), + blake2b_256(preimage1)[0..20].to_vec(), + blake2b_256(remote_htlc_key1.1.serialize())[0..20].to_vec(), + blake2b_256(local_htlc_key1.1.serialize())[0..20].to_vec(), + expiry1.as_u64().to_le_bytes().to_vec(), + ] + .concat(); + + let legacy_pending_htlcs = [[1].to_vec(), legacy_htlc_entry.clone()].concat(); + let two_party_settlement = [ + blake2b_256(remote_settlement_key.1.serialize())[0..20].to_vec(), + remote_amount.to_le_bytes().to_vec(), + blake2b_256(local_settlement_key.1.serialize())[0..20].to_vec(), + local_amount.to_le_bytes().to_vec(), + ] + .concat(); + let legacy_settlement_script = [legacy_pending_htlcs, two_party_settlement.clone()].concat(); + + let args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&legacy_settlement_script)[0..20], + &[0x00], + ] + .concat(); + + let lock_script = context + .build_script(&commitment_lock_out_point, args.clone().into()) + .expect("script"); + + // prepare cell deps + let commitment_lock_dep = CellDep::new_builder() + .out_point(commitment_lock_out_point) + .build(); + let auth_dep = CellDep::new_builder().out_point(auth_out_point).build(); + let cell_deps = vec![commitment_lock_dep, auth_dep].pack(); + + // prepare cells + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount1) as u64).pack()) + .lock(lock_script.clone()) + .build(), + Bytes::new(), + ); + let input = CellInput::new_builder() + .previous_output(input_out_point) + .since(delay_epoch.as_u64().pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount) as u64).pack()) + .lock(Script::new_builder().build()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps) + .inputs(vec![input]) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with remote_htlc_key1 + let message: [u8; 32] = compute_tx_message(&tx); + let signature = remote_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + [[1].to_vec(), v1_htlc_entry, two_party_settlement].concat(), + [0x00, 0x01].to_vec(), // unlock offered htlc1 with preimage + signature.clone(), + preimage1.to_vec(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("v1 witness entries with legacy args should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#15")); // WitnessHashError +} + +// A v1 partial settlement must not brick the derived commitment cell: the +// follow-up cell must keep 58-byte args with the features byte appended at +// [57], so a second settlement tx can still unlock the remaining HTLC through +// the full 32-byte-hash preimage path. +#[test] +fn v1_derived_cell_resettlement_succeeds() { + // deploy contract + let mut context = Context::default(); + let loader = Loader::default(); + let commitment_lock_bin = loader.load_binary("commitment-lock"); + let auth_bin = loader.load_binary("../../deps/auth"); + let commitment_lock_out_point = context.deploy_cell(commitment_lock_bin); + let auth_out_point = context.deploy_cell(auth_bin); + + // prepare script + let (_sec_key_1, _sec_key_2, key_agg_ctx) = generate_multisig_keys(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + let x_only_pubkey = aggregated_pubkey.x_only_public_key().0.serialize(); + let pubkey_hash = blake2b_256(x_only_pubkey); + let delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(10, 1, 2), false); // 10.5 epoch =~ 42 hours + let commitment_tx_version = 42u64; + + let mut generator = Generator::new(); + let remote_settlement_key = generator.gen_keypair(); + let remote_amount = (400 * BYTE_SHANNONS) as u128; + let local_settlement_key = generator.gen_keypair(); + let local_amount = (600 * BYTE_SHANNONS) as u128; + + let remote_htlc_key1 = generator.gen_keypair(); + let local_htlc_key2 = generator.gen_keypair(); + let preimage1 = [42u8; 32]; + let preimage2 = [24u8; 32]; + let payment_amount1 = 5 * BYTE_SHANNONS as u128; + let payment_amount2 = 8 * BYTE_SHANNONS as u128; + // timeout after 2024-04-01 01:00:00 + let expiry1 = Since::from_timestamp(1711976400, true).unwrap(); + // timeout after 2024-04-02 01:00:00 + let expiry2 = Since::from_timestamp(1712062800, true).unwrap(); + + // htlc1: offered, blake2b full hash; htlc2: received, sha256 full hash + let htlc1_entry = build_htlc_entry_v1( + 0b00000000, + payment_amount1, + &blake2b_256(preimage1), + &blake2b_256(remote_htlc_key1.1.serialize())[0..20], + &blake2b_256(local_htlc_key2.1.serialize())[0..20], + expiry1.as_u64(), + ); + let htlc2_entry = build_htlc_entry_v1( + 0b00000011, + payment_amount2, + &Sha256::digest(preimage2), + &blake2b_256(remote_htlc_key1.1.serialize())[0..20], + &blake2b_256(local_htlc_key2.1.serialize())[0..20], + expiry2.as_u64(), + ); + + let pending_htlcs = [[2].to_vec(), htlc1_entry.clone(), htlc2_entry.clone()].concat(); + + let two_party_settlement = [ + blake2b_256(remote_settlement_key.1.serialize())[0..20].to_vec(), + remote_amount.to_le_bytes().to_vec(), + blake2b_256(local_settlement_key.1.serialize())[0..20].to_vec(), + local_amount.to_le_bytes().to_vec(), + ] + .concat(); + + let settlement_script = [pending_htlcs.clone(), two_party_settlement.clone()].concat(); + + let mut args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&settlement_script)[0..20], + &[0x00], + ] + .concat(); + // v1: args gain one byte (57 -> 58), [57] = feature bitmap + args.push(0x01); + + let lock_script = context + .build_script(&commitment_lock_out_point, args.clone().into()) + .expect("script"); + + // prepare cell deps + let commitment_lock_dep = CellDep::new_builder() + .out_point(commitment_lock_out_point) + .build(); + let auth_dep = CellDep::new_builder().out_point(auth_out_point).build(); + let cell_deps = vec![commitment_lock_dep, auth_dep].pack(); + + // prepare cells + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity( + ((local_amount + remote_amount + payment_amount1 + payment_amount2) as u64).pack(), + ) + .lock(lock_script.clone()) + .build(), + Bytes::new(), + ); + + // first settlement tx: unlock offered htlc1 via preimage, leaving htlc2 + // pending in the derived commitment cell + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + + let new_pending_htlcs = [[1].to_vec(), htlc2_entry.clone()].concat(); + let new_settlement_script = [new_pending_htlcs.clone(), two_party_settlement.clone()].concat(); + let mut new_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&new_settlement_script)[0..20], + &[0x01], // subsequent commitment cell + ] + .concat(); + // the v1 features byte must be propagated into the derived cell's args + new_args.push(0x01); + assert_eq!(new_args.len(), 58); + assert_eq!(new_args[57], 0x01); + + let new_lock_script = lock_script + .clone() + .as_builder() + .args(new_args.pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount2) as u64).pack()) + .lock(new_lock_script.clone()) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with remote_htlc_key1 + let message: [u8; 32] = compute_tx_message(&tx); + let signature = remote_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock offered htlc1 with preimage + signature.clone(), + preimage1.to_vec(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("partial settlement should pass verification"); + println!("consume cycles: {}", cycles); + + // second settlement tx: unlock the remaining received htlc2 from the + // v1-derived commitment cell through the full-hash preimage path + let derived_input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity(((local_amount + remote_amount + payment_amount2) as u64).pack()) + .lock(new_lock_script.clone()) + .build(), + Bytes::new(), + ); + let input = CellInput::new_builder() + .previous_output(derived_input_out_point) + .since(delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + + let final_settlement_script = [[0].to_vec(), two_party_settlement.clone()].concat(); + let mut final_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&final_settlement_script)[0..20], + &[0x01], + ] + .concat(); + final_args.push(0x01); + let final_lock_script = lock_script + .clone() + .as_builder() + .args(final_args.pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity(((local_amount + remote_amount) as u64).pack()) + .lock(final_lock_script) + .build(), + ]; + let outputs_data = [Bytes::new()]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with local_htlc_key2 (received htlc preimage path uses the local key) + let message: [u8; 32] = compute_tx_message(&tx); + let signature = local_htlc_key2 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + new_settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock received htlc2 with preimage + signature.clone(), + preimage2.to_vec(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("resettlement of the v1-derived cell should pass verification"); + println!("consume cycles: {}", cycles); + + // wrong preimage must still be rejected on the v1-derived cell + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + new_settlement_script, + [0x00, 0x01].to_vec(), + signature, + preimage1.to_vec(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("wrong preimage should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#22")); // PreimageError +} + +// The "xUDT variant" of the v1 re-settlement flow: a v1 partial settlement of +// a UDT commitment cell must keep the features byte in the derived cell's +// args, so the remaining pending HTLC can be settled through the full-hash +// preimage path with the xUDT amount checks applied. +#[test] +fn v1_sudt_derived_cell_resettlement_succeeds() { + // deploy contract + let mut context = Context::default(); + let loader = Loader::default(); + let commitment_lock_bin = loader.load_binary("commitment-lock"); + let auth_bin = loader.load_binary("../../deps/auth"); + let simple_udt_bin = loader.load_binary("../../deps/simple_udt"); + let commitment_lock_out_point = context.deploy_cell(commitment_lock_bin); + let auth_out_point = context.deploy_cell(auth_bin); + let simple_udt_out_point = context.deploy_cell(simple_udt_bin); + + // prepare script + let (_sec_key_1, _sec_key_2, key_agg_ctx) = generate_multisig_keys(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + let x_only_pubkey = aggregated_pubkey.x_only_public_key().0.serialize(); + let pubkey_hash = blake2b_256(x_only_pubkey); + let delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(10, 1, 2), false); + let half_delay_epoch = Since::from_epoch(EpochNumberWithFraction::new(5, 0, 1), false); + let commitment_tx_version = 42u64; + + let mut generator = Generator::new(); + let remote_settlement_key = generator.gen_keypair(); + let remote_amount = 22222222222222222222u128; + let local_settlement_key = generator.gen_keypair(); + let local_amount = 11111111111111111111u128; + + let remote_htlc_key1 = generator.gen_keypair(); + let local_htlc_key2 = generator.gen_keypair(); + let preimage1 = [42u8; 32]; + let preimage2 = [24u8; 32]; + let payment_amount1 = 1234567890u128; + let payment_amount2 = 9876543210u128; + // timeout after 2024-04-01 01:00:00 + let expiry1 = Since::from_timestamp(1711976400, true).unwrap(); + // timeout after 2024-04-02 01:00:00 + let expiry2 = Since::from_timestamp(1712062800, true).unwrap(); + + // htlc1: offered, blake2b full hash; htlc2: received, sha256 full hash + let htlc1_entry = build_htlc_entry_v1( + 0b00000000, + payment_amount1, + &blake2b_256(preimage1), + &blake2b_256(remote_htlc_key1.1.serialize())[0..20], + &blake2b_256(local_htlc_key2.1.serialize())[0..20], + expiry1.as_u64(), + ); + let htlc2_entry = build_htlc_entry_v1( + 0b00000011, + payment_amount2, + &Sha256::digest(preimage2), + &blake2b_256(remote_htlc_key1.1.serialize())[0..20], + &blake2b_256(local_htlc_key2.1.serialize())[0..20], + expiry2.as_u64(), + ); + + let pending_htlcs = [[2].to_vec(), htlc1_entry, htlc2_entry.clone()].concat(); + + let two_party_settlement = [ + blake2b_256(remote_settlement_key.1.serialize())[0..20].to_vec(), + remote_amount.to_le_bytes().to_vec(), + blake2b_256(local_settlement_key.1.serialize())[0..20].to_vec(), + local_amount.to_le_bytes().to_vec(), + ] + .concat(); + + let settlement_script = [pending_htlcs.clone(), two_party_settlement.clone()].concat(); + + let mut args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&settlement_script)[0..20], + &[0x00], + ] + .concat(); + // v1: args gain one byte (57 -> 58), [57] = feature bitmap + args.push(0x01); + + let lock_script = context + .build_script(&commitment_lock_out_point, args.clone().into()) + .expect("script"); + let type_script = context + .build_script(&simple_udt_out_point, vec![42; 32].into()) + .expect("script"); + + // prepare cell deps + let commitment_lock_dep = CellDep::new_builder() + .out_point(commitment_lock_out_point) + .build(); + let auth_dep = CellDep::new_builder().out_point(auth_out_point).build(); + let simple_udt_dep = CellDep::new_builder() + .out_point(simple_udt_out_point) + .build(); + let cell_deps = vec![commitment_lock_dep, auth_dep, simple_udt_dep].pack(); + + // prepare cells + let total_sudt_amount = local_amount + remote_amount + payment_amount1 + payment_amount2; + let input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity((1000 * BYTE_SHANNONS).pack()) + .lock(lock_script.clone()) + .type_(Some(type_script.clone()).pack()) + .build(), + total_sudt_amount.to_le_bytes().to_vec().into(), + ); + + // first settlement tx: unlock offered htlc1 via preimage, leaving htlc2 + // pending in the derived commitment cell + let input = CellInput::new_builder() + .previous_output(input_out_point.clone()) + .since(half_delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + + let new_pending_htlcs = [[1].to_vec(), htlc2_entry.clone()].concat(); + let new_settlement_script = [new_pending_htlcs.clone(), two_party_settlement.clone()].concat(); + let mut new_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&new_settlement_script)[0..20], + &[0x01], // subsequent commitment cell + ] + .concat(); + // the v1 features byte must be propagated into the derived cell's args + new_args.push(0x01); + assert_eq!(new_args.len(), 58); + assert_eq!(new_args[57], 0x01); + + let new_lock_script = lock_script + .clone() + .as_builder() + .args(new_args.pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity((1000 * BYTE_SHANNONS).pack()) + .lock(new_lock_script.clone()) + .type_(Some(type_script.clone()).pack()) + .build(), + ]; + let outputs_data: Vec = vec![ + (total_sudt_amount - payment_amount1) + .to_le_bytes() + .to_vec() + .into(), + ]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs.clone()) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with remote_htlc_key1 + let message: [u8; 32] = compute_tx_message(&tx); + let signature = remote_htlc_key1 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock offered htlc1 with preimage + signature.clone(), + preimage1.to_vec(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("partial settlement of the v1 UDT cell should pass verification"); + println!("consume cycles: {}", cycles); + + // second settlement tx: unlock the remaining received htlc2 from the + // v1-derived UDT commitment cell through the full-hash preimage path + let derived_input_out_point = context.create_cell( + CellOutput::new_builder() + .capacity((1000 * BYTE_SHANNONS).pack()) + .lock(new_lock_script.clone()) + .type_(Some(type_script.clone()).pack()) + .build(), + (total_sudt_amount - payment_amount1) + .to_le_bytes() + .to_vec() + .into(), + ); + let input = CellInput::new_builder() + .previous_output(derived_input_out_point) + .since(delay_epoch.as_u64().pack()) + .build(); + let inputs = vec![input]; + + let final_settlement_script = [[0].to_vec(), two_party_settlement.clone()].concat(); + let mut final_args = [ + &pubkey_hash[0..20], + delay_epoch.as_u64().to_le_bytes().as_slice(), + commitment_tx_version.to_be_bytes().as_slice(), + &blake2b_256(&final_settlement_script)[0..20], + &[0x01], + ] + .concat(); + final_args.push(0x01); + let final_lock_script = lock_script + .clone() + .as_builder() + .args(final_args.pack()) + .build(); + let outputs = vec![ + CellOutput::new_builder() + .capacity((1000 * BYTE_SHANNONS).pack()) + .lock(final_lock_script) + .type_(Some(type_script.clone()).pack()) + .build(), + ]; + let outputs_data: Vec = vec![ + (total_sudt_amount - payment_amount1 - payment_amount2) + .to_le_bytes() + .to_vec() + .into(), + ]; + let tx = TransactionBuilder::default() + .cell_deps(cell_deps.clone()) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data.pack()) + .build(); + + // sign with local_htlc_key2 (received htlc preimage path uses the local key) + let message: [u8; 32] = compute_tx_message(&tx); + let signature = local_htlc_key2 + .0 + .sign_recoverable(&message.into()) + .unwrap() + .serialize(); + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + new_settlement_script.clone(), + [0x00, 0x01].to_vec(), // unlock received htlc2 with preimage + signature.clone(), + preimage2.to_vec(), + ] + .concat(); + + let success_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let cycles = context + .verify_tx(&success_tx, MAX_CYCLES) + .expect("resettlement of the v1-derived UDT cell should pass verification"); + println!("consume cycles: {}", cycles); + + // wrong preimage must still be rejected on the v1-derived UDT cell + let witness = [ + EMPTY_WITNESS_ARGS.to_vec(), + vec![0x01], + new_settlement_script, + [0x00, 0x01].to_vec(), + signature, + preimage1.to_vec(), + ] + .concat(); + + let fail_tx = tx.as_advanced_builder().witness(witness.pack()).build(); + let error = context + .verify_tx(&fail_tx, MAX_CYCLES) + .expect_err("wrong preimage should fail"); + println!("error: {}", error); + assert!(error.to_string().contains("#22")); // PreimageError +}