diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 79d913100c..4396d327ba 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -5,10 +5,11 @@ Summary - [Overview](overview.md) - [Digital Artifacts](digital-artifacts.md) - [Inscriptions](inscriptions.md) + - [Delegate](inscriptions/delegate.md) - [Metadata](inscriptions/metadata.md) + - [Pointer](inscriptions/pointer.md) - [Provenance](inscriptions/provenance.md) - [Recursion](inscriptions/recursion.md) - - [Pointer](inscriptions/pointer.md) - [FAQ](faq.md) - [Contributing](contributing.md) - [Donate](donate.md) diff --git a/docs/src/inscriptions.md b/docs/src/inscriptions.md index 4f9ac7c6b8..3104cee8b9 100644 --- a/docs/src/inscriptions.md +++ b/docs/src/inscriptions.md @@ -79,11 +79,12 @@ two data pushes, a tag and a value. Currently, there are six defined fields: - `content_type`, with a tag of `1`, whose value is the MIME type of the body. -- `pointer`, with a tag of `2`, see [pointer docs](./inscriptions/pointer.md). -- `parent`, with a tag of `3`, see [provenance](./inscriptions/provenance.md). -- `metadata`, with a tag of `5`, see [metadata](./inscriptions/metadata.md). +- `pointer`, with a tag of `2`, see [pointer docs](inscriptions/pointer.md). +- `parent`, with a tag of `3`, see [provenance](inscriptions/provenance.md). +- `metadata`, with a tag of `5`, see [metadata](inscriptions/metadata.md). - `metaprotocol`, with a tag of `7`, whose value is the metaprotocol identifier. - `content_encoding`, with a tag of `9`, whose value is the encoding of the body. +- `delegate`, with a tag of `11`, see [delegate](inscriptions/delegate.md). The beginning of the body and end of fields is indicated with an empty data push. diff --git a/docs/src/inscriptions/delegate.md b/docs/src/inscriptions/delegate.md new file mode 100644 index 0000000000..55fc800035 --- /dev/null +++ b/docs/src/inscriptions/delegate.md @@ -0,0 +1,39 @@ +Delegate +======== + +Inscriptions may nominate a delegate inscription. Requests for the content of +an inscription with a delegate will instead return the content and content type +of the delegate. This can be used to cheaply create copies of an inscription. + +### Specification + +To create an inscription I with delegate inscription D: + +- Create an inscription D. Note that inscription D does not have to exist when + making inscription I. It may be inscribed later. Before inscription D is + inscribed, requests for the content of inscription I will return a 404. +- Include tag `11`, i.e. `OP_PUSH 11`, in I, with the value of the serialized + binary inscription ID of D, serialized as the 32-byte `TXID`, followed by the + four-byte little-endian `INDEX`, with trailing zeroes omitted. + +_NB_ The bytes of a bitcoin transaction ID are reversed in their text +representation, so the serialized transaction ID will be in the opposite order. + +### Example + +An example of an inscription which delegates to +`000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fi0`: + +``` +OP_FALSE +OP_IF + OP_PUSH "ord" + OP_PUSH 11 + OP_PUSH 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100 +OP_ENDIF +``` + +Note that the value of tag `11` is binary, not hex. + +The delegate field value uses the same encoding as the parent field. See +[provenance](provenance.md) for more examples of inscrpition ID encodings; diff --git a/src/envelope.rs b/src/envelope.rs index ed712afe61..56af14bba7 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -23,6 +23,9 @@ pub(crate) const PARENT_TAG: [u8; 1] = [3]; pub(crate) const METADATA_TAG: [u8; 1] = [5]; pub(crate) const METAPROTOCOL_TAG: [u8; 1] = [7]; pub(crate) const CONTENT_ENCODING_TAG: [u8; 1] = [9]; +pub(crate) const DELEGATE_TAG: [u8; 1] = [11]; +#[allow(unused)] +pub(crate) const NOP_TAG: [u8; 1] = [255]; type Result = std::result::Result; type RawEnvelope = Envelope>>; @@ -89,6 +92,7 @@ impl From for ParsedEnvelope { let content_encoding = remove_field(&mut fields, &CONTENT_ENCODING_TAG); let content_type = remove_field(&mut fields, &CONTENT_TYPE_TAG); + let delegate = remove_field(&mut fields, &DELEGATE_TAG); let metadata = remove_and_concatenate_field(&mut fields, &METADATA_TAG); let metaprotocol = remove_field(&mut fields, &METAPROTOCOL_TAG); let parent = remove_field(&mut fields, &PARENT_TAG); @@ -109,6 +113,7 @@ impl From for ParsedEnvelope { }), content_encoding, content_type, + delegate, duplicate_field, incomplete_field, metadata, @@ -455,7 +460,7 @@ mod tests { b"ord", &[1], b"text/plain;charset=utf-8", - &[11], + &NOP_TAG, b"bar", &[], b"ord", @@ -785,7 +790,7 @@ mod tests { #[test] fn unknown_odd_fields_are_ignored() { assert_eq!( - parse(&[envelope(&[b"ord", &[11], &[0]])]), + parse(&[envelope(&[b"ord", &NOP_TAG, &[0]])]), vec![ParsedEnvelope { payload: Inscription::default(), ..Default::default() diff --git a/src/index.rs b/src/index.rs index 13097affd8..97ac0c027c 100644 --- a/src/index.rs +++ b/src/index.rs @@ -4781,7 +4781,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4828,7 +4828,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4882,7 +4882,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4936,7 +4936,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4992,7 +4992,7 @@ mod tests { body: Some("hello".into()), parent: Some( parent_inscription_id - .parent_value() + .value() .into_iter() .chain(iter::once(0)) .collect(), @@ -5177,7 +5177,7 @@ mod tests { let child_inscription = Inscription { content_type: Some("text/plain".into()), body: Some("pointer-child".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), pointer: Some(0u64.to_le_bytes().to_vec()), ..Default::default() }; diff --git a/src/inscription.rs b/src/inscription.rs index be92ca50dc..714ae0389d 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -19,6 +19,7 @@ pub struct Inscription { pub body: Option>, pub content_encoding: Option>, pub content_type: Option>, + pub delegate: Option>, pub duplicate_field: bool, pub incomplete_field: bool, pub metadata: Option>, @@ -102,7 +103,7 @@ impl Inscription { content_encoding, metadata, metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), - parent: parent.map(|id| id.parent_value()), + parent: parent.map(|id| id.value()), pointer: pointer.map(Self::pointer_value), ..Default::default() }) @@ -151,6 +152,12 @@ impl Inscription { .push_slice(PushBytesBuf::try_from(parent).unwrap()); } + if let Some(delegate) = self.delegate.clone() { + builder = builder + .push_slice(envelope::DELEGATE_TAG) + .push_slice(PushBytesBuf::try_from(delegate).unwrap()); + } + if let Some(pointer) = self.pointer.clone() { builder = builder .push_slice(envelope::POINTER_TAG) @@ -197,6 +204,41 @@ impl Inscription { Inscription::append_batch_reveal_script_to_builder(inscriptions, builder).into_script() } + fn inscription_id_field(field: &Option>) -> Option { + let value = field.as_ref()?; + + if value.len() < Txid::LEN { + return None; + } + + if value.len() > Txid::LEN + 4 { + return None; + } + + let (txid, index) = value.split_at(Txid::LEN); + + if let Some(last) = index.last() { + // Accept fixed length encoding with 4 bytes (with potential trailing zeroes) + // or variable length (no trailing zeroes) + if index.len() != 4 && *last == 0 { + return None; + } + } + + let txid = Txid::from_slice(txid).unwrap(); + + let index = [ + index.first().copied().unwrap_or(0), + index.get(1).copied().unwrap_or(0), + index.get(2).copied().unwrap_or(0), + index.get(3).copied().unwrap_or(0), + ]; + + let index = u32::from_le_bytes(index); + + Some(InscriptionId { txid, index }) + } + pub(crate) fn media(&self) -> Media { if self.body.is_none() { return Media::Unknown; @@ -229,6 +271,10 @@ impl Inscription { HeaderValue::from_str(str::from_utf8(self.content_encoding.as_ref()?).unwrap_or_default()).ok() } + pub(crate) fn delegate(&self) -> Option { + Self::inscription_id_field(&self.delegate) + } + pub(crate) fn metadata(&self) -> Option { ciborium::from_reader(Cursor::new(self.metadata.as_ref()?)).ok() } @@ -238,38 +284,7 @@ impl Inscription { } pub(crate) fn parent(&self) -> Option { - let value = self.parent.as_ref()?; - - if value.len() < Txid::LEN { - return None; - } - - if value.len() > Txid::LEN + 4 { - return None; - } - - let (txid, index) = value.split_at(Txid::LEN); - - if let Some(last) = index.last() { - // Accept fixed length encoding with 4 bytes (with potential trailing zeroes) - // or variable length (no trailing zeroes) - if index.len() != 4 && *last == 0 { - return None; - } - } - - let txid = Txid::from_slice(txid).unwrap(); - - let index = [ - index.first().copied().unwrap_or(0), - index.get(1).copied().unwrap_or(0), - index.get(2).copied().unwrap_or(0), - index.get(3).copied().unwrap_or(0), - ]; - - let index = u32::from_le_bytes(index); - - Some(InscriptionId { txid, index }) + Self::inscription_id_field(&self.parent) } pub(crate) fn pointer(&self) -> Option { @@ -505,6 +520,26 @@ mod tests { .is_none()); } + #[test] + fn inscription_delegate_txid_is_deserialized_correctly() { + assert_eq!( + Inscription { + delegate: Some(vec![ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]), + ..Default::default() + } + .delegate() + .unwrap() + .txid, + "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100" + .parse() + .unwrap() + ); + } + #[test] fn inscription_parent_txid_is_deserialized_correctly() { assert_eq!( diff --git a/src/inscription_id.rs b/src/inscription_id.rs index c51eddb172..5f3f70e225 100644 --- a/src/inscription_id.rs +++ b/src/inscription_id.rs @@ -16,7 +16,7 @@ impl Default for InscriptionId { } impl InscriptionId { - pub(crate) fn parent_value(self) -> Vec { + pub(crate) fn value(self) -> Vec { let index = self.index.to_le_bytes(); let mut index_slice = index.as_slice(); diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 51a4bfd090..4a6864f315 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1027,10 +1027,16 @@ impl Server { return Ok(PreviewUnknownHtml.into_response()); } - let inscription = index + let mut inscription = index .get_inscription_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + if let Some(delegate) = inscription.delegate() { + inscription = index + .get_inscription_by_id(delegate)? + .ok_or_not_found(|| format!("delegate {inscription_id}"))? + } + Ok( Self::content_response(inscription, accept_encoding, &server_config)? .ok_or_not_found(|| format!("inscription {inscription_id} content"))? @@ -1119,10 +1125,16 @@ impl Server { return Ok(PreviewUnknownHtml.into_response()); } - let inscription = index + let mut inscription = index .get_inscription_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + if let Some(delegate) = inscription.delegate() { + inscription = index + .get_inscription_by_id(delegate)? + .ok_or_not_found(|| format!("delegate {inscription_id}"))? + } + match inscription.media() { Media::Audio => Ok(PreviewAudioHtml { inscription_id }.into_response()), Media::Code(language) => Ok( @@ -3916,7 +3928,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_id.parent_value()), + parent: Some(parent_id.value()), ..Default::default() } .to_witness(), @@ -4068,7 +4080,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4141,7 +4153,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4188,7 +4200,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4200,7 +4212,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4212,7 +4224,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4224,7 +4236,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4236,7 +4248,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), ..Default::default() } .to_witness(), @@ -4859,7 +4871,7 @@ next builder = Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.parent_value()), + parent: Some(parent_inscription_id.value()), unrecognized_even_field: false, ..Default::default() } @@ -4946,4 +4958,61 @@ next "inscription 0 not found", ); } + + #[test] + fn delegate() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(1); + + let delegate = Inscription { + content_type: Some("text/html".into()), + body: Some("foo".into()), + ..Default::default() + }; + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, delegate.to_witness())], + ..Default::default() + }); + + let delegate = InscriptionId { txid, index: 0 }; + + server.mine_blocks(1); + + let inscription = Inscription { + delegate: Some(delegate.value()), + ..Default::default() + }; + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription.to_witness())], + ..Default::default() + }); + + server.mine_blocks(1); + + let id = InscriptionId { txid, index: 0 }; + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription 1

