@@ -420,7 +420,12 @@ impl Validator {
420420 new_delegations : vec ! [ ] ,
421421 } ;
422422
423- let commitment_op = parse_commitment_ops ( & tx) ;
423+ let ptr_op = parse_ptr_op ( & tx) ;
424+ let ( commitment_op, data_op) = match & ptr_op {
425+ Some ( PtrOp :: Commitment ( c) ) => ( Some ( c) , None ) ,
426+ Some ( PtrOp :: Data ( d) ) => ( None , Some ( d. clone ( ) ) ) ,
427+ None => ( None , None ) ,
428+ } ;
424429
425430 // Remove sptr -> space mappings if a space is spent
426431 changeset. revoked_delegations = spent_space_utxos
@@ -436,7 +441,7 @@ impl Validator {
436441 } )
437442 . collect ( ) ;
438443
439- // Allow revoked sptrs to be redefined
444+ // Allow sptrs to be redelegated
440445 let revoked_keys: Vec < RegistrySptrKey > = changeset. revoked_delegations
441446 . iter ( )
442447 . map ( |rd| RegistrySptrKey :: from_sptr :: < H > ( rd. sptr ) )
@@ -522,7 +527,7 @@ impl Validator {
522527 }
523528 // Process spend
524529 changeset. spends . push ( input_ctx. n ) ;
525- self . process_spend ( tx, input_ctx. n , input_ctx. ptrout , & new_space_utxos, & mut changeset, height) ;
530+ self . process_spend ( tx, input_ctx. n , input_ctx. ptrout , & new_space_utxos, & mut changeset, height, & data_op ) ;
526531 }
527532
528533 // Process new PTR outputs
@@ -545,7 +550,7 @@ impl Validator {
545550 n,
546551 sptr : Some ( Ptr {
547552 id : Sptr :: from_spk :: < H > ( output. script_pubkey . clone ( ) ) ,
548- data : None ,
553+ data : data_op . clone ( ) ,
549554 last_update : height,
550555 } ) ,
551556 value : output. value ,
@@ -564,6 +569,7 @@ impl Validator {
564569 new_space_utxos : & Vec < SpaceOut > ,
565570 changeset : & mut TxChangeSet ,
566571 height : u32 ,
572+ data : & Option < Vec < u8 > > ,
567573 ) {
568574 let mut ptr = match ptrout. sptr {
569575 None => return ,
@@ -592,6 +598,14 @@ impl Validator {
592598 }
593599
594600 ptr. last_update = height;
601+ // Only update data if:
602+ // 1. A data OP_RETURN is present
603+ // 2. PTR is P2TR and input uses SIGHASH_ALL (prevents malicious data injection)
604+ if let Some ( new_data) = data {
605+ if ptrout. script_pubkey . is_p2tr ( ) && is_p2tr_sighash_all ( tx, input_index) {
606+ ptr. data = Some ( new_data. clone ( ) ) ;
607+ }
608+ }
595609 ptrout. n = output_index;
596610 ptrout. value = output. value ;
597611 ptrout. script_pubkey = output. script_pubkey . clone ( ) ;
@@ -600,14 +614,26 @@ impl Validator {
600614 }
601615}
602616
617+ #[ repr( u8 ) ]
618+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
619+ pub enum PtrOpType {
620+ Commitment = 0x01 ,
621+ Data = 0x02 ,
622+ }
623+
603624pub enum CommitmentOp {
604625 /// Add one or more new commitments
605626 Commit ( Vec < [ u8 ; 32 ] > ) ,
606627 /// Rollback the last finalized commitment
607628 Rollback ,
608629}
609630
610- pub fn parse_commitment_ops ( tx : & Transaction ) -> Option < CommitmentOp > {
631+ pub enum PtrOp {
632+ Commitment ( CommitmentOp ) ,
633+ Data ( Vec < u8 > ) ,
634+ }
635+
636+ pub fn parse_ptr_op ( tx : & Transaction ) -> Option < PtrOp > {
611637 let txout = tx. output . iter ( ) . find ( |o| o. script_pubkey . is_op_return ( ) ) ?;
612638 let script = txout. script_pubkey . clone ( ) ;
613639
@@ -619,37 +645,50 @@ pub fn parse_commitment_ops(tx: &Transaction) -> Option<CommitmentOp> {
619645 _ => return None ,
620646 }
621647
622- // Second instruction must be push bytes with our marker
648+ // Second instruction contains the marker and payload
623649 match instructions. next ( ) ?. ok ( ) ? {
624650 Instruction :: PushBytes ( bytes) => {
625- // Check marker
626- if bytes. is_empty ( ) || bytes[ 0 ] != 0x77 {
651+ if bytes. is_empty ( ) {
627652 return None ;
628653 }
629654
630- // Rollback: just the marker byte
631- if bytes. len ( ) == 1 {
632- return Some ( CommitmentOp :: Rollback ) ;
633- }
655+ match bytes[ 0 ] {
656+ x if x == PtrOpType :: Commitment as u8 => {
657+ // Rollback: just the marker byte
658+ if bytes. len ( ) == 1 {
659+ return Some ( PtrOp :: Commitment ( CommitmentOp :: Rollback ) ) ;
660+ }
634661
635- // Commitments: marker + N * 32 bytes
636- let payload = & bytes[ 1 ..] ;
637- if payload. len ( ) % 32 != 0 {
638- return None ;
639- }
662+ // Commitments: marker + N * 32 bytes
663+ let payload = & bytes[ 1 ..] ;
664+ if payload. len ( ) % 32 != 0 {
665+ return None ;
666+ }
640667
641- let mut commitments = Vec :: with_capacity ( payload. len ( ) / 32 ) ;
642- for chunk in payload. as_bytes ( ) . chunks_exact ( 32 ) {
643- let mut commitment = [ 0u8 ; 32 ] ;
644- commitment. copy_from_slice ( chunk) ;
645- commitments. push ( commitment) ;
646- }
668+ let mut commitments = Vec :: with_capacity ( payload. len ( ) / 32 ) ;
669+ for chunk in payload. as_bytes ( ) . chunks_exact ( 32 ) {
670+ let mut commitment = [ 0u8 ; 32 ] ;
671+ commitment. copy_from_slice ( chunk) ;
672+ commitments. push ( commitment) ;
673+ }
647674
648- if commitments. is_empty ( ) {
649- return None ;
650- }
675+ if commitments. is_empty ( ) {
676+ return None ;
677+ }
651678
652- Some ( CommitmentOp :: Commit ( commitments) )
679+ Some ( PtrOp :: Commitment ( CommitmentOp :: Commit ( commitments) ) )
680+ }
681+ x if x == PtrOpType :: Data as u8 => {
682+ // Data: marker + payload
683+ let payload = & bytes[ 1 ..] ;
684+ if payload. is_empty ( ) {
685+ return None ;
686+ }
687+
688+ Some ( PtrOp :: Data ( payload. as_bytes ( ) . to_vec ( ) ) )
689+ }
690+ _ => None ,
691+ }
653692 }
654693 _ => None ,
655694 }
@@ -662,13 +701,13 @@ pub fn create_commitment_script(op: &CommitmentOp) -> ScriptBuf {
662701
663702 match op {
664703 CommitmentOp :: Rollback => {
665- // OP_RETURN OP_PUSHBYTES_1 0x77
666- builder = builder. push_slice ( & [ 0x77 ] ) ;
704+ // OP_RETURN OP_PUSHBYTES_1 0x01
705+ builder = builder. push_slice ( & [ PtrOpType :: Commitment as u8 ] ) ;
667706 }
668707 CommitmentOp :: Commit ( commitments) => {
669- // OP_RETURN OP_PUSHBYTES_N 0x77 [commitments...]
708+ // OP_RETURN OP_PUSHBYTES_N 0x01 [commitments...]
670709 let mut buf = PushBytesBuf :: new ( ) ;
671- buf. push ( 0x77 ) . expect ( "valid" ) ;
710+ buf. push ( PtrOpType :: Commitment as u8 ) . expect ( "valid" ) ;
672711 for commitment in commitments {
673712 buf. extend_from_slice ( commitment) . expect ( "" ) ;
674713 }
@@ -679,6 +718,44 @@ pub fn create_commitment_script(op: &CommitmentOp) -> ScriptBuf {
679718 builder. into_script ( )
680719}
681720
721+ /// Create data OP_RETURN script for PTR transfers
722+ /// Format: OP_RETURN 0x02 [data]
723+ pub fn create_data_script ( data : & [ u8 ] ) -> ScriptBuf {
724+ let mut buf = PushBytesBuf :: new ( ) ;
725+ buf. push ( PtrOpType :: Data as u8 ) . expect ( "valid" ) ;
726+ buf. extend_from_slice ( data) . expect ( "valid" ) ;
727+
728+ ScriptBuf :: builder ( )
729+ . push_opcode ( OP_RETURN )
730+ . push_slice ( buf)
731+ . into_script ( )
732+ }
733+
734+ /// Check if an input uses SIGHASH_ALL for a P2TR key path spend
735+ /// Per BIP 341:
736+ /// - 64 bytes = SIGHASH_DEFAULT (0x00), equivalent to SIGHASH_ALL
737+ /// - 65 bytes with last byte 0x01 = SIGHASH_ALL
738+ /// Note: 0x00 is never appended (always 64 bytes for default)
739+ fn is_p2tr_sighash_all ( tx : & Transaction , input_index : usize ) -> bool {
740+ let input = match tx. input . get ( input_index) {
741+ Some ( input) => input,
742+ None => return false ,
743+ } ;
744+
745+ let witness = & input. witness ;
746+ if witness. is_empty ( ) {
747+ return false ;
748+ }
749+
750+ // First witness element is the schnorr signature for P2TR key path
751+ let sig = & witness[ 0 ] ;
752+
753+ match sig. len ( ) {
754+ 64 => true , // SIGHASH_DEFAULT (0x00) - equivalent to SIGHASH_ALL
755+ 65 => sig[ 64 ] == 0x01 , // SIGHASH_ALL (0x01)
756+ _ => false ,
757+ }
758+ }
682759
683760pub fn is_ptr_minting_locktime ( lock_time : & LockTime ) -> bool {
684761 if let LockTime :: Seconds ( s) = lock_time {
0 commit comments