diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e827e17..242f628a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +- Add the explicit `cellscript-witnessargs-input-type-v2` placement ABI for + parameterized CKB entries. Generated wrappers now resolve witnesses relative + to the active script group, decode the `CSARGv1` payload from + `WitnessArgs.input_type`, preserve wallet/multisig ownership of `lock`, reject + malformed or wrongly placed payloads, and retain group-relative raw-v1 + compatibility. A canonical multisig-v2 CKB-VM regression covers a type group + whose first input is not transaction input zero. + ## 0.22.0 - 2026-07-19 - Make GitHub publication depend on the full release gate. Release evidence now diff --git a/Cargo.lock b/Cargo.lock index 05d1fc80..575c31c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,6 +315,7 @@ dependencies = [ "blake2b_simd", "camino", "cellscript-ckb-adapter", + "ckb-sdk", "ckb-std", "ckb-testtool", "ckb-types", diff --git a/Cargo.toml b/Cargo.toml index 7a83054e..05690cd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,7 @@ ckb-acceptance = [] pretty_assertions = "1.4" tempfile = "3.10" ckb-testtool = "1.1" +ckb-sdk = { path = "../ckb-sdk-rust" } ckb-std = { version = "1.1.0", default-features = false, features = ["type-id"] } sha2 = "0.10" regex = "1" diff --git a/crates/cellscript-ckb-adapter/src/lib.rs b/crates/cellscript-ckb-adapter/src/lib.rs index 869b5c49..0f1cb215 100644 --- a/crates/cellscript-ckb-adapter/src/lib.rs +++ b/crates/cellscript-ckb-adapter/src/lib.rs @@ -343,11 +343,19 @@ pub struct ScriptCodeDepEvidence { pub dep_type: String, } +pub const ENTRY_WITNESS_PLACEMENT_ABI: &str = "cellscript-witnessargs-input-type-v2"; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -pub enum WitnessPlacement { - Lock, - InputType, - OutputType, +pub enum EntryWitnessPlacementAbi { + WitnessArgsInputTypeV2, +} + +impl EntryWitnessPlacementAbi { + pub const fn name(self) -> &'static str { + match self { + Self::WitnessArgsInputTypeV2 => ENTRY_WITNESS_PLACEMENT_ABI, + } + } } #[derive(Debug, Clone, Serialize)] @@ -1421,30 +1429,18 @@ pub fn require_script_code_dep(script: &Script, deps: &[ScriptCodeDep]) -> Resul Ok(dep.to_cell_dep()) } -pub fn place_entry_witness_payload(base: &WitnessArgs, placement: WitnessPlacement, payload: Bytes) -> Result { +pub fn place_entry_witness_payload(base: &WitnessArgs, placement: EntryWitnessPlacementAbi, payload: Bytes) -> Result { if payload.is_empty() { bail!("CellScript entry witness payload must be non-empty"); } match placement { - WitnessPlacement::Lock => { - if base.lock().to_opt().is_some() { - bail!("refusing to overwrite WitnessArgs.lock; lock signatures must stay explicit"); - } - Ok(base.clone().as_builder().lock(Some(payload).pack()).build()) - } - WitnessPlacement::InputType => { + EntryWitnessPlacementAbi::WitnessArgsInputTypeV2 => { if base.input_type().to_opt().is_some() { bail!("refusing to overwrite WitnessArgs.input_type"); } Ok(base.clone().as_builder().input_type(Some(payload).pack()).build()) } - WitnessPlacement::OutputType => { - if base.output_type().to_opt().is_some() { - bail!("refusing to overwrite WitnessArgs.output_type"); - } - Ok(base.clone().as_builder().output_type(Some(payload).pack()).build()) - } } } @@ -2355,13 +2351,16 @@ mod tests { fn places_cellscript_entry_payload_without_hiding_lock_signatures() { let base = WitnessArgs::new_builder().lock(Some(Bytes::from(vec![0x77u8; 65])).pack()).build(); let payload = Bytes::from(b"CSARGv1\0\x4d\0\0\0\0\0\0\0".to_vec()); - let witness = place_entry_witness_payload(&base, WitnessPlacement::InputType, payload.clone()).unwrap(); + let placement = EntryWitnessPlacementAbi::WitnessArgsInputTypeV2; + assert_eq!(placement.name(), "cellscript-witnessargs-input-type-v2"); + let witness = place_entry_witness_payload(&base, placement, payload.clone()).unwrap(); assert_eq!(witness.lock().to_opt().expect("lock preserved").raw_data().len(), 65); assert_eq!(witness.input_type().to_opt().expect("entry payload").raw_data(), payload); assert!(witness.output_type().to_opt().is_none()); - let error = place_entry_witness_payload(&base, WitnessPlacement::Lock, Bytes::from(vec![1u8])).unwrap_err().to_string(); - assert!(error.contains("lock signatures must stay explicit"), "{error}"); + let occupied = witness; + let error = place_entry_witness_payload(&occupied, placement, Bytes::from(vec![1u8])).unwrap_err().to_string(); + assert!(error.contains("refusing to overwrite WitnessArgs.input_type"), "{error}"); } #[test] diff --git a/docs/CELLSCRIPT_CKB_ADAPTER.md b/docs/CELLSCRIPT_CKB_ADAPTER.md index 0a4cfab1..6b37f395 100644 --- a/docs/CELLSCRIPT_CKB_ADAPTER.md +++ b/docs/CELLSCRIPT_CKB_ADAPTER.md @@ -90,8 +90,9 @@ RPC, and exposes signer, `estimate_cycles`, `test_tx_pool_accept`, and optional submission as adapter-owned node calls. It also builds headless deploy transactions that create TYPE_ID code cells from a `DeployArtifactSpec`, and generates `DeploymentManifest` records from the resulting evidence. It also -tests that CellScript entry witness bytes are placed into an explicit -`WitnessArgs` field without overwriting lock signatures, and that TYPE_ID +tests that CellScript entry witness bytes use the versioned +`cellscript-witnessargs-input-type-v2` contract and are placed into +`WitnessArgs.input_type` without overwriting lock signatures, and that TYPE_ID args are computed from the packed first input plus output index before adapter submission. diff --git a/docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md b/docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md index ea1ed079..22780cd5 100644 --- a/docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md +++ b/docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md @@ -4,15 +4,44 @@ tooling. CellScript action and lock entrypoints are normal RISC-V functions at the machine -level. Most public arguments come through the grouped input witness. Lock +level. Most public arguments come through the current script group's witness. Lock parameters declared as `lock_args T` instead come from the executing lock script's `Script.args` bytes. The compiler-generated `_cellscript_entry` wrapper loads the required source(s), validates the envelope or script-args layout, decodes positional arguments, and then tail-calls the selected action or lock. -## Envelope +## Placement ABI v2 -Every parameterized entry witness that has witness-backed arguments starts with: +The current CKB placement contract is +`cellscript-witnessargs-input-type-v2`: + +```text +WitnessArgs { + lock: wallet / lock-script signatures, + input_type: CellScript CSARGv1 entry payload, + output_type: protocol-specific output witness data, +} +``` + +The generated wrapper first loads `GroupInput#0`. If the active script group +has no input, it loads `GroupOutput#0`. It never substitutes transaction-global +`Input#0`, because the first member of one lock/type group may be any global +input index. The selected witness must be a canonical three-field Molecule +`WitnessArgs`; its `input_type` `BytesOpt` must contain the entry payload. + +This split lets canonical lock scripts, including multisig-v2, retain exclusive +ownership of `WitnessArgs.lock`. Builders must preserve an existing lock field +and fail rather than overwrite an existing `input_type` field. + +For compatibility with transactions built before placement v2, the same +group-relative source may still contain the raw v1 payload directly. Raw-v1 is +recognized only by the exact `CSARGv1\0` prefix. A malformed `WitnessArgs`, an +absent `input_type`, or a payload placed in `lock`/`output_type` fails closed +with runtime error `25 entry-witness-abi-invalid`; those forms are not aliases. + +## Payload Envelope v1 + +Every parameterized entry payload that has witness-backed arguments starts with: ```text 43 53 41 52 47 76 31 00 @@ -20,8 +49,8 @@ Every parameterized entry witness that has witness-backed arguments starts with: This is the ASCII magic `CSARGv1\0`. -Wrong magic, missing bytes, or unsupported parameter placement fails closed with -runtime error `25 entry-witness-abi-invalid`. +Wrong magic, missing bytes, malformed Molecule, or unsupported parameter +placement fails closed with runtime error `25 entry-witness-abi-invalid`. Entries whose parameters are entirely runtime-bound or `lock_args`-backed do not require a witness envelope. diff --git a/scripts/cellscript_0_14_scope_audit.sh b/scripts/cellscript_0_14_scope_audit.sh index 134a2017..55fb72c6 100755 --- a/scripts/cellscript_0_14_scope_audit.sh +++ b/scripts/cellscript_0_14_scope_audit.sh @@ -123,7 +123,11 @@ for path in paths: target_profile = metadata.get("target_profile", {}) require(target_profile.get("name") == "ckb", f"{path} did not compile under ckb profile") require(target_profile.get("source_encoding") == "ckb-source-group-high-bit", f"{path} missing CKB Source encoding") - require(target_profile.get("witness_abi") == "ckb-molecule-witness-args+cellscript-entry-witness-v1", f"{path} missing WitnessArgs ABI") + require( + target_profile.get("witness_abi") + == "ckb-molecule-witness-args-input-type-v2+cellscript-entry-witness-v1+raw-v1-compat", + f"{path} missing WitnessArgs ABI", + ) require(target_profile.get("spawn_ipc_abi") == "ckb-vm-v2-spawn-ipc-syscalls-2601-2608", f"{path} missing Spawn/IPC ABI") require(target_profile.get("output_data_abi") == "ckb-outputs-and-outputs-data-index-aligned", f"{path} missing outputs_data ABI") require(target_profile.get("type_id_abi") == "ckb-type-id-v1", f"{path} missing TYPE_ID ABI") diff --git a/src/cli/commands.rs b/src/cli/commands.rs index d5e7055c..53a209ae 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -7,7 +7,8 @@ use crate::{ compile_path, compile_path_metadata_with_diagnostics, compile_path_with_entry_action, compile_path_with_entry_lock, default_metadata_path_for_artifact, default_output_path_for_input, load_modules_for_input, resolve_input_path, validate_artifact_metadata, validate_source_units_on_disk, ArtifactFormat, CompileMetadata, CompileOptions, EntryWitnessArg, - ParamMetadata, ProofPlanMetadata, TargetProfile, ENTRY_WITNESS_ABI, + ParamMetadata, ProofPlanMetadata, TargetProfile, ENTRY_WITNESS_ABI, ENTRY_WITNESS_PLACEMENT_ABI, ENTRY_WITNESS_PLACEMENT_FIELD, + ENTRY_WITNESS_PLACEMENT_SOURCE, }; use base64::Engine; use camino::Utf8Path; @@ -1863,6 +1864,10 @@ impl CommandExecutor { let summary = serde_json::json!({ "status": if entry_constraints.unsupported { "fail" } else { "ok" }, "abi": ENTRY_WITNESS_ABI, + "placement_abi": ENTRY_WITNESS_PLACEMENT_ABI, + "witness_args_field": ENTRY_WITNESS_PLACEMENT_FIELD, + "witness_source": ENTRY_WITNESS_PLACEMENT_SOURCE, + "raw_v1_compatible": true, "target_profile": result.metadata.target_profile.name, "entry_kind": selected.kind, "entry": selected.name, @@ -2085,9 +2090,12 @@ impl CommandExecutor { }, "witness_args_policy": { "entry_payload_abi": ENTRY_WITNESS_ABI, + "placement_abi": ENTRY_WITNESS_PLACEMENT_ABI, "entry_payload_owner": "compiler", "final_witness_args_owner": "adapter", - "default_action_payload_field": "input_type", + "default_action_payload_field": ENTRY_WITNESS_PLACEMENT_FIELD, + "runtime_source": ENTRY_WITNESS_PLACEMENT_SOURCE, + "raw_v1_compatible": true, "lock_signature_policy": "explicit-adapter-owned-do-not-overwrite", "placement_requires_deployment_role": true, "ckb_reference": "ckb_types::packed::WitnessArgs", @@ -2842,9 +2850,12 @@ impl CommandExecutor { "must_emit_lineage": true, "witness_policy": { "entry_payload_abi": ENTRY_WITNESS_ABI, + "placement_abi": ENTRY_WITNESS_PLACEMENT_ABI, "entry_payload_owner": "compiler", "final_witness_args_owner": "adapter", - "default_action_payload_field": "input_type", + "default_action_payload_field": ENTRY_WITNESS_PLACEMENT_FIELD, + "runtime_source": ENTRY_WITNESS_PLACEMENT_SOURCE, + "raw_v1_compatible": true, "lock_signature_policy": "explicit-adapter-owned-do-not-overwrite", "placement_requires_deployment_role": true, }, @@ -3099,6 +3110,10 @@ impl CommandExecutor { machine: serde_json::json!({ "status": "ok", "abi": ENTRY_WITNESS_ABI, + "placement_abi": ENTRY_WITNESS_PLACEMENT_ABI, + "witness_args_field": ENTRY_WITNESS_PLACEMENT_FIELD, + "witness_source": ENTRY_WITNESS_PLACEMENT_SOURCE, + "raw_v1_compatible": true, "entry_kind": selected.kind, "entry": selected.name, "witness_hex": witness_hex, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 90d83840..175f0017 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -1239,7 +1239,6 @@ impl CodeGenerator { let has_dynamic_payload = payload.iter().any(|arg| arg.schema_dynamic); let min_witness_len = ENTRY_WITNESS_HEADER_SIZE + payload_len; let loaded_label = self.fresh_label("entry_witness_loaded"); - let try_group_input_label = self.fresh_label("entry_witness_try_group_input"); let try_group_output_label = self.fresh_label("entry_witness_try_group_output"); let buffer_ok_label = self.fresh_label("entry_witness_buffer_ok"); let size_ok_label = self.fresh_label("entry_witness_size_ok"); @@ -1249,10 +1248,10 @@ impl CodeGenerator { self.emit_global(ENTRY_WITNESS_LABEL); self.emit_label(ENTRY_WITNESS_LABEL); self.emit(format!( - "# cellscript entry abi: {} loads Input#0 witness args for {} and falls back to GroupInput#0/GroupOutput#0", + "# cellscript entry abi: {} loads GroupInput#0 witness args for {} and falls back to GroupOutput#0", ENTRY_WITNESS_LABEL, target )); - self.emit("# cellscript entry abi: witness magic CSARGv1 followed by positional fixed/scalar payload"); + self.emit("# cellscript entry abi: v2 reads CSARGv1 from WitnessArgs.input_type; raw CSARGv1 remains compatible"); self.emit_large_addi("sp", "sp", -(ENTRY_WITNESS_FRAME_SIZE as i64)); self.emit_stack_store("ra", ENTRY_WITNESS_RA_OFFSET); if has_lock_args { @@ -1261,18 +1260,7 @@ impl CodeGenerator { if has_witness_payload { self.emit_load_witness_syscall_to_offsets( "entry_args", - CKB_SOURCE_INPUT, - 0, - ENTRY_WITNESS_SIZE_OFFSET, - ENTRY_WITNESS_BUFFER_OFFSET, - ENTRY_WITNESS_BUFFER_SIZE, - ); - self.emit(format!("beqz a0, {}", loaded_label)); - self.emit(format!("j {}", try_group_input_label)); - self.emit_label(&try_group_input_label); - self.emit_load_witness_syscall_to_offsets( - "entry_args_fallback_group_input", - self.runtime_abi().source_group_input, + CKB_SOURCE_GROUP_INPUT, 0, ENTRY_WITNESS_SIZE_OFFSET, ENTRY_WITNESS_BUFFER_OFFSET, @@ -1300,6 +1288,10 @@ impl CodeGenerator { self.emit(format!("bnez t2, {}", buffer_ok_label)); self.emit(format!("j {}", fail_label)); self.emit_label(&buffer_ok_label); + + self.emit_entry_normalize_witness_args_input_type_v2(&fail_label); + + self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); self.emit(format!("li t1, {}", min_witness_len)); self.emit("sltu t2, t0, t1"); self.emit(format!("beqz t2, {}", size_ok_label)); @@ -1568,6 +1560,109 @@ impl CodeGenerator { Ok(()) } + /// Normalize the versioned entry placement ABI into the legacy raw-v1 + /// buffer shape consumed by the positional decoder. + /// + /// V2 loads a canonical CKB `WitnessArgs` from the current script group and + /// copies the `input_type` Bytes payload to the start of the local buffer. + /// A buffer already beginning with `CSARGv1\0` is left unchanged so + /// pre-v2 raw-v1 transactions remain valid. + fn emit_entry_normalize_witness_args_input_type_v2(&mut self, fail_label: &str) { + let witness_args_label = self.fresh_label("entry_witness_v2_witness_args"); + let normalized_label = self.fresh_label("entry_witness_v2_normalized"); + let validate_loop_label = self.fresh_label("entry_witness_v2_validate_loop"); + let field_end_ready_label = self.fresh_label("entry_witness_v2_field_end_ready"); + let field_done_label = self.fresh_label("entry_witness_v2_field_done"); + let copy_loop_label = self.fresh_label("entry_witness_v2_copy_loop"); + let copy_done_label = self.fresh_label("entry_witness_v2_copy_done"); + + self.emit("# cellscript entry placement v2: detect raw-v1 before parsing WitnessArgs.input_type"); + self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); + self.emit(format!("li t1, {}", ENTRY_WITNESS_HEADER_SIZE)); + self.emit(format!("bltu t0, t1, {}", witness_args_label)); + self.emit_stack_load("t0", ENTRY_WITNESS_BUFFER_OFFSET); + self.emit(format!("li t1, {}", u64::from_le_bytes(*ENTRY_WITNESS_MAGIC))); + self.emit(format!("bne t0, t1, {}", witness_args_label)); + self.emit(format!("j {}", normalized_label)); + + self.emit_label(&witness_args_label); + self.emit("# cellscript entry placement v2: validate the exact three-field WitnessArgs table"); + self.emit_stack_load("t0", ENTRY_WITNESS_SIZE_OFFSET); + self.emit("li t1, 16"); + self.emit(format!("bltu t0, t1, {}", fail_label)); + self.emit_sp_addi("t3", ENTRY_WITNESS_BUFFER_OFFSET); + + // The table header and local buffer are eight-byte aligned, so load its + // four u32 words in two pairs. Keep variable-offset Bytes lengths below + // on byte loads because Molecule payload offsets need not be aligned. + self.emit("ld a4, 0(t3)"); + self.emit("slli t1, a4, 32"); + self.emit("srli t1, t1, 32"); + self.emit(format!("bne t1, t0, {}", fail_label)); + self.emit("srli t4, a4, 32"); + self.emit("li t1, 16"); + self.emit(format!("bne t4, t1, {}", fail_label)); + self.emit("ld a4, 8(t3)"); + self.emit("slli t5, a4, 32"); + self.emit("srli t5, t5, 32"); + self.emit(format!("bltu t5, t4, {}", fail_label)); + self.emit("srli t6, a4, 32"); + self.emit(format!("bltu t6, t5, {}", fail_label)); + self.emit(format!("bltu t0, t6, {}", fail_label)); + + // Validate lock, input_type, and output_type through one compact loop. + // a5 is the field index and t4 the current start. The three ends are + // the preserved input_type offset, output_type offset, and total_size. + self.emit("li t4, 16"); + self.emit("li a5, 0"); + self.emit_label(&validate_loop_label); + self.emit("addi a6, t5, 0"); + self.emit(format!("beqz a5, {}", field_end_ready_label)); + self.emit("addi a6, t6, 0"); + self.emit("li a0, 1"); + self.emit(format!("beq a5, a0, {}", field_end_ready_label)); + self.emit("addi a6, t0, 0"); + self.emit_label(&field_end_ready_label); + self.emit("sub a1, a6, t4"); + self.emit(format!("beqz a1, {}", field_done_label)); + self.emit("li a0, 4"); + self.emit(format!("bltu a1, a0, {}", fail_label)); + self.emit("add a2, t3, t4"); + self.emit_u32_le_from_base_to("t1", "a2", 0, "t2"); + self.emit("addi a1, a1, -4"); + self.emit(format!("bne t1, a1, {}", fail_label)); + self.emit_label(&field_done_label); + self.emit("addi t4, a6, 0"); + self.emit("addi a5, a5, 1"); + self.emit("li a0, 3"); + self.emit(format!("bltu a5, a0, {}", validate_loop_label)); + + // input_type is mandatory for v2, while lock and output_type remain + // optional. t5 and t6 still hold its start and end offsets. + self.emit("sub t1, t6, t5"); + self.emit(format!("beqz t1, {}", fail_label)); + self.emit("addi t1, t1, -4"); + self.emit("add t4, t3, t5"); + + self.emit("# cellscript entry placement v2: copy input_type payload over the table envelope"); + self.emit("addi t4, t4, 4"); + self.emit_sp_addi("t5", ENTRY_WITNESS_BUFFER_OFFSET); + self.emit("li t2, 0"); + self.emit_label(©_loop_label); + self.emit("sltu t6, t2, t1"); + self.emit(format!("beqz t6, {}", copy_done_label)); + self.emit("add t3, t4, t2"); + self.emit("lbu t6, 0(t3)"); + self.emit("add t3, t5, t2"); + self.emit("sb t6, 0(t3)"); + self.emit("addi t2, t2, 1"); + self.emit(format!("j {}", copy_loop_label)); + self.emit_label(©_done_label); + self.emit_stack_store("t1", ENTRY_WITNESS_SIZE_OFFSET); + + self.emit_label(&normalized_label); + } + fn emit_entry_call_target(&mut self, target: &str, outgoing_stack_arg_bytes: usize) { if outgoing_stack_arg_bytes > 0 { self.emit(format!("# cellscript entry abi: reserve {} bytes for outgoing stack call arguments", outgoing_stack_arg_bytes)); diff --git a/src/lib.rs b/src/lib.rs index 6692991a..23b898c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,6 +215,12 @@ pub const MAX_SOURCE_BYTES: usize = 1024 * 1024; const STACK_COLLECTION_BACKING_BYTES: usize = 256; pub const ENTRY_WITNESS_ABI: &str = "cellscript-entry-witness-v1"; pub(crate) const ENTRY_WITNESS_ABI_MAGIC: &[u8; 8] = b"CSARGv1\0"; +/// Versioned CKB placement contract for parameterized entry payloads. +pub const ENTRY_WITNESS_PLACEMENT_ABI: &str = "cellscript-witnessargs-input-type-v2"; +/// Canonical `WitnessArgs` field owned by the CellScript entry placement ABI. +pub const ENTRY_WITNESS_PLACEMENT_FIELD: &str = "input_type"; +/// Script-group-relative witness lookup order used by generated CKB entries. +pub const ENTRY_WITNESS_PLACEMENT_SOURCE: &str = "group-input-0-then-group-output-0"; pub const CKB_DEFAULT_HASH_PERSONALIZATION: &[u8; 16] = b"ckb-default-hash"; pub const CKB_BLANK_HASH: [u8; 32] = [ 68, 244, 198, 151, 68, 213, 248, 197, 93, 100, 32, 98, 148, 157, 202, 228, 155, 196, 231, 239, 67, 211, 136, 197, 161, 47, 66, @@ -284,7 +290,7 @@ impl TargetProfile { }, header_abi: "ckb-header".to_string(), scheduler_abi: "none".to_string(), - witness_abi: "ckb-molecule-witness-args+cellscript-entry-witness-v1".to_string(), + witness_abi: "ckb-molecule-witness-args-input-type-v2+cellscript-entry-witness-v1+raw-v1-compat".to_string(), lock_args_abi: "ckb-script-args-typed-fixed-bytes".to_string(), source_encoding: "ckb-source-group-high-bit".to_string(), spawn_ipc_abi: "ckb-vm-v2-spawn-ipc-syscalls-2601-2608".to_string(), @@ -31168,14 +31174,23 @@ action spend(amount: u64) -> u64 { assert!(asm.contains(".global _cellscript_entry"), "parameterized entrypoints need a generated ELF entry wrapper:\n{}", asm); assert!( - asm.contains("# cellscript entry abi: _cellscript_entry loads Input#0 witness args for spend and falls back to GroupInput#0/GroupOutput#0"), + asm.contains( + "# cellscript entry abi: _cellscript_entry loads GroupInput#0 witness args for spend and falls back to GroupOutput#0" + ), "entry wrapper did not document its target ABI:\n{}", asm ); assert!( - asm.contains("# cellscript abi: LOAD_WITNESS reason=entry_args source=Input index=0") - && asm.contains("# cellscript abi: LOAD_WITNESS reason=entry_args_fallback_group_input source=GroupInput index=0"), - "entry wrapper did not load positional arguments from Input witness with GroupInput fallback:\n{}", + asm.contains("# cellscript abi: LOAD_WITNESS reason=entry_args source=GroupInput index=0") + && asm.contains("# cellscript abi: LOAD_WITNESS reason=entry_args_fallback_group_output source=GroupOutput index=0") + && !asm.contains("LOAD_WITNESS reason=entry_args source=Input index=0"), + "entry wrapper did not use script-group-relative witness sourcing:\n{}", + asm + ); + assert!( + asm.contains("# cellscript entry placement v2: detect raw-v1 before parsing WitnessArgs.input_type") + && asm.contains("# cellscript entry placement v2: copy input_type payload over the table envelope"), + "entry wrapper did not expose the versioned WitnessArgs.input_type placement ABI:\n{}", asm ); assert!( diff --git a/tests/backend_shape_baseline.json b/tests/backend_shape_baseline.json index 05025800..e3e83627 100644 --- a/tests/backend_shape_baseline.json +++ b/tests/backend_shape_baseline.json @@ -1,110 +1,110 @@ [ { "example": "amm_pool.cell", - "line_count": 19915, - "text_size": 83512, + "line_count": 20136, + "text_size": 84380, "relaxed_branch_count": 1, "max_cond_branch_abs_distance": 4680, - "machine_block_count": 2325, + "machine_block_count": 2361, "max_machine_block_size": 352, - "machine_cfg_edge_count": 4400, + "machine_cfg_edge_count": 4462, "machine_call_edge_count": 994, "unreachable_machine_block_count": 2054 }, { "example": "atomic_swap.cell", - "line_count": 11283, - "text_size": 46992, + "line_count": 11504, + "text_size": 47860, "relaxed_branch_count": 2, "max_cond_branch_abs_distance": 5628, - "machine_block_count": 989, + "machine_block_count": 1025, "max_machine_block_size": 20252, - "machine_cfg_edge_count": 1844, + "machine_cfg_edge_count": 1906, "machine_call_edge_count": 421, "unreachable_machine_block_count": 866 }, { "example": "launch.cell", - "line_count": 6948, - "text_size": 28836, + "line_count": 7169, + "text_size": 29704, "relaxed_branch_count": 2, "max_cond_branch_abs_distance": 5492, - "machine_block_count": 576, + "machine_block_count": 612, "max_machine_block_size": 1924, - "machine_cfg_edge_count": 997, + "machine_cfg_edge_count": 1059, "machine_call_edge_count": 179, "unreachable_machine_block_count": 144 }, { "example": "multi_phase_dao.cell", - "line_count": 12260, - "text_size": 49624, + "line_count": 12481, + "text_size": 50492, "relaxed_branch_count": 2, "max_cond_branch_abs_distance": 5140, - "machine_block_count": 1732, + "machine_block_count": 1768, "max_machine_block_size": 252, - "machine_cfg_edge_count": 3217, + "machine_cfg_edge_count": 3279, "machine_call_edge_count": 712, "unreachable_machine_block_count": 1663 }, { "example": "multisig.cell", - "line_count": 23738, - "text_size": 93408, + "line_count": 23959, + "text_size": 94276, "relaxed_branch_count": 4, "max_cond_branch_abs_distance": 7608, - "machine_block_count": 3499, + "machine_block_count": 3535, "max_machine_block_size": 300, - "machine_cfg_edge_count": 5540, + "machine_cfg_edge_count": 5602, "machine_call_edge_count": 358, "unreachable_machine_block_count": 3324 }, { "example": "nft.cell", - "line_count": 19681, - "text_size": 79944, + "line_count": 19902, + "text_size": 80812, "relaxed_branch_count": 1, "max_cond_branch_abs_distance": 11188, - "machine_block_count": 2925, + "machine_block_count": 2961, "max_machine_block_size": 376, - "machine_cfg_edge_count": 5168, + "machine_cfg_edge_count": 5230, "machine_call_edge_count": 850, "unreachable_machine_block_count": 2764 }, { "example": "timelock.cell", - "line_count": 18764, - "text_size": 75456, + "line_count": 18985, + "text_size": 76324, "relaxed_branch_count": 1, "max_cond_branch_abs_distance": 4404, - "machine_block_count": 2135, + "machine_block_count": 2171, "max_machine_block_size": 20252, - "machine_cfg_edge_count": 3744, + "machine_cfg_edge_count": 3806, "machine_call_edge_count": 578, "unreachable_machine_block_count": 2060 }, { "example": "token.cell", - "line_count": 2956, - "text_size": 11764, + "line_count": 3177, + "text_size": 12632, "relaxed_branch_count": 0, "max_cond_branch_abs_distance": 1260, - "machine_block_count": 414, + "machine_block_count": 450, "max_machine_block_size": 212, - "machine_cfg_edge_count": 723, + "machine_cfg_edge_count": 785, "machine_call_edge_count": 123, "unreachable_machine_block_count": 226 }, { "example": "vesting.cell", - "line_count": 8995, - "text_size": 36048, + "line_count": 9223, + "text_size": 36948, "relaxed_branch_count": 2, - "max_cond_branch_abs_distance": 7184, - "machine_block_count": 1077, + "max_cond_branch_abs_distance": 7216, + "machine_block_count": 1115, "max_machine_block_size": 356, - "machine_cfg_edge_count": 1965, + "machine_cfg_edge_count": 2031, "machine_call_edge_count": 412, - "unreachable_machine_block_count": 1000 + "unreachable_machine_block_count": 1002 } ] diff --git a/tests/cli.rs b/tests/cli.rs index c26df4f4..30c59b97 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -2196,7 +2196,10 @@ action main(value: u64) -> u64 { assert_eq!(dep["tx_hash"], "0x1111111111111111111111111111111111111111111111111111111111111111"); assert_eq!(dep["index"], 0); assert_eq!(dep["hash_type"], "type"); - assert_eq!(ckb["profile_abi_contract"]["witness_abi"], "ckb-molecule-witness-args+cellscript-entry-witness-v1"); + assert_eq!( + ckb["profile_abi_contract"]["witness_abi"], + "ckb-molecule-witness-args-input-type-v2+cellscript-entry-witness-v1+raw-v1-compat" + ); assert_eq!(ckb["profile_abi_contract"]["lock_args_abi"], "ckb-script-args-typed-fixed-bytes"); assert_eq!(ckb["profile_abi_contract"]["source_encoding"], "ckb-source-group-high-bit"); assert_eq!(ckb["profile_abi_contract"]["cell_dep_abi"], "ckb-cell-dep-outpoint-and-dep-group"); @@ -6597,7 +6600,7 @@ fn cellc_explain_profile_reports_ckb_v0_14_contract() { let summary: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(summary["profile"], "ckb"); - assert_eq!(summary["witness_abi"], "ckb-molecule-witness-args+cellscript-entry-witness-v1"); + assert_eq!(summary["witness_abi"], "ckb-molecule-witness-args-input-type-v2+cellscript-entry-witness-v1+raw-v1-compat"); assert_eq!(summary["lock_args_abi"], "ckb-script-args-typed-fixed-bytes"); assert_eq!(summary["source_encoding"], "ckb-source-group-high-bit"); assert_eq!(summary["spawn_ipc_abi"], "ckb-vm-v2-spawn-ipc-syscalls-2601-2608"); @@ -7437,7 +7440,10 @@ action mint(amount: u64) -> Token { assert_eq!(plan["adapter_contract"]["accepted_output_state"], "AcceptedActionTx"); assert_eq!(plan["adapter_contract"]["must_not_infer_protocol_semantics_from_action_name"], true); assert_eq!(plan["adapter_contract"]["witness_policy"]["entry_payload_abi"], "cellscript-entry-witness-v1"); + assert_eq!(plan["adapter_contract"]["witness_policy"]["placement_abi"], "cellscript-witnessargs-input-type-v2"); assert_eq!(plan["adapter_contract"]["witness_policy"]["default_action_payload_field"], "input_type"); + assert_eq!(plan["adapter_contract"]["witness_policy"]["runtime_source"], "group-input-0-then-group-output-0"); + assert_eq!(plan["adapter_contract"]["witness_policy"]["raw_v1_compatible"], true); assert_eq!(plan["adapter_contract"]["witness_policy"]["lock_signature_policy"], "explicit-adapter-owned-do-not-overwrite"); assert!(plan["adapter_contract"]["resolved_tx_required_fields"] .as_array() @@ -8823,6 +8829,10 @@ action main(amount: u64) -> u64 { let stdout: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(stdout["status"], "ok"); assert_eq!(stdout["abi"], "cellscript-entry-witness-v1"); + assert_eq!(stdout["placement_abi"], "cellscript-witnessargs-input-type-v2"); + assert_eq!(stdout["witness_args_field"], "input_type"); + assert_eq!(stdout["witness_source"], "group-input-0-then-group-output-0"); + assert_eq!(stdout["raw_v1_compatible"], true); assert_eq!(stdout["entry_kind"], "action"); assert_eq!(stdout["entry"], "main"); assert_eq!(stdout["witness_hex"], "43534152477631004d00000000000000"); @@ -9098,6 +9108,10 @@ fn cellc_ckb_std_compat_reports_runtime_boundary() { assert_eq!(report["ckb_std_refs"]["type_id"], "ckb_std::type_id"); assert_eq!(report["inline_abi"]["fields"]["cell_occupied_capacity"], 6); assert_eq!(report["witness_args_policy"]["entry_payload_abi"], "cellscript-entry-witness-v1"); + assert_eq!(report["witness_args_policy"]["placement_abi"], "cellscript-witnessargs-input-type-v2"); + assert_eq!(report["witness_args_policy"]["default_action_payload_field"], "input_type"); + assert_eq!(report["witness_args_policy"]["runtime_source"], "group-input-0-then-group-output-0"); + assert_eq!(report["witness_args_policy"]["raw_v1_compatible"], true); assert_eq!(report["witness_args_policy"]["final_witness_args_owner"], "adapter"); assert_eq!(report["witness_args_policy"]["lock_signature_policy"], "explicit-adapter-owned-do-not-overwrite"); assert_eq!(report["adapter_boundary"]["transaction_realizer"], "ckb-sdk-rust-or-CCC-adapter"); diff --git a/tests/entry_witness_abi.rs b/tests/entry_witness_abi.rs new file mode 100644 index 00000000..a4b40fc8 --- /dev/null +++ b/tests/entry_witness_abi.rs @@ -0,0 +1,125 @@ +#![allow(dead_code)] + +use ckb_sdk::{constants::MultisigScript, unlock::MultisigConfig}; +use ckb_testtool::ckb_types::{ + bytes::Bytes, + packed, + prelude::{Builder, Entity, Pack}, + H160, +}; + +#[path = "support/ckb_script_runner.rs"] +mod ckb_script_runner; + +use ckb_script_runner::{build_simple_fixture, compile_cellscript_source_to_elf, execute_cellscript_script}; + +const PARAMETERIZED_ENTRY: &str = r#" +module entry_witness_abi + +action verify(witness expected: u64) -> u64 { + verification + require expected == 42 + return 0 +} +"#; + +fn canonical_multisig_v2_witness(entry_payload: Bytes) -> packed::WitnessArgs { + let signer_a = H160::from_slice(&[0x11; 20]).expect("20-byte signer hash"); + let signer_b = H160::from_slice(&[0x22; 20]).expect("20-byte signer hash"); + let config = + MultisigConfig::new_with(MultisigScript::V2, vec![signer_a, signer_b], 0, 2).expect("canonical 2-of-2 multisig-v2 config"); + + config.placeholder_witness().as_builder().input_type(Some(entry_payload).pack()).build() +} + +fn raw_entry_payload(value: u64) -> Bytes { + let mut payload = b"CSARGv1\0".to_vec(); + payload.extend_from_slice(&value.to_le_bytes()); + Bytes::from(payload) +} + +fn execute_on_second_group_input(witness: Bytes) -> ckb_script_runner::CkbScriptExecutionResult { + let elf = compile_cellscript_source_to_elf(PARAMETERIZED_ENTRY, "verify", None); + let mut fixture = build_simple_fixture(Bytes::default(), 2, 1, true, None); + fixture.current_type_script_input_indices = vec![1]; + fixture.witnesses = vec![Bytes::from_static(b"unrelated-global-input-zero"), witness]; + execute_cellscript_script(&elf, &fixture) +} + +fn execute_on_output_only_group(witness: Bytes) -> ckb_script_runner::CkbScriptExecutionResult { + let elf = compile_cellscript_source_to_elf(PARAMETERIZED_ENTRY, "verify", None); + let mut fixture = build_simple_fixture(Bytes::default(), 1, 1, true, None); + fixture.current_type_script_input_indices.clear(); + fixture.witnesses = vec![witness]; + execute_cellscript_script(&elf, &fixture) +} + +#[test] +fn canonical_multisig_v2_lock_and_input_type_entry_payload_execute_in_ckb_vm() { + let witness = canonical_multisig_v2_witness(raw_entry_payload(42)); + let lock = witness.lock().to_opt().expect("multisig lock field").raw_data(); + assert_eq!(&lock[..4], &[0, 0, 2, 2], "canonical 2-of-2 multisig header"); + assert_eq!(lock.len(), 4 + 2 * 20 + 2 * 65, "multisig config plus two signature slots"); + + // Input 0 is outside the type group. A global-input lookup would read the + // unrelated witness instead of the group input at transaction index 1. + let result = execute_on_second_group_input(witness.as_bytes()); + assert_eq!( + result.exit_code, 0, + "CellScript must read GroupInput#0 and decode CSARGv1 from WitnessArgs.input_type while preserving multisig-v2 lock: {:?}", + result.captured_debug + ); +} + +#[test] +fn raw_v1_group_input_payload_remains_compatible() { + let result = execute_on_second_group_input(raw_entry_payload(42)); + assert_eq!(result.exit_code, 0, "raw-v1 compatibility failed: {:?}", result.captured_debug); +} + +#[test] +fn witnessargs_input_type_falls_back_to_group_output_zero() { + let witness = canonical_multisig_v2_witness(raw_entry_payload(42)); + let result = execute_on_output_only_group(witness.as_bytes()); + assert_eq!(result.exit_code, 0, "an output-only type group must resolve GroupOutput#0: {:?}", result.captured_debug); +} + +#[test] +fn witnessargs_output_type_is_not_an_entry_payload_alias() { + let witness = canonical_multisig_v2_witness(Bytes::from_static(b"not-the-entry-payload")) + .as_builder() + .input_type(None::.pack()) + .output_type(Some(raw_entry_payload(42)).pack()) + .build(); + let result = execute_on_second_group_input(witness.as_bytes()); + assert_eq!(result.exit_code, 25, "wrong WitnessArgs field must fail closed: {:?}", result.captured_debug); +} + +#[test] +fn malformed_witnessargs_input_type_length_fails_closed() { + let witness = canonical_multisig_v2_witness(raw_entry_payload(42)); + let mut encoded = witness.as_slice().to_vec(); + let input_type_offset = u32::from_le_bytes(encoded[8..12].try_into().expect("input_type table offset")) as usize; + let declared_len = + u32::from_le_bytes(encoded[input_type_offset..input_type_offset + 4].try_into().expect("input_type Bytes length")); + encoded[input_type_offset..input_type_offset + 4].copy_from_slice(&(declared_len + 1).to_le_bytes()); + + let result = execute_on_second_group_input(Bytes::from(encoded)); + assert_eq!(result.exit_code, 25, "malformed Molecule must fail closed: {:?}", result.captured_debug); +} + +#[test] +fn malformed_unselected_witnessargs_field_still_fails_closed() { + let witness = canonical_multisig_v2_witness(raw_entry_payload(42)) + .as_builder() + .output_type(Some(Bytes::from_static(b"protocol-output-data")).pack()) + .build(); + let mut encoded = witness.as_slice().to_vec(); + let output_type_offset = u32::from_le_bytes(encoded[12..16].try_into().expect("output_type table offset")) as usize; + let declared_len = + u32::from_le_bytes(encoded[output_type_offset..output_type_offset + 4].try_into().expect("output_type Bytes length")); + encoded[output_type_offset..output_type_offset + 4].copy_from_slice(&(declared_len + 1).to_le_bytes()); + + let result = execute_on_second_group_input(Bytes::from(encoded)); + assert_eq!(result.exit_code, 25, "the placement ABI must validate the whole WitnessArgs table: {:?}", result.captured_debug); +} diff --git a/tests/examples.rs b/tests/examples.rs index 99bb0441..bd7a0b2c 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -173,7 +173,9 @@ const BUNDLED_EXAMPLE_ASM_SHAPE_BUDGETS: [(&str, AssemblyShapeBudget); 9] = [ max_lines: 24_500, max_fail_handlers: 64, max_shared_epilogues: 20, - max_text_bytes: 92 * 1024, + // The v2 placement parser adds 68 bytes to the full multisig text + // surface while the focused transfer entry remains below 7 KiB. + max_text_bytes: 93 * 1024, max_relaxed_branches: 4, max_cond_branch_abs_distance: 7_700, max_machine_blocks: 3_600, diff --git a/tests/support/ckb_script_runner.rs b/tests/support/ckb_script_runner.rs index 3a8282af..6b702cf6 100644 --- a/tests/support/ckb_script_runner.rs +++ b/tests/support/ckb_script_runner.rs @@ -290,6 +290,11 @@ pub struct CkbVmFixture { pub script_args: Bytes, /// Input cells. pub inputs: Vec, + /// Input indexes that carry the CellScript type script under test. + /// + /// This is separate from `FixtureCell::type_script` so tests can express a + /// real type-script group whose first member is not transaction input 0. + pub current_type_script_input_indices: Vec, /// Output cells. pub outputs: Vec, /// Additional cell deps (beyond the script code cell itself). @@ -388,11 +393,17 @@ pub fn execute_cellscript_script(elf_bytes: &[u8], fixture: &CkbVmFixture) -> Ck let input_out_points: Vec = fixture .inputs .iter() - .map(|cell| { + .enumerate() + .map(|(index, cell)| { + let input_type_script = if fixture.current_type_script_input_indices.contains(&index) { + Some(type_script.clone()) + } else { + cell.type_script.clone() + }; let output = packed::CellOutput::new_builder() .capacity::(cell.capacity.pack()) .lock(always_success_lock.clone()) - .type_(packed::ScriptOpt::from(cell.type_script.clone())) + .type_(packed::ScriptOpt::from(input_type_script)) .build(); context.create_cell(output, cell.data.clone()) }) @@ -518,6 +529,7 @@ pub fn build_simple_fixture( CkbVmFixture { script_args, inputs, + current_type_script_input_indices: Vec::new(), outputs, cell_deps: Vec::new(), witnesses: Vec::new(), @@ -563,6 +575,7 @@ pub fn build_dao_fixture( CkbVmFixture { script_args, inputs, + current_type_script_input_indices: Vec::new(), outputs, cell_deps: Vec::new(), witnesses: Vec::new(), @@ -595,6 +608,7 @@ pub fn build_dao_data_fixture( CkbVmFixture { script_args, inputs, + current_type_script_input_indices: Vec::new(), outputs, cell_deps: Vec::new(), witnesses: Vec::new(),