1use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey};
2
3use crate::address_funds::OrchardAddress;
4use crate::address_funds::PlatformAddress;
5use crate::fee::Credits;
6use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
7use crate::identity::signer::Signer;
8use crate::identity::IdentityPublicKey;
9use crate::serialization::Signable;
10use crate::shielded::compute_shielded_identity_create_fee;
11use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
12use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation;
13use crate::shielded::OrchardBundleParams;
14use crate::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::methods::IdentityCreateFromShieldedPoolTransitionMethodsV0;
15use crate::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::{
16 derive_identity_id_from_actions, identity_id_from_nullifiers,
17 IdentityCreateFromShieldedPoolTransition,
18};
19use crate::state_transition::StateTransition;
20use crate::ProtocolError;
21use platform_value::Identifier;
22use platform_version::version::PlatformVersion;
23
24use super::{build_spend_bundle_with, serialize_authorized_bundle, OrchardProver, SpendableNote};
25
26pub struct IdentityCreateFromShieldedPoolBuildResult {
33 pub public_keys: Vec<IdentityPublicKeyInCreation>,
35 pub bundle: OrchardBundleParams,
37 pub identity_id: Identifier,
40 pub predicted_fee: Credits,
42}
43
44#[allow(clippy::too_many_arguments)]
92pub async fn build_identity_create_from_shielded_pool_transition<P, S>(
93 public_keys: Vec<(IdentityPublicKey, IdentityPublicKeyInCreation)>,
94 denomination: u64,
95 send_to_address_on_creation_failure: PlatformAddress,
96 spends: Vec<SpendableNote>,
97 change_address: &OrchardAddress,
98 fvk: &FullViewingKey,
99 ask: &SpendAuthorizingKey,
100 anchor: Anchor,
101 prover: &P,
102 identity_signer: &S,
103 memo: [u8; 36],
104 platform_version: &PlatformVersion,
105) -> Result<IdentityCreateFromShieldedPoolBuildResult, ProtocolError>
106where
107 P: OrchardProver,
108 S: Signer<IdentityPublicKey>,
109{
110 if denomination > i64::MAX as u64 {
111 return Err(ProtocolError::ShieldedBuildError(format!(
112 "denomination {} exceeds maximum allowed value {}",
113 denomination,
114 i64::MAX as u64
115 )));
116 }
117 if public_keys.is_empty() {
118 return Err(ProtocolError::ShieldedBuildError(
119 "identity-create-from-shielded-pool requires at least one public key".to_string(),
120 ));
121 }
122
123 let allowed_denominations = platform_version
127 .drive_abci
128 .validation_and_processing
129 .event_constants
130 .shielded_identity_create_denominations;
131 if !allowed_denominations.contains(&denomination) {
132 return Err(ProtocolError::ShieldedBuildError(format!(
133 "denomination {denomination} is not a member of the allowed exit-denomination set {allowed_denominations:?}"
134 )));
135 }
136
137 let total_spent = spends
139 .iter()
140 .try_fold(0u64, |acc, s| acc.checked_add(s.note.value().inner()))
141 .ok_or_else(|| {
142 ProtocolError::ShieldedBuildError(
143 "identity-create-from-shielded-pool total spent value overflows u64".to_string(),
144 )
145 })?;
146 if denomination > total_spent {
147 return Err(ProtocolError::ShieldedBuildError(format!(
148 "denomination {} exceeds total spendable value {}",
149 denomination, total_spent
150 )));
151 }
152
153 let change_amount = total_spent - denomination;
157
158 let num_actions = spends.len().max(2);
162 let fee =
163 compute_shielded_identity_create_fee(num_actions, public_keys.len(), platform_version)?;
164
165 if fee >= denomination {
169 return Err(ProtocolError::ShieldedBuildError(format!(
170 "predicted fee {fee} is not less than the denomination {denomination}; the new identity would have a non-positive balance"
171 )));
172 }
173
174 let in_creation_keys: Vec<IdentityPublicKeyInCreation> =
177 public_keys.iter().map(|(_, c)| c.clone()).collect();
178
179 let mut bound_identity_id: Option<Identifier> = None;
185 let bundle = build_spend_bundle_with(
186 spends,
187 change_address,
188 change_amount,
189 memo,
190 fvk,
191 ask,
192 anchor,
193 prover,
194 |published_nullifiers| {
195 let id = identity_id_from_nullifiers(published_nullifiers);
196 let data = crate::shielded::identity_create_from_shielded_extra_sighash_data(
197 &id.to_buffer(),
198 denomination,
199 &send_to_address_on_creation_failure,
200 &in_creation_keys,
201 platform_version,
202 )?;
203 bound_identity_id = Some(id);
204 Ok(data)
205 },
206 )?;
207 let identity_id = bound_identity_id.ok_or_else(|| {
208 ProtocolError::ShieldedBuildError(
209 "identity id was not derived during bundle build".to_string(),
210 )
211 })?;
212
213 let sb = serialize_authorized_bundle(&bundle);
214
215 if identity_id != derive_identity_id_from_actions(&sb.actions) {
219 return Err(ProtocolError::ShieldedBuildError(
220 "bound identity id does not match the id re-derived from the bundle's published \
221 nullifiers"
222 .to_string(),
223 ));
224 }
225
226 let mut state_transition = IdentityCreateFromShieldedPoolTransition::try_from_bundle(
229 in_creation_keys,
230 denomination,
231 send_to_address_on_creation_failure,
232 sb.actions.clone(),
233 sb.anchor,
234 sb.proof.clone(),
235 sb.binding_signature,
236 platform_version,
237 )?;
238
239 let key_signable_bytes = state_transition.signable_bytes()?;
243
244 let StateTransition::IdentityCreateFromShieldedPool(
245 IdentityCreateFromShieldedPoolTransition::V0(v0),
246 ) = &mut state_transition
247 else {
248 return Err(ProtocolError::ShieldedBuildError(
249 "unexpected state transition variant after try_from_bundle".to_string(),
250 ));
251 };
252
253 for (key_with_witness, (original_key, _)) in v0.public_keys.iter_mut().zip(public_keys.iter()) {
254 if original_key.key_type().is_unique_key_type() {
255 let signature = identity_signer
256 .sign(original_key, &key_signable_bytes)
257 .await?;
258 key_with_witness.set_signature(signature);
259 }
260 }
261
262 let signed_public_keys = std::mem::take(&mut v0.public_keys);
266
267 Ok(IdentityCreateFromShieldedPoolBuildResult {
268 public_keys: signed_public_keys,
269 bundle: OrchardBundleParams {
270 actions: sb.actions,
271 anchor: sb.anchor,
272 proof: sb.proof,
273 binding_signature: sb.binding_signature,
274 },
275 identity_id,
276 predicted_fee: fee,
277 })
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::address_funds::AddressWitness;
284 use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
285 use crate::identity::{KeyType, Purpose, SecurityLevel};
286 use crate::shielded::builder::test_helpers::{
287 test_orchard_address, test_spendable_note, TestProver,
288 };
289 use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0;
290 use grovedb_commitment_tree::{
291 ExtractedNoteCommitment, Hashable, MerkleHashOrchard, MerklePath, SpendingKey,
292 NOTE_COMMITMENT_TREE_DEPTH,
293 };
294 use platform_value::BinaryData;
295
296 #[derive(Debug)]
299 struct DummySigner;
300
301 #[async_trait::async_trait]
302 impl Signer<IdentityPublicKey> for DummySigner {
303 async fn sign(
304 &self,
305 _key: &IdentityPublicKey,
306 _data: &[u8],
307 ) -> Result<BinaryData, ProtocolError> {
308 Ok(BinaryData::new(vec![0u8; 65]))
309 }
310
311 async fn sign_create_witness(
312 &self,
313 _key: &IdentityPublicKey,
314 _data: &[u8],
315 ) -> Result<AddressWitness, ProtocolError> {
316 Err(ProtocolError::ShieldedBuildError(
317 "identity PoP signer never creates address witnesses".to_string(),
318 ))
319 }
320
321 fn can_sign_with(&self, _key: &IdentityPublicKey) -> bool {
322 true
323 }
324 }
325
326 fn key_pair(id: u32) -> (IdentityPublicKey, IdentityPublicKeyInCreation) {
328 let public = IdentityPublicKey::V0(IdentityPublicKeyV0 {
329 id,
330 purpose: Purpose::AUTHENTICATION,
331 security_level: SecurityLevel::MASTER,
332 contract_bounds: None,
333 key_type: KeyType::ECDSA_SECP256K1,
334 read_only: false,
335 data: BinaryData::new(vec![0xAB; 33]),
336 disabled_at: None,
337 });
338 let in_creation = IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 {
339 id,
340 key_type: KeyType::ECDSA_SECP256K1,
341 purpose: Purpose::AUTHENTICATION,
342 security_level: SecurityLevel::MASTER,
343 contract_bounds: None,
344 read_only: false,
345 data: BinaryData::new(vec![0xAB; 33]),
346 signature: BinaryData::new(vec![]),
347 });
348 (public, in_creation)
349 }
350
351 const DENOMINATION: u64 = 10_000_000_000;
353
354 #[tokio::test]
360 async fn single_spend_padded_bundle_derives_id_from_published_nullifiers() {
361 let platform_version = PlatformVersion::latest();
362 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key");
363 let fvk = FullViewingKey::from(&sk);
364 let ask = SpendAuthorizingKey::from(&sk);
365 let change_address = test_orchard_address();
366
367 let spend = test_spendable_note(12_000_000_000);
370 let cmx = ExtractedNoteCommitment::from(spend.note.commitment());
371 let anchor = spend.merkle_path.root(cmx);
372 let real_nullifier = spend.note.nullifier(&fvk).to_bytes();
373
374 let result = build_identity_create_from_shielded_pool_transition(
375 vec![key_pair(0)],
376 DENOMINATION,
377 PlatformAddress::P2pkh([0u8; 20]),
378 vec![spend],
379 &change_address,
380 &fvk,
381 &ask,
382 anchor,
383 &TestProver,
384 &DummySigner,
385 [0u8; 36],
386 platform_version,
387 )
388 .await
389 .expect("a single-spend (padded) build must succeed");
390
391 assert_eq!(
392 result.bundle.actions.len(),
393 2,
394 "a single spend must be padded to the 2-action minimum"
395 );
396 assert!(
397 result
398 .bundle
399 .actions
400 .iter()
401 .any(|action| action.nullifier == real_nullifier),
402 "the real spend's nullifier must be among the published actions"
403 );
404 assert_eq!(
406 result.identity_id,
407 derive_identity_id_from_actions(&result.bundle.actions),
408 "identity id must match the consensus derivation over the published actions"
409 );
410 assert_ne!(
413 result.identity_id,
414 identity_id_from_nullifiers(&[real_nullifier]),
415 "the padding action's dummy nullifier must participate in the id derivation"
416 );
417 assert!(
418 result.predicted_fee < DENOMINATION,
419 "predicted fee must leave the new identity a positive balance"
420 );
421 }
422
423 #[tokio::test]
426 async fn two_spend_unpadded_bundle_id_matches_real_nullifier_derivation() {
427 let platform_version = PlatformVersion::latest();
428 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key");
429 let fvk = FullViewingKey::from(&sk);
430 let ask = SpendAuthorizingKey::from(&sk);
431 let change_address = test_orchard_address();
432
433 let note_a = test_spendable_note(6_000_000_000).note;
437 let note_b = test_spendable_note(7_000_000_000).note;
438 let cmx_a = ExtractedNoteCommitment::from(note_a.commitment());
439 let cmx_b = ExtractedNoteCommitment::from(note_b.commitment());
440
441 let mut auth_path_a = [MerkleHashOrchard::empty_leaf(); NOTE_COMMITMENT_TREE_DEPTH];
442 auth_path_a[0] = MerkleHashOrchard::from_cmx(&cmx_b);
443 let mut auth_path_b = [MerkleHashOrchard::empty_leaf(); NOTE_COMMITMENT_TREE_DEPTH];
444 auth_path_b[0] = MerkleHashOrchard::from_cmx(&cmx_a);
445 let path_a = MerklePath::from_parts(0, auth_path_a);
446 let path_b = MerklePath::from_parts(1, auth_path_b);
447
448 let anchor = path_a.root(cmx_a);
449 assert_eq!(
450 anchor.to_bytes(),
451 path_b.root(cmx_b).to_bytes(),
452 "both witnesses must compute the same anchor"
453 );
454
455 let nf_a = note_a.nullifier(&fvk).to_bytes();
456 let nf_b = note_b.nullifier(&fvk).to_bytes();
457 let spends = vec![
458 SpendableNote {
459 note: note_a,
460 merkle_path: path_a,
461 },
462 SpendableNote {
463 note: note_b,
464 merkle_path: path_b,
465 },
466 ];
467
468 let result = build_identity_create_from_shielded_pool_transition(
469 vec![key_pair(0)],
470 DENOMINATION,
471 PlatformAddress::P2pkh([0u8; 20]),
472 spends,
473 &change_address,
474 &fvk,
475 &ask,
476 anchor,
477 &TestProver,
478 &DummySigner,
479 [0u8; 36],
480 platform_version,
481 )
482 .await
483 .expect("a two-spend build must succeed");
484
485 assert_eq!(
486 result.bundle.actions.len(),
487 2,
488 "two spends + one change output need no padding"
489 );
490 assert_eq!(
491 result.identity_id,
492 derive_identity_id_from_actions(&result.bundle.actions),
493 "identity id must match the consensus derivation over the published actions"
494 );
495 assert_eq!(
496 result.identity_id,
497 identity_id_from_nullifiers(&[nf_a, nf_b]),
498 "with no padding, the published set is exactly the real spends' nullifiers"
499 );
500 }
501}