diff --git a/src/envelope.rs b/src/envelope.rs index 5d5f5c2747..68415799fb 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -2,8 +2,13 @@ use { super::*, bitcoin::blockdata::{ opcodes, - script::{self, Instruction, Instructions}, + script::{ + self, + Instruction::{self, Op, PushBytes}, + Instructions, + }, }, + std::iter::Peekable, }; pub(crate) const PROTOCOL_ID: [u8; 3] = *b"ord"; @@ -22,10 +27,11 @@ pub(crate) type ParsedEnvelope = Envelope; #[derive(Debug, Default, PartialEq, Clone)] pub(crate) struct Envelope { - pub(crate) payload: T, pub(crate) input: u32, pub(crate) offset: u32, + pub(crate) payload: T, pub(crate) pushnum: bool, + pub(crate) stutter: bool, } fn remove_field(fields: &mut BTreeMap<&[u8], Vec<&[u8]>>, field: &[u8]) -> Option> { @@ -111,6 +117,7 @@ impl From for ParsedEnvelope { input: envelope.input, offset: envelope.offset, pushnum: envelope.pushnum, + stutter: envelope.stutter, } } } @@ -142,13 +149,17 @@ impl RawEnvelope { fn from_tapscript(tapscript: &Script, input: usize) -> Result> { let mut envelopes = Vec::new(); - let mut instructions = tapscript.instructions(); + let mut instructions = tapscript.instructions().peekable(); - while let Some(instruction) = instructions.next() { - if instruction? == Instruction::PushBytes((&[]).into()) { - if let Some(envelope) = Self::from_instructions(&mut instructions, input, envelopes.len())? - { + let mut stuttered = false; + while let Some(instruction) = instructions.next().transpose()? { + if instruction == PushBytes((&[]).into()) { + let (stutter, envelope) = + Self::from_instructions(&mut instructions, input, envelopes.len(), stuttered)?; + if let Some(envelope) = envelope { envelopes.push(envelope); + } else { + stuttered = stutter; } } } @@ -156,17 +167,29 @@ impl RawEnvelope { Ok(envelopes) } + fn accept(instructions: &mut Peekable, instruction: Instruction) -> Result { + if instructions.peek() == Some(&Ok(instruction)) { + instructions.next().transpose()?; + Ok(true) + } else { + Ok(false) + } + } + fn from_instructions( - instructions: &mut Instructions, + instructions: &mut Peekable, input: usize, offset: usize, - ) -> Result> { - if instructions.next().transpose()? != Some(Instruction::Op(opcodes::all::OP_IF)) { - return Ok(None); + stutter: bool, + ) -> Result<(bool, Option)> { + if !Self::accept(instructions, Op(opcodes::all::OP_IF))? { + let stutter = instructions.peek() == Some(&Ok(PushBytes((&[]).into()))); + return Ok((stutter, None)); } - if instructions.next().transpose()? != Some(Instruction::PushBytes((&PROTOCOL_ID).into())) { - return Ok(None); + if !Self::accept(instructions, PushBytes((&PROTOCOL_ID).into()))? { + let stutter = instructions.peek() == Some(&Ok(PushBytes((&[]).into()))); + return Ok((stutter, None)); } let mut pushnum = false; @@ -175,87 +198,91 @@ impl RawEnvelope { loop { match instructions.next().transpose()? { - None => return Ok(None), - Some(Instruction::Op(opcodes::all::OP_ENDIF)) => { - return Ok(Some(Envelope { - input: input.try_into().unwrap(), - offset: offset.try_into().unwrap(), - payload, - pushnum, - })); + None => return Ok((false, None)), + Some(Op(opcodes::all::OP_ENDIF)) => { + return Ok(( + false, + Some(Envelope { + input: input.try_into().unwrap(), + offset: offset.try_into().unwrap(), + payload, + pushnum, + stutter, + }), + )); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_NEG1)) => { + Some(Op(opcodes::all::OP_PUSHNUM_NEG1)) => { pushnum = true; payload.push(vec![0x81]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_1)) => { + Some(Op(opcodes::all::OP_PUSHNUM_1)) => { pushnum = true; payload.push(vec![1]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_2)) => { + Some(Op(opcodes::all::OP_PUSHNUM_2)) => { pushnum = true; payload.push(vec![2]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_3)) => { + Some(Op(opcodes::all::OP_PUSHNUM_3)) => { pushnum = true; payload.push(vec![3]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_4)) => { + Some(Op(opcodes::all::OP_PUSHNUM_4)) => { pushnum = true; payload.push(vec![4]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_5)) => { + Some(Op(opcodes::all::OP_PUSHNUM_5)) => { pushnum = true; payload.push(vec![5]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_6)) => { + Some(Op(opcodes::all::OP_PUSHNUM_6)) => { pushnum = true; payload.push(vec![6]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_7)) => { + Some(Op(opcodes::all::OP_PUSHNUM_7)) => { pushnum = true; payload.push(vec![7]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_8)) => { + Some(Op(opcodes::all::OP_PUSHNUM_8)) => { pushnum = true; payload.push(vec![8]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_9)) => { + Some(Op(opcodes::all::OP_PUSHNUM_9)) => { pushnum = true; payload.push(vec![9]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_10)) => { + Some(Op(opcodes::all::OP_PUSHNUM_10)) => { pushnum = true; payload.push(vec![10]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_11)) => { + Some(Op(opcodes::all::OP_PUSHNUM_11)) => { pushnum = true; payload.push(vec![11]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_12)) => { + Some(Op(opcodes::all::OP_PUSHNUM_12)) => { pushnum = true; payload.push(vec![12]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_13)) => { + Some(Op(opcodes::all::OP_PUSHNUM_13)) => { pushnum = true; payload.push(vec![13]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_14)) => { + Some(Op(opcodes::all::OP_PUSHNUM_14)) => { pushnum = true; payload.push(vec![14]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_15)) => { + Some(Op(opcodes::all::OP_PUSHNUM_15)) => { pushnum = true; payload.push(vec![15]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_16)) => { + Some(Op(opcodes::all::OP_PUSHNUM_16)) => { pushnum = true; payload.push(vec![16]); } - Some(Instruction::PushBytes(push)) => { + Some(PushBytes(push)) => { payload.push(push.as_bytes().to_vec()); } - Some(_) => return Ok(None), + Some(_) => return Ok((false, None)), } } } @@ -895,4 +922,81 @@ mod tests { ); } } + + #[test] + fn stuttering() { + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + assert_eq!( + parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), + vec![ParsedEnvelope { + payload: Default::default(), + stutter: true, + ..Default::default() + }], + ); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + assert_eq!( + parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), + vec![ParsedEnvelope { + payload: Default::default(), + stutter: true, + ..Default::default() + }], + ); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + assert_eq!( + parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), + vec![ParsedEnvelope { + payload: Default::default(), + stutter: true, + ..Default::default() + }], + ); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_AND) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + assert_eq!( + parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), + vec![ParsedEnvelope { + payload: Default::default(), + stutter: false, + ..Default::default() + }], + ); + } } diff --git a/src/index.rs b/src/index.rs index 14ba28a4d7..94bc4c6ad8 100644 --- a/src/index.rs +++ b/src/index.rs @@ -3117,7 +3117,45 @@ mod tests { } #[test] + fn inscriptions_with_stutter_are_cursed() { + for context in Context::configurations() { + context.mine_blocks(1); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice([]) + .push_opcode(opcodes::all::OP_PUSHNUM_1) + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness)], + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .inscription_number, + -1 + ); + } + } + // https://github.com/ordinals/ord/issues/2062 + #[test] fn zero_value_transaction_inscription_not_cursed_but_unbound() { for context in Context::configurations() { context.mine_blocks(1); diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index a224da67cb..83b3a7d370 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -9,6 +9,7 @@ enum Curse { Pointer, Pushnum, Reinscription, + Stutter, UnrecognizedEvenField, } @@ -148,6 +149,8 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Some(Curse::Pointer) } else if inscription.pushnum { Some(Curse::Pushnum) + } else if inscription.stutter { + Some(Curse::Stutter) } else if let Some((id, count)) = inscribed_offset { if *count > 1 { Some(Curse::Reinscription)