Skip to content

Commit

Permalink
Allow inscriptions to nominate a delegate (ordinals#2912)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey committed Dec 27, 2023
1 parent ee2d117 commit 2db64cb
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 59 deletions.
3 changes: 2 additions & 1 deletion docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions docs/src/inscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions docs/src/inscriptions/delegate.md
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 7 additions & 2 deletions src/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = std::result::Result<T, script::Error>;
type RawEnvelope = Envelope<Vec<Vec<u8>>>;
Expand Down Expand Up @@ -89,6 +92,7 @@ impl From<RawEnvelope> 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);
Expand All @@ -109,6 +113,7 @@ impl From<RawEnvelope> for ParsedEnvelope {
}),
content_encoding,
content_type,
delegate,
duplicate_field,
incomplete_field,
metadata,
Expand Down Expand Up @@ -455,7 +460,7 @@ mod tests {
b"ord",
&[1],
b"text/plain;charset=utf-8",
&[11],
&NOP_TAG,
b"bar",
&[],
b"ord",
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 6 additions & 6 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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()
};
Expand Down
101 changes: 68 additions & 33 deletions src/inscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct Inscription {
pub body: Option<Vec<u8>>,
pub content_encoding: Option<Vec<u8>>,
pub content_type: Option<Vec<u8>>,
pub delegate: Option<Vec<u8>>,
pub duplicate_field: bool,
pub incomplete_field: bool,
pub metadata: Option<Vec<u8>>,
Expand Down Expand Up @@ -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()
})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -197,6 +204,41 @@ impl Inscription {
Inscription::append_batch_reveal_script_to_builder(inscriptions, builder).into_script()
}

fn inscription_id_field(field: &Option<Vec<u8>>) -> Option<InscriptionId> {
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;
Expand Down Expand Up @@ -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<InscriptionId> {
Self::inscription_id_field(&self.delegate)
}

pub(crate) fn metadata(&self) -> Option<Value> {
ciborium::from_reader(Cursor::new(self.metadata.as_ref()?)).ok()
}
Expand All @@ -238,38 +284,7 @@ impl Inscription {
}

pub(crate) fn parent(&self) -> Option<InscriptionId> {
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<u64> {
Expand Down Expand Up @@ -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!(
Expand Down
2 changes: 1 addition & 1 deletion src/inscription_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl Default for InscriptionId {
}

impl InscriptionId {
pub(crate) fn parent_value(self) -> Vec<u8> {
pub(crate) fn value(self) -> Vec<u8> {
let index = self.index.to_le_bytes();
let mut index_slice = index.as_slice();

Expand Down
Loading

0 comments on commit 2db64cb

Please sign in to comment.