Skip to content

Commit a569542

Browse files
committed
fix: harden mainnet deployment boundary
1 parent b395010 commit a569542

4 files changed

Lines changed: 389 additions & 343 deletions

File tree

‎crates/cellscript-ckb-adapter/src/bin/cellscript-deploy.rs‎

Lines changed: 157 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ use cellscript_ckb_adapter::{
55
};
66
use ckb_types::{
77
bytes::Bytes,
8-
core::ScriptHashType,
9-
packed::{CellInput, OutPoint},
8+
core::{DepType, ScriptHashType},
9+
packed::{CellDep, CellInput, OutPoint},
1010
prelude::*,
1111
H160,
1212
};
13-
use clap::{Parser, Subcommand};
13+
use clap::{Parser, Subcommand, ValueEnum};
1414
use std::path::PathBuf;
1515

1616
#[derive(Parser, Debug)]
1717
#[command(name = "cellscript-deploy")]
18-
#[command(about = "CellScript CKB adapter CLI — deploy, act, and query on-chain state")]
18+
#[command(about = "CellScript CKB adapter CLI — build mainnet deployment transactions, act, and query on-chain state")]
1919
#[command(version = env!("CARGO_PKG_VERSION"))]
2020
struct Cli {
2121
/// CKB node RPC URL
@@ -32,10 +32,11 @@ struct Cli {
3232

3333
#[derive(Subcommand, Debug)]
3434
enum Commands {
35-
/// Deploy a compiled artifact as an on-chain code cell with TYPE_ID
35+
/// Refuse unsigned submission and direct callers to the external-signing flow
36+
#[command(hide = true)]
3637
Deploy(DeployArgs),
3738

38-
/// Build a headless deploy transaction without submitting
39+
/// Build a mainnet unsigned deploy transaction for external signing
3940
BuildDeploy(BuildDeployArgs),
4041

4142
/// Build a transaction from an action plan
@@ -49,8 +50,8 @@ enum Commands {
4950
}
5051

5152
#[derive(clap::Args, Debug)]
52-
struct DeployArgs {
53-
/// Artifact binary file path (.s or .cell)
53+
struct DeploySpecArgs {
54+
/// Compiled RISC-V ELF artifact path
5455
#[arg(long)]
5556
artifact: PathBuf,
5657

@@ -63,16 +64,30 @@ struct DeployArgs {
6364
name: String,
6465

6566
/// Fee in shannons
66-
#[arg(long, default_value_t = 1_000)]
67+
#[arg(long, default_value_t = 10_000)]
6768
fee: u64,
6869

6970
/// Capacity input out_point (format: 0x<TX_HASH>:<INDEX>)
7071
#[arg(long)]
7172
capacity_out_point: String,
7273

73-
/// Capacity input shannons
74-
#[arg(long, default_value_t = 200_000_000_000)]
75-
capacity_shannons: u64,
74+
/// Code reference hash type: type creates a TYPE_ID cell; data variants create an immutable data cell
75+
#[arg(long, value_enum, default_value_t = DeploymentHashType::Type)]
76+
hash_type: DeploymentHashType,
77+
78+
/// CellDep out_point for the input lock script (format: 0x<TX_HASH>:<INDEX>)
79+
#[arg(long, default_value = "0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c:0")]
80+
lock_cell_dep_out_point: String,
81+
82+
/// CellDep kind for the input lock script
83+
#[arg(long, value_enum, default_value_t = CliDepType::DepGroup)]
84+
lock_cell_dep_type: CliDepType,
85+
}
86+
87+
#[derive(clap::Args, Debug)]
88+
struct DeployArgs {
89+
#[command(flatten)]
90+
spec: DeploySpecArgs,
7691

7792
/// Max attempts to wait for commitment
7893
#[arg(long, default_value_t = 30)]
@@ -89,29 +104,42 @@ struct DeployArgs {
89104

90105
#[derive(clap::Args, Debug)]
91106
struct BuildDeployArgs {
92-
/// Artifact binary file path
93-
#[arg(long)]
94-
artifact: PathBuf,
95-
96-
/// Deployer lock script args (hex, 20 bytes for secp256k1-sighash)
97-
#[arg(long)]
98-
lock_arg: String,
107+
#[command(flatten)]
108+
spec: DeploySpecArgs,
109+
}
99110

100-
/// Name for the deployment
101-
#[arg(long, default_value = "cellscript-contract")]
102-
name: String,
111+
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
112+
enum DeploymentHashType {
113+
Type,
114+
Data,
115+
Data1,
116+
Data2,
117+
}
103118

104-
/// Fee in shannons
105-
#[arg(long, default_value_t = 1_000)]
106-
fee: u64,
119+
impl From<DeploymentHashType> for ScriptHashType {
120+
fn from(value: DeploymentHashType) -> Self {
121+
match value {
122+
DeploymentHashType::Type => Self::Type,
123+
DeploymentHashType::Data => Self::Data,
124+
DeploymentHashType::Data1 => Self::Data1,
125+
DeploymentHashType::Data2 => Self::Data2,
126+
}
127+
}
128+
}
107129

108-
/// Capacity input out_point (format: 0x<TX_HASH>:<INDEX>)
109-
#[arg(long)]
110-
capacity_out_point: String,
130+
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
131+
enum CliDepType {
132+
Code,
133+
DepGroup,
134+
}
111135

112-
/// Capacity input shannons
113-
#[arg(long, default_value_t = 200_000_000_000)]
114-
capacity_shannons: u64,
136+
impl From<CliDepType> for DepType {
137+
fn from(value: CliDepType) -> Self {
138+
match value {
139+
CliDepType::Code => Self::Code,
140+
CliDepType::DepGroup => Self::DepGroup,
141+
}
142+
}
115143
}
116144

117145
#[derive(clap::Args, Debug)]
@@ -173,20 +201,13 @@ fn parse_lock_arg(s: &str) -> Result<H160> {
173201
}
174202

175203
/// Shared spec builder for deploy and build-deploy.
176-
fn build_deploy_spec(
177-
artifact: PathBuf,
178-
lock_arg: String,
179-
name: String,
180-
fee: u64,
181-
capacity_out_point: String,
182-
capacity_shannons: u64,
183-
) -> Result<DeployArtifactSpec> {
184-
let artifact_binary = std::fs::read(&artifact)?;
204+
fn build_deploy_spec(adapter: &CellScriptAdapter, args: DeploySpecArgs) -> Result<DeployArtifactSpec> {
205+
let artifact_binary = std::fs::read(&args.artifact)?;
185206
let artifact_binary = Bytes::from(artifact_binary);
186207
let artifact_hash = ckb_hash::blake2b_256(&artifact_binary).iter().map(|b| format!("{:02x}", b)).collect::<String>();
187208

188-
let lock_arg = parse_lock_arg(&lock_arg)?;
189-
// Construct secp256k1-sighash lock script (code_hash for mainnet/devnet).
209+
let lock_arg = parse_lock_arg(&args.lock_arg)?;
210+
// Construct the mainnet secp256k1-sighash lock script.
190211
let lock_script = cellscript_ckb_adapter::construct_script(&cellscript_ckb_adapter::ScriptSpec::new(
191212
[
192213
0x9b, 0x81, 0x97, 0x34, 0x7e, 0x6e, 0x47, 0x1d, 0x7e, 0xa2, 0x8b, 0x52, 0x0c, 0x45, 0x3e, 0x18, 0x54, 0xf0, 0x96, 0x2e,
@@ -196,97 +217,65 @@ fn build_deploy_spec(
196217
lock_arg.as_bytes().to_vec(),
197218
));
198219

199-
let capacity_out_point = parse_out_point(&capacity_out_point)?;
220+
let capacity_out_point = parse_out_point(&args.capacity_out_point)?;
221+
let (capacity_input_shannons, capacity_input_data) = adapter.resolve_pure_capacity_input(&capacity_out_point, &lock_script)?;
200222
let capacity_input = CellInput::new_builder().previous_output(capacity_out_point).build();
223+
let lock_cell_dep = CellDep::new_builder()
224+
.out_point(parse_out_point(&args.lock_cell_dep_out_point)?)
225+
.dep_type(DepType::from(args.lock_cell_dep_type))
226+
.build();
201227

202228
Ok(DeployArtifactSpec {
203-
name,
229+
name: args.name,
204230
artifact_binary,
205231
artifact_hash,
206232
deployer_lock: lock_script,
207233
capacity_input,
208-
capacity_input_shannons: capacity_shannons,
209-
capacity_input_data: Bytes::new(),
210-
type_id_hash_type: ScriptHashType::Type,
234+
capacity_input_shannons,
235+
capacity_input_data,
236+
type_id_hash_type: args.hash_type.into(),
211237
type_script: None,
212-
cell_deps: Vec::new(),
238+
cell_deps: vec![lock_cell_dep],
213239
header_deps: Vec::new(),
214-
fee_shannons: fee,
240+
fee_shannons: args.fee,
215241
})
216242
}
217243

218-
fn cmd_deploy(rpc: &str, json: bool, args: DeployArgs) -> Result<()> {
219-
let spec = build_deploy_spec(args.artifact, args.lock_arg, args.name, args.fee, args.capacity_out_point, args.capacity_shannons)?;
220-
let name = spec.name.clone();
221-
222-
let (tx, deploy_evidence) = build_deploy_transaction(&spec)?;
223-
224-
// Connect and submit.
225-
let adapter = CellScriptAdapter::connect(rpc)?;
226-
227-
// Estimate cycles.
228-
let estimate_cycles = adapter.estimate_cycles(&tx).ok().map(|e| e.cycles.value());
229-
230-
// Test tx-pool acceptance.
231-
let tx_pool_accepted = adapter.test_tx_pool_accept(&tx).is_ok();
232-
233-
// Submit.
234-
let tx_hash = adapter.submit_transaction(&tx)?;
235-
eprintln!("submitted tx: 0x{}", hex::encode(tx_hash.as_bytes()));
236-
237-
// Wait for commitment.
238-
let committed = adapter.wait_for_commitment(&tx_hash, args.wait_attempts, args.wait_delay_ms)?;
239-
240-
// Build manifest.
241-
let mut hash_bytes = [0u8; 32];
242-
hash_bytes.copy_from_slice(tx_hash.as_bytes());
243-
let manifest = cellscript_ckb_adapter::build_deployment_manifest_from_evidence(&deploy_evidence, &hash_bytes, 0);
244-
245-
// Write manifest if requested.
246-
if let Some(ref path) = args.manifest_out {
247-
let manifest_json = serde_json::to_string_pretty(&manifest)?;
248-
std::fs::write(path, &manifest_json)?;
249-
eprintln!("manifest written to {}", path.display());
250-
}
251-
252-
if json {
253-
let output = serde_json::json!({
254-
"tx_hash": format!("0x{}", hex::encode(tx_hash.as_bytes())),
255-
"committed": true,
256-
"block_hash": format!("0x{}", hex::encode(committed.block_hash.as_bytes())),
257-
"estimate_cycles": estimate_cycles,
258-
"tx_pool_accepted": tx_pool_accepted,
259-
"manifest": manifest,
260-
});
261-
println!("{}", serde_json::to_string_pretty(&output)?);
262-
} else {
263-
println!("deployed {} at tx 0x{}", name, hex::encode(tx_hash.as_bytes()));
264-
println!(" committed in block 0x{}", hex::encode(committed.block_hash.as_bytes()));
265-
if let Some(cycles) = estimate_cycles {
266-
println!(" estimate_cycles: {cycles}");
267-
}
268-
}
269-
270-
Ok(())
244+
fn cmd_deploy(_rpc: &str, _json: bool, args: DeployArgs) -> Result<()> {
245+
let _ = (args.spec, args.wait_attempts, args.wait_delay_ms, args.manifest_out);
246+
bail!(
247+
"direct deploy is disabled because this CLI does not hold or invoke a signer; use build-deploy, sign the returned transaction with a CKB wallet, then broadcast it"
248+
)
271249
}
272250

273251
fn cmd_build_deploy(rpc: &str, json: bool, args: BuildDeployArgs) -> Result<()> {
274-
let spec = build_deploy_spec(args.artifact, args.lock_arg, args.name, args.fee, args.capacity_out_point, args.capacity_shannons)?;
252+
let adapter = CellScriptAdapter::connect(rpc)?;
253+
adapter.require_mainnet()?;
254+
let spec = build_deploy_spec(&adapter, args.spec)?;
275255

276-
let (tx, _evidence) = build_deploy_transaction(&spec)?;
256+
let (tx, evidence) = build_deploy_transaction(&spec)?;
277257

278-
// Try to estimate cycles if node is available.
279-
let estimate = CellScriptAdapter::connect(rpc).ok().and_then(|a| a.estimate_cycles(&tx).ok()).map(|e| e.cycles.value());
258+
// An unsigned secp transaction is expected to fail script verification, so
259+
// cycle estimation remains informational until the external signer fills it.
260+
let estimate = adapter.estimate_cycles(&tx).ok().map(|e| e.cycles.value());
280261

281262
if json {
282263
let tx_json = serde_json::to_value(cellscript_ckb_adapter::to_rpc_transaction(&tx))?;
283264
let output = serde_json::json!({
265+
"can_submit": false,
266+
"signing_required": true,
284267
"transaction": tx_json,
285268
"estimate_cycles": estimate,
269+
"evidence": evidence,
286270
});
287271
println!("{}", serde_json::to_string_pretty(&output)?);
288272
} else {
289-
println!("built deploy transaction ({} bytes)", tx.data().serialized_size_in_block());
273+
println!("built unsigned deploy transaction ({} bytes)", tx.data().serialized_size_in_block());
274+
println!(" can_submit: false");
275+
println!(" signing_required: true");
276+
println!(" hash_type: {}", evidence.hash_type);
277+
println!(" code_hash: 0x{}", hex::encode(&evidence.code_hash));
278+
println!(" cell_deps: {}", evidence.cell_deps);
290279
if let Some(cycles) = estimate {
291280
println!(" estimate_cycles: {cycles}");
292281
}
@@ -407,3 +396,64 @@ fn cmd_info(rpc: &str, json: bool) -> Result<()> {
407396

408397
Ok(())
409398
}
399+
400+
#[cfg(test)]
401+
mod tests {
402+
use super::*;
403+
404+
const INPUT: &str = "0x1111111111111111111111111111111111111111111111111111111111111111:0";
405+
406+
fn parse_build_deploy(extra: &[&str]) -> Cli {
407+
let mut args = vec![
408+
"cellscript-deploy",
409+
"build-deploy",
410+
"--artifact",
411+
"registry-type-script",
412+
"--lock-arg",
413+
"0x2222222222222222222222222222222222222222",
414+
"--capacity-out-point",
415+
INPUT,
416+
];
417+
args.extend_from_slice(extra);
418+
Cli::try_parse_from(args).unwrap()
419+
}
420+
421+
#[test]
422+
fn build_deploy_accepts_immutable_data1() {
423+
let cli = parse_build_deploy(&["--hash-type", "data1"]);
424+
let Commands::BuildDeploy(args) = cli.command else {
425+
panic!("expected build-deploy command");
426+
};
427+
assert_eq!(args.spec.hash_type, DeploymentHashType::Data1);
428+
assert_eq!(args.spec.fee, 10_000);
429+
assert_eq!(args.spec.lock_cell_dep_type, CliDepType::DepGroup);
430+
assert_eq!(args.spec.lock_cell_dep_out_point, "0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c:0");
431+
}
432+
433+
#[test]
434+
fn build_deploy_defaults_to_type_id() {
435+
let cli = parse_build_deploy(&[]);
436+
let Commands::BuildDeploy(args) = cli.command else {
437+
panic!("expected build-deploy command");
438+
};
439+
assert_eq!(args.spec.hash_type, DeploymentHashType::Type);
440+
}
441+
442+
#[test]
443+
fn build_deploy_rejects_unknown_hash_type() {
444+
let error = Cli::try_parse_from([
445+
"cellscript-deploy",
446+
"build-deploy",
447+
"--artifact",
448+
"registry-type-script",
449+
"--lock-arg",
450+
"0x2222222222222222222222222222222222222222",
451+
"--capacity-out-point",
452+
INPUT,
453+
"--hash-type",
454+
"data3",
455+
])
456+
.unwrap_err();
457+
assert!(error.to_string().contains("invalid value 'data3'"));
458+
}
459+
}

0 commit comments

Comments
 (0)