1use hns_covenants::{
2 Covenant, CovenantError, CovenantKind, FinalizeCovenant, MAX_RESOURCE_SIZE, NameState,
3 TransferCovenant,
4};
5use hns_primitives::{BlockHash, Height, NameHash};
6use thiserror::Error;
7
8use crate::{Address, Coin, Input, Output, Transaction, TransactionError, Witness};
9
10const NAME_TRANSACTION_VERSION: u32 = 0;
11const NAME_TRANSACTION_LOCKTIME: u32 = 0;
12const NAME_INPUT_SEQUENCE: u32 = u32::MAX;
13
14pub fn build_transfer_output(
21 owner: &Coin,
22 recipient: &Address,
23) -> Result<Output, NameTransactionError> {
24 validate_name_coin_source(owner, "TRANSFER")?;
25 owner.address.validate()?;
26 recipient.validate()?;
27 let (name_hash, start_height) = transferable_owner_anchor(&owner.covenant)?;
28 let transfer = TransferCovenant::new(
29 name_hash,
30 start_height,
31 recipient.version,
32 recipient.hash.clone(),
33 )?;
34 let output = Output {
35 value: owner.value,
36 address: owner.address.clone(),
37 covenant: transfer.to_covenant()?,
38 };
39 output.encode()?;
40 Ok(output)
41}
42
43pub fn verify_transfer_output(
46 output: &Output,
47 owner: &Coin,
48 recipient: &Address,
49) -> Result<(), NameTransactionError> {
50 if output != &build_transfer_output(owner, recipient)? {
51 return Err(NameTransactionError::InvalidTransfer(
52 "linked output differs from the canonical owner-preserving TRANSFER",
53 ));
54 }
55 TransferCovenant::try_from(&output.covenant)?;
56 Ok(())
57}
58
59pub fn build_transfer_transaction(
69 owner: &Coin,
70 recipient: &Address,
71 mut additional_inputs: Vec<Input>,
72 additional_outputs: Vec<Output>,
73) -> Result<Transaction, NameTransactionError> {
74 reject_repeated_name_input(owner, &additional_inputs)?;
75 let mut inputs = vec![Input {
76 previous_output: owner.outpoint,
77 sequence: NAME_INPUT_SEQUENCE,
78 witness: Witness::default(),
79 }];
80 inputs.append(&mut additional_inputs);
81 let mut outputs = vec![build_transfer_output(owner, recipient)?];
82 outputs.extend(additional_outputs);
83 let transaction = Transaction {
84 version: NAME_TRANSACTION_VERSION,
85 inputs,
86 outputs,
87 locktime: NAME_TRANSACTION_LOCKTIME,
88 };
89 transaction.size()?;
90 Ok(transaction)
91}
92
93pub fn verify_transfer_at_index_zero(
101 transaction: &Transaction,
102 owner: &Coin,
103 recipient: &Address,
104) -> Result<(), NameTransactionError> {
105 verify_name_transaction_header(transaction, owner, "TRANSFER")?;
106 let output = transaction
107 .outputs
108 .first()
109 .ok_or(NameTransactionError::InvalidTransfer(
110 "linked output at index zero is missing",
111 ))?;
112 verify_transfer_output(output, owner, recipient)?;
113 Ok(())
114}
115
116pub fn build_finalize_output(
125 transfer_coin: &Coin,
126 state: &NameState,
127 renewal_block: BlockHash,
128) -> Result<Output, NameTransactionError> {
129 validate_name_coin_source(transfer_coin, "FINALIZE")?;
130 transfer_coin.address.validate()?;
131 let transfer = TransferCovenant::try_from(&transfer_coin.covenant)?;
132 validate_transfer_state(transfer_coin, state, &transfer)?;
133 let recipient = Address::new(transfer.recipient_version, transfer.recipient_hash.clone())?;
134 let finalize = FinalizeCovenant::from_name_state(state, renewal_block)?;
135 let output = Output {
136 value: transfer_coin.value,
137 address: recipient,
138 covenant: finalize.to_covenant()?,
139 };
140 output.encode()?;
141 Ok(output)
142}
143
144pub fn verify_finalize_output(
149 output: &Output,
150 transfer_coin: &Coin,
151 state: &NameState,
152 renewal_block: BlockHash,
153) -> Result<(), NameTransactionError> {
154 if output != &build_finalize_output(transfer_coin, state, renewal_block)? {
155 return Err(NameTransactionError::InvalidFinalize(
156 "linked output differs from the canonical value-preserving FINALIZE",
157 ));
158 }
159 FinalizeCovenant::try_from(&output.covenant)?;
160 Ok(())
161}
162
163pub fn build_finalize_transaction(
172 transfer_coin: &Coin,
173 state: &NameState,
174 renewal_block: BlockHash,
175 mut additional_inputs: Vec<Input>,
176 additional_outputs: Vec<Output>,
177) -> Result<Transaction, NameTransactionError> {
178 reject_repeated_name_input(transfer_coin, &additional_inputs)?;
179 let mut inputs = vec![Input {
180 previous_output: transfer_coin.outpoint,
181 sequence: NAME_INPUT_SEQUENCE,
182 witness: Witness::default(),
183 }];
184 inputs.append(&mut additional_inputs);
185 let mut outputs = vec![build_finalize_output(transfer_coin, state, renewal_block)?];
186 outputs.extend(additional_outputs);
187 let transaction = Transaction {
188 version: NAME_TRANSACTION_VERSION,
189 inputs,
190 outputs,
191 locktime: NAME_TRANSACTION_LOCKTIME,
192 };
193 transaction.size()?;
194 Ok(transaction)
195}
196
197pub fn verify_finalize_at_index_zero(
205 transaction: &Transaction,
206 transfer_coin: &Coin,
207 state: &NameState,
208 renewal_block: BlockHash,
209) -> Result<(), NameTransactionError> {
210 verify_name_transaction_header(transaction, transfer_coin, "FINALIZE")?;
211 let output = transaction
212 .outputs
213 .first()
214 .ok_or(NameTransactionError::InvalidFinalize(
215 "linked output at index zero is missing",
216 ))?;
217 verify_finalize_output(output, transfer_coin, state, renewal_block)?;
218 Ok(())
219}
220
221fn transferable_owner_anchor(
222 covenant: &Covenant,
223) -> Result<(NameHash, Height), NameTransactionError> {
224 match covenant.kind {
225 CovenantKind::Register => {
226 require_exact_items(covenant, 4, "REGISTER")?;
227 require_bounded_resource(covenant, 2, "REGISTER")?;
228 require_hash(covenant, 3, "REGISTER renewal block")?;
229 }
230 CovenantKind::Update => {
231 require_exact_items(covenant, 3, "UPDATE")?;
232 require_bounded_resource(covenant, 2, "UPDATE")?;
233 }
234 CovenantKind::Renew => {
235 require_exact_items(covenant, 3, "RENEW")?;
236 require_hash(covenant, 2, "RENEW renewal block")?;
237 }
238 CovenantKind::Finalize => {
239 let finalize = FinalizeCovenant::try_from(covenant)?;
240 return Ok((finalize.name_hash, finalize.start_height));
241 }
242 kind => return Err(NameTransactionError::UnsupportedTransferSource { kind }),
243 }
244 Ok((
245 NameHash::new(require_hash(covenant, 0, "name hash")?),
246 Height::new(require_u32(covenant, 1, "start height")?),
247 ))
248}
249
250fn validate_transfer_state(
251 transfer_coin: &Coin,
252 state: &NameState,
253 transfer: &TransferCovenant,
254) -> Result<(), NameTransactionError> {
255 state.validate_key_binding()?;
256 if state.is_null()
257 || !state.registered
258 || state.expired
259 || state.revoked.get() != 0
260 || state.transfer.get() == 0
261 {
262 return Err(NameTransactionError::InvalidFinalize(
263 "name state is not an active registered transfer",
264 ));
265 }
266 if state.owner_outpoint() != Some(transfer_coin.outpoint) {
267 return Err(NameTransactionError::InvalidFinalize(
268 "name-state owner does not identify the TRANSFER coin",
269 ));
270 }
271 if state.value != transfer_coin.value {
272 return Err(NameTransactionError::InvalidFinalize(
273 "name-state locked value differs from the TRANSFER coin",
274 ));
275 }
276 if state.transfer != transfer_coin.height {
277 return Err(NameTransactionError::InvalidFinalize(
278 "name-state transfer height differs from the TRANSFER coin height",
279 ));
280 }
281 if state.name_hash != transfer.name_hash || state.height != transfer.start_height {
282 return Err(NameTransactionError::InvalidFinalize(
283 "name-state identity differs from the TRANSFER covenant",
284 ));
285 }
286 Ok(())
287}
288
289fn verify_name_transaction_header(
290 transaction: &Transaction,
291 name_coin: &Coin,
292 operation: &'static str,
293) -> Result<(), NameTransactionError> {
294 transaction.size()?;
295 if transaction.version != NAME_TRANSACTION_VERSION
296 || transaction.locktime != NAME_TRANSACTION_LOCKTIME
297 {
298 return Err(NameTransactionError::InvalidTransaction(
299 "name transaction version or locktime differs from HSD construction",
300 ));
301 }
302 let name_input = transaction
303 .inputs
304 .first()
305 .ok_or(NameTransactionError::InvalidTransaction(
306 "name input at index zero is missing",
307 ))?;
308 if name_input.previous_output != name_coin.outpoint
309 || name_input.sequence != NAME_INPUT_SEQUENCE
310 {
311 return Err(NameTransactionError::InvalidTransaction(
312 "name input is not the canonical index-zero outpoint",
313 ));
314 }
315 if transaction.inputs[1..]
316 .iter()
317 .any(|input| input.previous_output == name_coin.outpoint)
318 {
319 return Err(NameTransactionError::RepeatedNameInput { operation });
320 }
321 Ok(())
322}
323
324fn reject_repeated_name_input(
325 name_coin: &Coin,
326 additional_inputs: &[Input],
327) -> Result<(), NameTransactionError> {
328 if additional_inputs
329 .iter()
330 .any(|input| input.previous_output == name_coin.outpoint)
331 {
332 return Err(NameTransactionError::RepeatedNameInput {
333 operation: "name transition",
334 });
335 }
336 Ok(())
337}
338
339fn validate_name_coin_source(
340 coin: &Coin,
341 operation: &'static str,
342) -> Result<(), NameTransactionError> {
343 if coin.outpoint.is_null() || coin.coinbase {
344 return Err(NameTransactionError::InvalidSourceCoin { operation });
345 }
346 Ok(())
347}
348
349fn require_exact_items(
350 covenant: &Covenant,
351 expected: usize,
352 kind: &'static str,
353) -> Result<(), NameTransactionError> {
354 if covenant.items.len() != expected {
355 return Err(NameTransactionError::MalformedOwnerCovenant { kind });
356 }
357 Ok(())
358}
359
360fn require_bounded_resource(
361 covenant: &Covenant,
362 index: usize,
363 kind: &'static str,
364) -> Result<(), NameTransactionError> {
365 if covenant
366 .item(index)
367 .is_none_or(|resource| resource.len() > MAX_RESOURCE_SIZE)
368 {
369 return Err(NameTransactionError::MalformedOwnerCovenant { kind });
370 }
371 Ok(())
372}
373
374fn require_hash(
375 covenant: &Covenant,
376 index: usize,
377 field: &'static str,
378) -> Result<[u8; 32], NameTransactionError> {
379 covenant
380 .item(index)
381 .and_then(|item| item.try_into().ok())
382 .ok_or(NameTransactionError::MalformedOwnerField { field })
383}
384
385fn require_u32(
386 covenant: &Covenant,
387 index: usize,
388 field: &'static str,
389) -> Result<u32, NameTransactionError> {
390 covenant
391 .item_u32(index)
392 .ok_or(NameTransactionError::MalformedOwnerField { field })
393}
394
395#[derive(Debug, Error)]
396pub enum NameTransactionError {
397 #[error(transparent)]
398 Covenant(#[from] CovenantError),
399 #[error(transparent)]
400 Transaction(#[from] TransactionError),
401 #[error("{kind:?} cannot be the source of a TRANSFER")]
402 UnsupportedTransferSource { kind: CovenantKind },
403 #[error("malformed owner {kind} covenant")]
404 MalformedOwnerCovenant { kind: &'static str },
405 #[error("malformed owner covenant field: {field}")]
406 MalformedOwnerField { field: &'static str },
407 #[error("invalid canonical TRANSFER: {0}")]
408 InvalidTransfer(&'static str),
409 #[error("invalid canonical FINALIZE: {0}")]
410 InvalidFinalize(&'static str),
411 #[error("invalid canonical name transaction: {0}")]
412 InvalidTransaction(&'static str),
413 #[error("{operation} source is a null-outpoint or coinbase coin")]
414 InvalidSourceCoin { operation: &'static str },
415 #[error("{operation} repeats the name outpoint in its funding inputs")]
416 RepeatedNameInput { operation: &'static str },
417}
418
419#[cfg(test)]
420mod tests {
421 use hns_covenants::hash_name;
422 use hns_primitives::{Dollarydoos, Outpoint, TransactionHash};
423
424 use super::*;
425
426 fn owner_coin() -> Coin {
427 let name = b"handshake";
428 Coin {
429 outpoint: Outpoint {
430 transaction_hash: TransactionHash::new([1; 32]),
431 index: 2,
432 },
433 value: Dollarydoos::new(3),
434 height: Height::new(4),
435 coinbase: false,
436 address: Address::new(0, vec![5; 20]).expect("address"),
437 covenant: FinalizeCovenant::new(
438 name.to_vec(),
439 Height::new(6),
440 false,
441 Height::new(0),
442 1,
443 BlockHash::new([7; 32]),
444 )
445 .expect("finalize")
446 .to_covenant()
447 .expect("covenant"),
448 }
449 }
450
451 #[test]
452 fn transfer_preserves_owner_value_and_address() {
453 let owner = owner_coin();
454 let recipient = Address::new(0, vec![8; 20]).expect("recipient");
455 let transaction = build_transfer_transaction(&owner, &recipient, Vec::new(), Vec::new())
456 .expect("transfer");
457 assert_eq!(transaction.outputs[0].value, owner.value);
458 assert_eq!(transaction.outputs[0].address, owner.address);
459 verify_transfer_at_index_zero(&transaction, &owner, &recipient).expect("valid");
460 }
461
462 #[test]
463 fn index_zero_transfer_accepts_an_independent_name_transition_suffix() {
464 let owner = owner_coin();
465 let recipient = Address::new(0, vec![8; 20]).expect("recipient");
466 let mut additional_owner = owner_coin();
467 additional_owner.outpoint = Outpoint {
468 transaction_hash: TransactionHash::new([12; 32]),
469 index: 3,
470 };
471 additional_owner.covenant = FinalizeCovenant::new(
472 b"batched".to_vec(),
473 Height::new(7),
474 false,
475 Height::new(0),
476 1,
477 BlockHash::new([13; 32]),
478 )
479 .expect("additional finalize")
480 .to_covenant()
481 .expect("additional covenant");
482 let additional_recipient = Address::new(0, vec![14; 20]).expect("recipient");
483 let additional_output = build_transfer_output(&additional_owner, &additional_recipient)
484 .expect("additional transfer output");
485 let transaction = build_transfer_transaction(
486 &owner,
487 &recipient,
488 vec![Input {
489 previous_output: additional_owner.outpoint,
490 sequence: NAME_INPUT_SEQUENCE,
491 witness: Witness::default(),
492 }],
493 vec![additional_output],
494 )
495 .expect("batched transfer");
496
497 verify_transfer_at_index_zero(&transaction, &owner, &recipient)
498 .expect("index-zero transfer");
499 assert_eq!(
500 crate::verify_covenant_links(&transaction, &[owner, additional_owner])
501 .expect("valid covenant batch")
502 .linked_outputs,
503 2
504 );
505 }
506
507 #[test]
508 fn finalize_binds_current_state_and_transfer_recipient() {
509 let owner = owner_coin();
510 let recipient = Address::new(0, vec![8; 20]).expect("recipient");
511 let transfer_output = build_transfer_output(&owner, &recipient).expect("transfer output");
512 let transfer_coin = Coin {
513 outpoint: Outpoint {
514 transaction_hash: TransactionHash::new([9; 32]),
515 index: 0,
516 },
517 value: transfer_output.value,
518 height: Height::new(10),
519 coinbase: false,
520 address: transfer_output.address,
521 covenant: transfer_output.covenant,
522 };
523 let name = b"handshake".to_vec();
524 let mut state = NameState::null(hash_name(&name).expect("name hash"));
525 state.name = name;
526 state.height = Height::new(6);
527 state.owner = transfer_coin.outpoint;
528 state.value = transfer_coin.value;
529 state.transfer = transfer_coin.height;
530 state.registered = true;
531 let renewal_block = BlockHash::new([11; 32]);
532 let transaction = build_finalize_transaction(
533 &transfer_coin,
534 &state,
535 renewal_block,
536 Vec::new(),
537 Vec::new(),
538 )
539 .expect("finalize");
540 assert_eq!(transaction.outputs[0].value, transfer_coin.value);
541 assert_eq!(transaction.outputs[0].address, recipient);
542 verify_finalize_at_index_zero(&transaction, &transfer_coin, &state, renewal_block)
543 .expect("valid");
544 }
545
546 #[test]
547 fn name_transition_authority_mutations_fail_closed() {
548 let owner = owner_coin();
549 let recipient = Address::new(0, vec![8; 20]).expect("recipient");
550
551 let mut null_owner = owner.clone();
552 null_owner.outpoint = Outpoint::NULL;
553 assert!(matches!(
554 build_transfer_output(&null_owner, &recipient),
555 Err(NameTransactionError::InvalidSourceCoin {
556 operation: "TRANSFER"
557 })
558 ));
559 let mut coinbase_owner = owner.clone();
560 coinbase_owner.coinbase = true;
561 assert!(build_transfer_output(&coinbase_owner, &recipient).is_err());
562 assert!(matches!(
563 build_transfer_transaction(
564 &owner,
565 &recipient,
566 vec![Input {
567 previous_output: owner.outpoint,
568 sequence: NAME_INPUT_SEQUENCE,
569 witness: Witness::default(),
570 }],
571 Vec::new(),
572 ),
573 Err(NameTransactionError::RepeatedNameInput { .. })
574 ));
575
576 let transfer_output = build_transfer_output(&owner, &recipient).expect("transfer output");
577 let transfer_coin = Coin {
578 outpoint: Outpoint {
579 transaction_hash: TransactionHash::new([9; 32]),
580 index: 0,
581 },
582 value: transfer_output.value,
583 height: Height::new(10),
584 coinbase: false,
585 address: transfer_output.address,
586 covenant: transfer_output.covenant,
587 };
588 let name = b"handshake".to_vec();
589 let mut state = NameState::null(hash_name(&name).expect("name hash"));
590 state.name = name;
591 state.height = Height::new(6);
592 state.owner = transfer_coin.outpoint;
593 state.value = transfer_coin.value;
594 state.transfer = transfer_coin.height;
595 state.registered = true;
596 let renewal_block = BlockHash::new([11; 32]);
597
598 let assert_state_rejected = |mutated: NameState| {
599 assert!(build_finalize_output(&transfer_coin, &mutated, renewal_block).is_err());
600 };
601 let mut wrong_owner = state.clone();
602 wrong_owner.owner.index += 1;
603 assert_state_rejected(wrong_owner);
604 let mut wrong_value = state.clone();
605 wrong_value.value = Dollarydoos::new(state.value.get() + 1);
606 assert_state_rejected(wrong_value);
607 let mut wrong_transfer_height = state.clone();
608 wrong_transfer_height.transfer = Height::new(state.transfer.get() + 1);
609 assert_state_rejected(wrong_transfer_height);
610 let mut unregistered = state.clone();
611 unregistered.registered = false;
612 assert_state_rejected(unregistered);
613 let mut expired = state.clone();
614 expired.expired = true;
615 assert_state_rejected(expired);
616 let mut revoked = state.clone();
617 revoked.revoked = Height::new(1);
618 assert_state_rejected(revoked);
619 let mut wrong_identity = state.clone();
620 wrong_identity.height = Height::new(state.height.get() + 1);
621 assert_state_rejected(wrong_identity);
622
623 let transaction = build_finalize_transaction(
624 &transfer_coin,
625 &state,
626 renewal_block,
627 Vec::new(),
628 Vec::new(),
629 )
630 .expect("finalize");
631 let mut wrong_version = transaction.clone();
632 wrong_version.version += 1;
633 assert!(
634 verify_finalize_at_index_zero(&wrong_version, &transfer_coin, &state, renewal_block,)
635 .is_err()
636 );
637 let mut wrong_sequence = transaction.clone();
638 wrong_sequence.inputs[0].sequence -= 1;
639 assert!(
640 verify_finalize_at_index_zero(&wrong_sequence, &transfer_coin, &state, renewal_block,)
641 .is_err()
642 );
643 let mut wrong_outpoint = transaction.clone();
644 wrong_outpoint.inputs[0].previous_output.index += 1;
645 assert!(
646 verify_finalize_at_index_zero(&wrong_outpoint, &transfer_coin, &state, renewal_block,)
647 .is_err()
648 );
649 let mut wrong_output = transaction;
650 wrong_output.outputs[0].value = Dollarydoos::new(transfer_coin.value.get() + 1);
651 assert!(
652 verify_finalize_at_index_zero(&wrong_output, &transfer_coin, &state, renewal_block,)
653 .is_err()
654 );
655 }
656}