Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ jobs:
version: "16"
- name: Prepare
run: make prepare
- name: Install Rust components
run: rustup component add rustfmt clippy
- name: Format
run: make fmt-check
- name: Clippy
run: make clippy CARGO_ARGS="--all-targets"
- name: Build
run: scripts/reproducible_build_docker
- name: Run tests
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ clippy:
fmt:
cargo fmt $(CARGO_ARGS)

fmt-check:
cargo fmt --check $(CARGO_ARGS)

pr-verify: fmt-check
$(MAKE) clippy CARGO_ARGS="--all-targets"
$(MAKE) build
$(MAKE) test

# Arbitrary cargo command is supported here. For example:
#
# make cargo CARGO_CMD=expand CARGO_ARGS="--ugly"
Expand Down Expand Up @@ -150,4 +158,4 @@ CHECKSUM_FILE := build/checksums-$(MODE).txt
checksum: build
shasum -a 256 build/$(MODE)/* > $(CHECKSUM_FILE)

.PHONY: build test check clippy fmt cargo clean prepare checksum
.PHONY: build test check clippy fmt fmt-check pr-verify cargo clean prepare checksum
5 changes: 5 additions & 0 deletions scripts/find_clang
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ if [[ -n "${BREW_PREFIX}" ]]; then
fi

for candidate in ${CANDIDATES[@]}; do
VERSION_TEXT=$($candidate --version 2> /dev/null)
if echo "${VERSION_TEXT}" | grep -qi "Apple clang"; then
continue
fi

OUTPUT=$($candidate -dumpversion 2> /dev/null | cut -d'.' -f 1)

if [[ $((OUTPUT)) -ge 16 ]]; then
Expand Down
46 changes: 46 additions & 0 deletions tests/src/ci_workflow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const CI_WORKFLOW: &str = include_str!("../../.github/workflows/ci.yml");
const FIND_CLANG: &str = include_str!("../../scripts/find_clang");
const MAKEFILE: &str = include_str!("../../Makefile");

fn assert_contains(haystack: &str, needle: &str) {
assert!(
haystack.contains(needle),
"expected project validation config to contain `{needle}`"
);
}

#[test]
fn ci_workflow_validates_pull_requests_to_main() {
assert_contains(CI_WORKFLOW, "pull_request:");
assert_contains(CI_WORKFLOW, "branches: [ \"main\" ]");
}

#[test]
fn ci_workflow_runs_pr_validation_layers() {
for required_step in [
"make prepare",
"make fmt-check",
"make clippy CARGO_ARGS=\"--all-targets\"",
"scripts/reproducible_build_docker",
"make test",
] {
assert_contains(CI_WORKFLOW, required_step);
}
}

#[test]
fn makefile_exposes_local_pr_validation_entrypoint() {
for required_target in [
"fmt-check:",
"pr-verify: fmt-check",
"$(MAKE) build",
"$(MAKE) test",
] {
assert_contains(MAKEFILE, required_target);
}
}

#[test]
fn clang_discovery_rejects_apple_clang_for_riscv_contract_builds() {
assert_contains(FIND_CLANG, "Apple clang");
}
2 changes: 2 additions & 0 deletions tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use std::fs;
use std::path::PathBuf;
use std::str::FromStr;

#[cfg(test)]
mod ci_workflow;
#[cfg(test)]
mod tests;

Expand Down
216 changes: 214 additions & 2 deletions tests/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,123 @@ fn compute_tx_message(tx: &TransactionView) -> [u8; 32] {
blake2b_256(tx.as_slice())
}

fn assert_script_error_code(
context: &Context,
tx: &TransactionView,
expected_error_code: i8,
case_name: &str,
) {
let error = context
.verify_tx(tx, MAX_CYCLES)
.expect_err(&format!("{case_name} should fail"));
let error_message = error.to_string();
assert!(
error_message.contains(&format!("#{expected_error_code}")),
"{case_name} failed with unexpected error: {error_message}"
);
}

struct FundingLockFixture {
context: Context,
cell_deps: CellDepVec,
lock_script: Script,
sec_key_1: SecretKey,
sec_key_2: SecretKey,
key_agg_ctx: KeyAggContext,
x_only_pubkey: [u8; 32],
}

fn setup_funding_lock() -> FundingLockFixture {
let mut context = Context::default();
let loader = Loader::default();
let funding_lock_bin = loader.load_binary("funding-lock");
let auth_bin = loader.load_binary("../../deps/auth");
let funding_lock_out_point = context.deploy_cell(funding_lock_bin);
let auth_out_point = context.deploy_cell(auth_bin);

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 lock_script = context
.build_script(&funding_lock_out_point, pubkey_hash[0..20].to_vec().into())
.expect("script");

let funding_lock_dep = CellDep::new_builder()
.out_point(funding_lock_out_point)
.build();
let auth_dep = CellDep::new_builder().out_point(auth_out_point).build();
let cell_deps = vec![funding_lock_dep, auth_dep].pack();

FundingLockFixture {
context,
cell_deps,
lock_script,
sec_key_1,
sec_key_2,
key_agg_ctx,
x_only_pubkey,
}
}

fn build_funding_tx(
context: &mut Context,
lock_script: &Script,
cell_deps: CellDepVec,
input_count: usize,
) -> TransactionView {
let inputs = (0..input_count)
.map(|_| {
let input_out_point = context.create_cell(
CellOutput::new_builder()
.capacity(1000u64.pack())
.lock(lock_script.clone())
.build(),
Bytes::new(),
);
CellInput::new_builder()
.previous_output(input_out_point)
.build()
})
.collect::<Vec<_>>();

let output_lock = Script::new_builder()
.args(Bytes::from("output_lock").pack())
.build();
let outputs = vec![
CellOutput::new_builder()
.capacity((1000u64 * input_count as u64).pack())
.lock(output_lock)
.build(),
];
let outputs_data = [Bytes::new()];

TransactionBuilder::default()
.cell_deps(cell_deps)
.inputs(inputs)
.outputs(outputs)
.outputs_data(outputs_data.pack())
.build()
}

fn sign_funding_tx(tx: TransactionView, fixture: &FundingLockFixture) -> TransactionView {
let message = compute_tx_message(&tx);
let signature = multisig(
fixture.sec_key_1,
fixture.sec_key_2,
fixture.key_agg_ctx.clone(),
message,
);
let witness = [
EMPTY_WITNESS_ARGS.to_vec(),
fixture.x_only_pubkey.to_vec(),
signature,
]
.concat();

tx.as_advanced_builder().witness(witness.pack()).build()
}

#[test]
fn test_funding_lock() {
// deploy contract
Expand Down Expand Up @@ -195,6 +312,77 @@ fn test_funding_lock() {
println!("consume cycles: {}", cycles);
}

#[test]
fn test_funding_lock_rejects_multiple_group_inputs() {
let mut fixture = setup_funding_lock();
let tx = build_funding_tx(
&mut fixture.context,
&fixture.lock_script,
fixture.cell_deps.clone(),
2,
);
let tx = sign_funding_tx(tx, &fixture);

assert_script_error_code(&fixture.context, &tx, 5, "funding lock multiple inputs");
}

#[test]
fn test_funding_lock_rejects_malformed_witness_prefix() {
let mut fixture = setup_funding_lock();
let tx = build_funding_tx(
&mut fixture.context,
&fixture.lock_script,
fixture.cell_deps.clone(),
1,
);
let message = compute_tx_message(&tx);
let signature = multisig(
fixture.sec_key_1,
fixture.sec_key_2,
fixture.key_agg_ctx.clone(),
message,
);
let mut empty_witness_args = EMPTY_WITNESS_ARGS.to_vec();
empty_witness_args[0] = 0;
let witness = [
empty_witness_args,
fixture.x_only_pubkey.to_vec(),
signature,
]
.concat();
let tx = tx.as_advanced_builder().witness(witness.pack()).build();

assert_script_error_code(
&fixture.context,
&tx,
7,
"funding lock malformed witness args",
);
}

#[test]
fn test_funding_lock_rejects_wrong_signature() {
let mut fixture = setup_funding_lock();
let tx = build_funding_tx(
&mut fixture.context,
&fixture.lock_script,
fixture.cell_deps.clone(),
1,
);
let (wrong_sec_key_1, wrong_sec_key_2, wrong_key_agg_ctx) = generate_multisig_keys();
let message = compute_tx_message(&tx);
let signature = multisig(wrong_sec_key_1, wrong_sec_key_2, wrong_key_agg_ctx, message);
let witness = [
EMPTY_WITNESS_ARGS.to_vec(),
fixture.x_only_pubkey.to_vec(),
signature,
]
.concat();
let tx = tx.as_advanced_builder().witness(witness.pack()).build();

assert_script_error_code(&fixture.context, &tx, 110, "funding lock wrong signature");
}

#[test]
fn test_commitment_lock_no_pending_htlcs() {
// deploy contract
Expand Down Expand Up @@ -277,7 +465,7 @@ fn test_commitment_lock_no_pending_htlcs() {
.previous_output(input_out_point.clone())
.build();

let tx = TransactionBuilder::default()
let unsigned_revocation_tx = TransactionBuilder::default()
.cell_deps(cell_deps.clone())
.input(input)
.outputs(outputs)
Expand Down Expand Up @@ -307,7 +495,11 @@ fn test_commitment_lock_no_pending_htlcs() {
]
.concat();

let tx = tx.as_advanced_builder().witness(witness.pack()).build();
let tx = unsigned_revocation_tx
.clone()
.as_advanced_builder()
.witness(witness.pack())
.build();
println!("tx: {:?}", tx);

// run
Expand All @@ -316,6 +508,26 @@ fn test_commitment_lock_no_pending_htlcs() {
.expect("pass verification");
println!("consume cycles: {}", cycles);

let stale_version = commitment_tx_version - 1;
let witness = [
EMPTY_WITNESS_ARGS.to_vec(),
vec![0x00],
stale_version.to_be_bytes().to_vec(),
x_only_pubkey.to_vec(),
vec![0u8; 64],
]
.concat();
let stale_revocation_tx = unsigned_revocation_tx
.as_advanced_builder()
.witness(witness.pack())
.build();
assert_script_error_code(
&context,
&stale_revocation_tx,
17,
"commitment lock stale revocation version",
);

// test with settlement unlock logic (local settlement key)
let new_settlement_script = [
[0].to_vec(),
Expand Down
Loading