Skip to content

Commit df63252

Browse files
committed
Allow setting fallback data for sptrs
1 parent e86a3a1 commit df63252

File tree

3 files changed

+118
-32
lines changed

3 files changed

+118
-32
lines changed

ptr/src/lib.rs

Lines changed: 108 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
603624
pub 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

683760
pub fn is_ptr_minting_locktime(lock_time: &LockTime) -> bool {
684761
if let LockTime::Seconds(s) = lock_time {

veritas/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ if (ptrout) {
116116
if (ptr) {
117117
console.log('📍 SPTR ID: ', ptr.getId().toString());
118118
console.log('📅 Last update: ', ptr.getLastUpdate());
119+
const data = ptr.getData();
120+
if (data) {
121+
console.log('📦 Data: ', Buffer.from(data).toString('utf-8'));
122+
}
119123
}
120124
}
121125

veritas/src/wasm.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ mod wasm_api {
312312
pub fn get_last_update(&self) -> u32 {
313313
self.inner.last_update
314314
}
315+
316+
#[wasm_bindgen(js_name = "getData")]
317+
pub fn get_data(&self) -> Option<Vec<u8>> {
318+
self.inner.data.clone()
319+
}
315320
}
316321

317322
#[wasm_bindgen]
@@ -326,7 +331,7 @@ mod wasm_api {
326331

327332
#[wasm_bindgen(js_name = "getRoot")]
328333
pub fn get_root(&self) -> Vec<u8> {
329-
self.inner.root.to_vec()
334+
self.inner.state_root.to_vec()
330335
}
331336

332337
#[wasm_bindgen(js_name = "getBlockHeight")]

0 commit comments

Comments
 (0)