1use std::io;
21use std::path::Path;
22
23use alloy::primitives::{Address, B256, Bytes};
24use alloy::providers::Provider;
25use alloy::signers::SignerSync;
26use alloy::signers::local::PrivateKeySigner;
27use anyhow::Context;
28use decdn_common::cli;
29use decdn_common::identity;
30use decdn_incentive::capacity_bond::CapacityBond;
31use decdn_incentive::{bind_sig, node_register};
32
33use crate::commands::{chain_ctx, terms};
34
35pub async fn run(args: &cli::RegisterArgs, global_config: Option<&Path>) -> anyhow::Result<()> {
37 let config_path = args.chain.common.config.as_deref().or(global_config);
38 let file = chain_ctx::load_optional_config(config_path)?;
39 let resolved = chain_ctx::resolve(&args.chain, &file)?;
40 let cb_addr = resolved.capacity_bond_address;
41
42 let signer = chain_ctx::load_operator_signer(&args.chain.common, &resolved.keystore).await?;
43 let provider = decdn_client::provider::build_provider(&resolved.rpc_url, &signer)?;
44
45 let terms_hash = CapacityBond::new(cb_addr, &provider)
51 .currentTermsHash()
52 .call()
53 .await
54 .with_context(|| {
55 format!("failed to read currentTermsHash from CapacityBond at {cb_addr}")
56 })?;
57 terms::ensure_accepted_async(terms_hash, args.accept_terms).await?;
58
59 let mut tx_slot = None;
62 let multiaddrs = registration_multiaddrs(&args.multiaddrs, &file);
63 let outcome = submit_registration(
64 &provider,
65 &signer,
66 &resolved.data_dir,
67 cb_addr,
68 resolved.chain_id,
69 &args.region,
70 &multiaddrs,
71 terms_hash,
72 args.chain.common.dry_run,
73 &mut tx_slot,
74 )
75 .await?;
76
77 let mut out = io::stdout().lock();
78 let label = if outcome.tx.is_some() {
79 "failed to write result"
80 } else {
81 "failed to write dry-run output"
82 };
83 write_outcome(&mut out, &outcome, args.chain.common.json).context(label)?;
84 Ok(())
85}
86
87pub(crate) fn registration_multiaddrs(
94 explicit: &[String],
95 file: &chain_ctx::FileConfig,
96) -> Vec<String> {
97 if !explicit.is_empty() {
98 return explicit.to_vec();
99 }
100 let bind_port = file.bind_port();
101 let chosen = default_multiaddrs(&decdn_common::net::route_public_ips(), bind_port);
102 if chosen.is_empty() {
103 eprintln!(
104 "note: no --multiaddr given and no public address on the default route (behind \
105 NAT); registering none. Peers reach the node through the relay and its pkarr \
106 record. If UDP {bind_port} is forwarded to this host, pass the forwarded address \
107 with --multiaddr."
108 );
109 } else {
110 eprintln!(
111 "note: no --multiaddr given; registering this host's public address: {}",
112 chosen.join(", ")
113 );
114 }
115 chosen
116}
117
118fn default_multiaddrs(public: &[std::net::IpAddr], bind_port: u16) -> Vec<String> {
119 public
120 .iter()
121 .map(|ip| decdn_common::net::quic_multiaddr(*ip, bind_port))
122 .collect()
123}
124
125pub(crate) struct RegisterOutcome {
129 pub(crate) node_id: B256,
130 pub(crate) operator: Address,
131 pub(crate) chain_id: u64,
132 pub(crate) capacity_bond: Address,
133 pub(crate) region: String,
134 pub(crate) binding_nonce: u64,
135 pub(crate) registration_nonce: u64,
136 pub(crate) multiaddr_count: usize,
137 pub(crate) binding_sig: Vec<u8>,
138 pub(crate) ed25519_sig: Vec<u8>,
139 pub(crate) tx: Option<B256>,
140}
141
142#[allow(clippy::too_many_arguments)]
153pub(crate) async fn submit_registration<P: Provider + Clone>(
154 provider: &P,
155 signer: &PrivateKeySigner,
156 data_dir: &Path,
157 cb_addr: Address,
158 chain_id: u64,
159 region: &str,
160 multiaddrs: &[String],
161 terms_hash: B256,
162 dry_run: bool,
163 tx_slot: &mut Option<B256>,
164) -> anyhow::Result<RegisterOutcome> {
165 let key_path = identity::key_path(data_dir);
170 anyhow::ensure!(
171 key_path.exists(),
172 "no node key at {}; run `decdn key-gen` first (register binds the existing iroh identity)",
173 key_path.display(),
174 );
175 let node_secret = identity::load_or_generate(data_dir)
176 .with_context(|| format!("failed to load node key from {}", data_dir.display()))?;
177 let node_id = B256::from_slice(node_secret.public().as_bytes());
178
179 let operator = signer.address();
180 let bond = CapacityBond::new(cb_addr, provider);
181
182 let binding_nonce: u64 =
184 bond.bindingNonce(operator).call().await.with_context(|| {
185 format!("failed to read bindingNonce from CapacityBond at {cb_addr}")
186 })?;
187 let registration_nonce: u64 =
188 bond.registrationNonce(node_id)
189 .call()
190 .await
191 .with_context(|| {
192 format!("failed to read registrationNonce from CapacityBond at {cb_addr}")
193 })?;
194
195 let domain = bind_sig::bind_node_id_domain(chain_id, cb_addr);
204 let bind_hash =
205 bind_sig::register_node_signing_hash(node_id, binding_nonce, terms_hash, &domain);
206 let binding_sig = signer.sign_hash_sync(&bind_hash)?.as_bytes().to_vec();
207
208 let digest =
211 node_register::ownership_message_digest(node_id, operator, chain_id, registration_nonce);
212 let ed25519_sig = node_secret.sign(digest.as_slice()).to_bytes().to_vec();
213
214 let packed_multiaddrs = node_register::pack_multiaddrs(multiaddrs)?;
215
216 let mut outcome = RegisterOutcome {
217 node_id,
218 operator,
219 chain_id,
220 capacity_bond: cb_addr,
221 region: region.to_string(),
222 binding_nonce,
223 registration_nonce,
224 multiaddr_count: multiaddrs.len(),
225 binding_sig: binding_sig.clone(),
226 ed25519_sig: ed25519_sig.clone(),
227 tx: None,
228 };
229
230 if dry_run {
231 return Ok(outcome);
232 }
233
234 let tx = chain_ctx::send(
244 bond.registerNode(
245 node_id,
246 Bytes::from(packed_multiaddrs),
247 region.to_string(),
248 terms_hash,
249 Bytes::from(binding_sig),
250 Bytes::from(ed25519_sig),
251 ),
252 "registerNode",
253 Some(
254 "most likely the bond does not cover minBond / the declared-capacity curve, the \
255 nodeId/address is already bound, or a signature was rejected",
256 ),
257 tx_slot,
258 )
259 .await?;
260 outcome.tx = Some(tx);
261 Ok(outcome)
262}
263
264pub(crate) fn write_outcome(
268 w: &mut impl io::Write,
269 o: &RegisterOutcome,
270 json: bool,
271) -> io::Result<()> {
272 if json {
273 let value = serde_json::json!({
274 "submitted": o.tx.is_some(),
275 "tx": o.tx.map(|h| format!("{h:#x}")),
276 "node_id": format!("{:#x}", o.node_id),
277 "operator": format!("{:#x}", o.operator),
278 "chain_id": o.chain_id,
279 "capacity_bond": format!("{:#x}", o.capacity_bond),
280 "region": o.region,
281 "binding_nonce": o.binding_nonce,
282 "registration_nonce": o.registration_nonce,
283 "multiaddrs": o.multiaddr_count,
284 "binding_sig": format!("0x{}", alloy::hex::encode(&o.binding_sig)),
285 "ed25519_sig": format!("0x{}", alloy::hex::encode(&o.ed25519_sig)),
286 });
287 return writeln!(w, "{value}");
288 }
289 writeln!(w, "node_id={:#x}", o.node_id)?;
290 writeln!(w, "operator={:#x}", o.operator)?;
291 writeln!(w, "chain_id={}", o.chain_id)?;
292 writeln!(w, "capacity_bond={:#x}", o.capacity_bond)?;
293 writeln!(w, "region={}", o.region)?;
294 writeln!(w, "binding_nonce={}", o.binding_nonce)?;
295 writeln!(w, "registration_nonce={}", o.registration_nonce)?;
296 writeln!(w, "multiaddrs={}", o.multiaddr_count)?;
297 writeln!(w, "binding_sig=0x{}", alloy::hex::encode(&o.binding_sig))?;
298 writeln!(w, "ed25519_sig=0x{}", alloy::hex::encode(&o.ed25519_sig))?;
299 match o.tx {
300 Some(h) => writeln!(w, "submitted=true tx={h:#x}"),
301 None => writeln!(w, "submitted=false dry_run=true"),
302 }
303}
304
305#[cfg(test)]
306#[allow(clippy::unwrap_used, clippy::expect_used)]
307mod tests;