Skip to content
Closed
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
41 changes: 20 additions & 21 deletions crates/cellscript-ckb-adapter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<WitnessArgs> {
pub fn place_entry_witness_payload(base: &WitnessArgs, placement: EntryWitnessPlacementAbi, payload: Bytes) -> Result<WitnessArgs> {
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())
}
}
}

Expand Down Expand Up @@ -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]
Expand Down
5 changes: 3 additions & 2 deletions docs/CELLSCRIPT_CKB_ADAPTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
39 changes: 34 additions & 5 deletions docs/CELLSCRIPT_ENTRY_WITNESS_ABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,53 @@
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
```

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.
Expand Down
6 changes: 5 additions & 1 deletion scripts/cellscript_0_14_scope_audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
21 changes: 18 additions & 3 deletions src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading