From de40c3083424a678e20791b6b507f2325872da03 Mon Sep 17 00:00:00 2001 From: xue Date: Mon, 20 Jul 2026 14:28:28 +0800 Subject: [PATCH] Add CI and lock script regressions --- .github/workflows/ci.yml | 6 ++ Makefile | 10 +- scripts/find_clang | 5 + tests/src/ci_workflow.rs | 46 +++++++++ tests/src/lib.rs | 2 + tests/src/tests.rs | 216 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 tests/src/ci_workflow.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d49a41d..e0a6535 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Makefile b/Makefile index 6cd1398..b2bdf1d 100644 --- a/Makefile +++ b/Makefile @@ -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" @@ -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 diff --git a/scripts/find_clang b/scripts/find_clang index 6994c9a..87b247c 100755 --- a/scripts/find_clang +++ b/scripts/find_clang @@ -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 diff --git a/tests/src/ci_workflow.rs b/tests/src/ci_workflow.rs new file mode 100644 index 0000000..743b2b8 --- /dev/null +++ b/tests/src/ci_workflow.rs @@ -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"); +} diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 53fb2ab..050c37e 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -11,6 +11,8 @@ use std::fs; use std::path::PathBuf; use std::str::FromStr; +#[cfg(test)] +mod ci_workflow; #[cfg(test)] mod tests; diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 4fba48e..605f9f0 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -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::>(); + + 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 @@ -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 @@ -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) @@ -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 @@ -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(),