Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delegation #2912

Merged
merged 11 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
[parent](parent.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
Loading