.* +
+
id
+
{id}
+ .* +
delegate
+
{delegate}
+ .* +
.*" + ) + .unindent(), + ); + + server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo"); + + server.assert_response(format!("/preview/{id}"), StatusCode::OK, "foo"); + } } diff --git a/src/test.rs b/src/test.rs index a306c19bba..49f8c4fed0 100644 --- a/src/test.rs +++ b/src/test.rs @@ -120,7 +120,7 @@ pub(crate) struct InscriptionTemplate { impl From for Inscription { fn from(template: InscriptionTemplate) -> Self { Self { - parent: template.parent.map(|id| id.parent_value()), + parent: template.parent.map(|id| id.value()), pointer: template.pointer.map(Inscription::pointer_value), ..Default::default() } diff --git a/templates/inscription.html b/templates/inscription.html index 72917c36a4..8294498cf2 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -64,14 +64,20 @@

Inscription {{ self.inscription_number }}

metaprotocol
{{ metaprotocol }}
%% } -%% if let Some(content_length) = self.inscription.content_length() { +%% if self.inscription.content_length().is_some() || self.inscription.delegate().is_some() { +%% if let Some(delegate) = self.inscription.delegate() { +
delegate
+
{{ delegate }}
+%% }
preview
link
content
link
+%% if let Some(content_length) = self.inscription.content_length() {
content length
{{ content_length }} bytes
%% } +%% } %% if let Some(content_type) = self.inscription.content_type() {
content type
{{ content_type }}