feat: import inscription parser

This commit is contained in:
Ludo Galabru
2023-02-10 10:17:12 -05:00
parent 20dc1a4094
commit 45e0147ecf
6 changed files with 318 additions and 41 deletions

View File

@@ -141,7 +141,15 @@ pub fn serialize_bitcoin_payload_to_json<'a>(
"transaction_identifier": transaction.transaction_identifier,
"operations": transaction.operations,
"metadata": json!({
"inputs": transaction.metadata.inputs,
"inputs": transaction.metadata.inputs.iter().map(|input| {
json!({
"previous_output": {
"txid": format!("0x{}", input.previous_output.txid),
"vout": input.previous_output.vout,
},
"sequence": input.sequence,
})
}).collect::<Vec<_>>(),
"outputs": transaction.metadata.outputs,
"stacks_operations": transaction.metadata.stacks_operations,
"ordinal_operations": transaction.metadata.ordinal_operations,
@@ -388,7 +396,7 @@ impl BitcoinChainhookSpecification {
false
}
BitcoinPredicateType::Protocol(Protocols::Ordinal(
OrdinalOperations::NewInscription,
OrdinalOperations::InscriptionRevealed,
)) => {
for op in tx.metadata.ordinal_operations.iter() {
if let OrdinalOperation::InscriptionReveal(_) = op {

View File

@@ -337,7 +337,7 @@ pub enum StacksOperations {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OrdinalOperations {
NewInscription,
InscriptionRevealed,
}
pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] {
@@ -348,10 +348,6 @@ pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] {
}
}
pub fn get_ordinal_canonical_magic_bytes() -> (usize, [u8; 3]) {
return (37, *b"ord");
}
pub struct PoxConfig {
pub genesis_block_height: u64,
pub prepare_phase_len: u64,

View File

@@ -1,15 +1,15 @@
mod blocks_pool;
mod ordinal;
use std::time::Duration;
use crate::chainhooks::types::{
get_canonical_pox_config, get_ordinal_canonical_magic_bytes, get_stacks_canonical_magic_bytes,
PoxConfig, StacksOpcodes,
get_canonical_pox_config, get_stacks_canonical_magic_bytes, PoxConfig, StacksOpcodes,
};
use crate::indexer::IndexerConfig;
use crate::observer::BitcoinConfig;
use crate::utils::Context;
use bitcoincore_rpc::bitcoin;
use bitcoincore_rpc::bitcoin::{self, Script};
use bitcoincore_rpc_json::{GetRawTransactionResult, GetRawTransactionResultVout};
pub use blocks_pool::BitcoinBlockPool;
use chainhook_types::bitcoin::{OutPoint, TxIn, TxOut};
@@ -23,6 +23,8 @@ use clarity_repl::clarity::util::hash::{hex_bytes, to_hex};
use hiro_system_kit::slog;
use rocket::serde::json::Value as JsonValue;
use self::ordinal::InscriptionParser;
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Block {
@@ -205,28 +207,26 @@ fn try_parse_ordinal_operation(
block_height: u64,
ctx: &Context,
) -> Option<OrdinalOperation> {
let (pos, magic_bytes) = get_ordinal_canonical_magic_bytes();
let limit = pos + magic_bytes.len();
for input in tx.vin.iter() {
if let Some(ref witnesses) = input.txinwitness {
for witness in witnesses.iter() {
if witness.len() > limit && witness[pos..limit] == magic_bytes {
ctx.try_log(|logger| {
slog::info!(
logger,
"Ordinal operation detected in transaction {}",
tx.txid,
)
});
return Some(OrdinalOperation::InscriptionReveal(
OrdinalInscriptionRevealData {
satoshi_point: "".into(),
content_type: "".into(),
content: vec![],
},
));
}
for bytes in witnesses.iter() {
let script = Script::from(bytes.to_vec());
let parser = InscriptionParser {
instructions: script.instructions().peekable(),
};
let inscription = match parser.parse_script() {
Ok(inscription) => inscription,
Err(_) => continue,
};
return Some(OrdinalOperation::InscriptionReveal(
OrdinalInscriptionRevealData {
satoshi_point: "".into(),
content_type: inscription.content_type().unwrap_or("unknown").to_string(),
content: format!("0x{}", to_hex(&inscription.body().unwrap_or(&vec![]))),
},
));
}
}
}

View File

@@ -0,0 +1,268 @@
use std::collections::BTreeMap;
use std::str::FromStr;
use {
bitcoincore_rpc::bitcoin::{
blockdata::{
opcodes,
script::{self, Instruction, Instructions},
},
util::taproot::TAPROOT_ANNEX_PREFIX,
Script, Witness,
},
std::{iter::Peekable, str},
};
const PROTOCOL_ID: &[u8] = b"ord";
const BODY_TAG: &[u8] = &[];
const CONTENT_TYPE_TAG: &[u8] = &[1];
#[derive(Debug, PartialEq, Clone)]
pub struct Inscription {
body: Option<Vec<u8>>,
content_type: Option<Vec<u8>>,
}
impl Inscription {
fn append_reveal_script_to_builder(&self, mut builder: script::Builder) -> script::Builder {
builder = builder
.push_opcode(opcodes::OP_FALSE)
.push_opcode(opcodes::all::OP_IF)
.push_slice(PROTOCOL_ID);
if let Some(content_type) = &self.content_type {
builder = builder
.push_slice(CONTENT_TYPE_TAG)
.push_slice(content_type);
}
if let Some(body) = &self.body {
builder = builder.push_slice(BODY_TAG);
for chunk in body.chunks(520) {
builder = builder.push_slice(chunk);
}
}
builder.push_opcode(opcodes::all::OP_ENDIF)
}
pub(crate) fn append_reveal_script(&self, builder: script::Builder) -> Script {
self.append_reveal_script_to_builder(builder).into_script()
}
pub(crate) fn media(&self) -> Media {
if self.body.is_none() {
return Media::Unknown;
}
let content_type = match self.content_type() {
Some(content_type) => content_type,
None => return Media::Unknown,
};
content_type.parse().unwrap_or(Media::Unknown)
}
pub(crate) fn body(&self) -> Option<&[u8]> {
Some(self.body.as_ref()?)
}
pub(crate) fn into_body(self) -> Option<Vec<u8>> {
self.body
}
pub(crate) fn content_length(&self) -> Option<usize> {
Some(self.body()?.len())
}
pub(crate) fn content_type(&self) -> Option<&str> {
str::from_utf8(self.content_type.as_ref()?).ok()
}
}
#[derive(Debug, PartialEq)]
pub enum InscriptionError {
EmptyWitness,
InvalidInscription,
KeyPathSpend,
NoInscription,
Script(script::Error),
UnrecognizedEvenField,
}
type Result<T, E = InscriptionError> = std::result::Result<T, E>;
pub struct InscriptionParser<'a> {
pub instructions: Peekable<Instructions<'a>>,
}
impl<'a> InscriptionParser<'a> {
pub fn parse(witness: &Witness) -> Result<Inscription> {
if witness.is_empty() {
return Err(InscriptionError::EmptyWitness);
}
if witness.len() == 1 {
return Err(InscriptionError::KeyPathSpend);
}
let annex = witness
.last()
.and_then(|element| element.first().map(|byte| *byte == TAPROOT_ANNEX_PREFIX))
.unwrap_or(false);
if witness.len() == 2 && annex {
return Err(InscriptionError::KeyPathSpend);
}
let script = witness
.iter()
.nth(if annex {
witness.len() - 1
} else {
witness.len() - 2
})
.unwrap();
InscriptionParser {
instructions: Script::from(Vec::from(script)).instructions().peekable(),
}
.parse_script()
}
pub fn parse_script(mut self) -> Result<Inscription> {
loop {
let next = self.advance()?;
if next == Instruction::PushBytes(&[]) {
if let Some(inscription) = self.parse_inscription()? {
return Ok(inscription);
}
}
}
}
fn advance(&mut self) -> Result<Instruction<'a>> {
self.instructions
.next()
.ok_or(InscriptionError::NoInscription)?
.map_err(InscriptionError::Script)
}
fn parse_inscription(&mut self) -> Result<Option<Inscription>> {
if self.advance()? == Instruction::Op(opcodes::all::OP_IF) {
if !self.accept(Instruction::PushBytes(PROTOCOL_ID))? {
return Err(InscriptionError::NoInscription);
}
let mut fields = BTreeMap::new();
loop {
match self.advance()? {
Instruction::PushBytes(BODY_TAG) => {
let mut body = Vec::new();
while !self.accept(Instruction::Op(opcodes::all::OP_ENDIF))? {
body.extend_from_slice(self.expect_push()?);
}
fields.insert(BODY_TAG, body);
break;
}
Instruction::PushBytes(tag) => {
if fields.contains_key(tag) {
return Err(InscriptionError::InvalidInscription);
}
fields.insert(tag, self.expect_push()?.to_vec());
}
Instruction::Op(opcodes::all::OP_ENDIF) => break,
_ => return Err(InscriptionError::InvalidInscription),
}
}
let body = fields.remove(BODY_TAG);
let content_type = fields.remove(CONTENT_TYPE_TAG);
for tag in fields.keys() {
if let Some(lsb) = tag.first() {
if lsb % 2 == 0 {
return Err(InscriptionError::UnrecognizedEvenField);
}
}
}
return Ok(Some(Inscription { body, content_type }));
}
Ok(None)
}
fn expect_push(&mut self) -> Result<&'a [u8]> {
match self.advance()? {
Instruction::PushBytes(bytes) => Ok(bytes),
_ => Err(InscriptionError::InvalidInscription),
}
}
fn accept(&mut self, instruction: Instruction) -> Result<bool> {
match self.instructions.peek() {
Some(Ok(next)) => {
if *next == instruction {
self.advance()?;
Ok(true)
} else {
Ok(false)
}
}
Some(Err(err)) => Err(InscriptionError::Script(*err)),
None => Ok(false),
}
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub(crate) enum Media {
Audio,
Iframe,
Image,
Pdf,
Text,
Unknown,
Video,
}
impl Media {
const TABLE: &'static [(&'static str, Media, &'static [&'static str])] = &[
("application/json", Media::Text, &["json"]),
("application/pdf", Media::Pdf, &["pdf"]),
("application/pgp-signature", Media::Text, &["asc"]),
("application/yaml", Media::Text, &["yaml", "yml"]),
("audio/flac", Media::Audio, &["flac"]),
("audio/mpeg", Media::Audio, &["mp3"]),
("audio/wav", Media::Audio, &["wav"]),
("image/apng", Media::Image, &["apng"]),
("image/avif", Media::Image, &[]),
("image/gif", Media::Image, &["gif"]),
("image/jpeg", Media::Image, &["jpg", "jpeg"]),
("image/png", Media::Image, &["png"]),
("image/svg+xml", Media::Iframe, &["svg"]),
("image/webp", Media::Image, &["webp"]),
("model/stl", Media::Unknown, &["stl"]),
("text/html;charset=utf-8", Media::Iframe, &["html"]),
("text/plain;charset=utf-8", Media::Text, &["txt"]),
("video/mp4", Media::Video, &["mp4"]),
("video/webm", Media::Video, &["webm"]),
];
}
impl FromStr for Media {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
for entry in Self::TABLE {
if entry.0 == s {
return Ok(entry.1);
}
}
Err("unknown content type: {s}".to_string())
}
}

View File

@@ -1,3 +1,7 @@
use bitcoincore_rpc::bitcoin::Script;
use crate::indexer::bitcoin::ordinal::InscriptionParser;
use super::super::tests::{helpers, process_bitcoin_blocks_and_check_expectations};
#[test]
@@ -209,17 +213,17 @@ fn test_bitcoin_vector_040() {
fn test_ordinal_inscription_parsing() {
use clarity_repl::clarity::util::hash::hex_bytes;
let witness = hex_bytes("208737bc46923c3e64c7e6768c0346879468bf3aba795a5f5f56efca288f50ed2aac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d38004c9948656c6c6f2030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030300a68").unwrap();
let bytes = hex_bytes("208737bc46923c3e64c7e6768c0346879468bf3aba795a5f5f56efca288f50ed2aac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d38004c9948656c6c6f2030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030300a68").unwrap();
let (pos, magic_bytes) = super::get_ordinal_canonical_magic_bytes();
let limit = pos + magic_bytes.len();
let script = Script::from(bytes);
let parser = InscriptionParser {
instructions: script.instructions().peekable(),
};
println!("{:?}", &magic_bytes);
let inscription = match parser.parse_script() {
Ok(inscription) => inscription,
Err(_) => panic!(),
};
// println!("{:?}", &witness);
println!("{:?}", &witness[pos..limit]);
if witness.len() > limit && witness[pos..limit] == magic_bytes {
} else {
panic!();
}
println!("{:?}", inscription);
}

View File

@@ -273,6 +273,7 @@ pub struct BitcoinTransactionMetadata {
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OrdinalOperation {
InscriptionCommit(OrdinalInscriptionCommitData),
InscriptionReveal(OrdinalInscriptionRevealData),
@@ -285,7 +286,7 @@ pub struct OrdinalInscriptionCommitData {}
pub struct OrdinalInscriptionRevealData {
pub satoshi_point: String,
pub content_type: String,
pub content: Vec<u8>,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]