Skip to content

Commit

Permalink
Allow inscriptions to include CBOR metadata (ordinals#2421)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey committed Oct 3, 2023
1 parent be516f8 commit 26a755f
Show file tree
Hide file tree
Showing 13 changed files with 590 additions and 24 deletions.
35 changes: 35 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ bip39 = "2.0.0"
bitcoin = { version = "0.30.0", features = ["rand"] }
boilerplate = { version = "1.0.0", features = ["axum"] }
chrono = "0.4.19"
ciborium = "0.2.1"
clap = { version = "4.4.2", features = ["derive"] }
ctrlc = { version = "3.2.1", features = ["termination"] }
derive_more = "0.99.17"
Expand All @@ -49,7 +50,7 @@ rust-embed = "8.0.0"
rustls = "0.21.1"
rustls-acme = { version = "0.7.1", features = ["axum"] }
serde = { version = "1.0.137", features = ["derive"] }
serde_json = { version = "1.0.81" }
serde_json = { version = "1.0.81", features = ["preserve_order"] }
serde_yaml = "0.9.17"
sysinfo = "0.29.2"
tempfile = "3.2.0"
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [Overview](overview.md)
- [Digital Artifacts](digital-artifacts.md)
- [Inscriptions](inscriptions.md)
- [Metadata](inscriptions/metadata.md)
- [Provenance](inscriptions/provenance.md)
- [Recursion](inscriptions/recursion.md)
- [FAQ](faq.md)
Expand Down
86 changes: 86 additions & 0 deletions docs/src/inscriptions/metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
Metadata
========

Inscriptions may include [CBOR](https://cbor.io/) metadata, stored as data
pushes in fields with tag `5`. Since data pushes are limited to 520 bytes,
metadata longer than 520 bytes must be split into multiple tag `5` fields,
which will then be concatenated before decoding.

Metadata is human readable, and all metadata will be displayed to the user with
its inscription. Inscribers are encouraged to consider how metadata will be
displayed, and make metadata concise and attractive.

Metadata is rendered to HTML for display as follows:

- `null`, `true`, `false`, numbers, floats, and strings are rendered as plain
text.
- Byte strings are rendered as uppercase hexadecimal.
- Arrays are rendered as `<ul>` tags, with every element wrapped in `<li>`
tags.
- Maps are rendered as `<dl>` tags, with every key wrapped in `<dt>` tags, and
every value wrapped in `<dd>` tags.
- Tags are rendered as the tag , enclosed in a `<sup>` tag, followed by the
value.

CBOR is a complex spec with many different data types, and multiple ways of
representing the same data. Exotic data types, such as tags, floats, and
bignums, and encoding such as indefinite values, may fail to display correctly
or at all. Contributions to `ord` to remedy this are welcome.

Example
-------

Since CBOR is not human readable, in these examples it is represented as JSON.
Keep in mind that this is *only* for these examples, and JSON metadata will
*not* be displayed correctly.

The metadata `{"foo":"bar","baz":[null,true,false,0]}` would be included in an inscription as:

```
OP_FALSE
OP_IF
...
OP_PUSH 0x05 OP_PUSH '{"foo":"bar","baz":[null,true,false,0]}'
...
OP_ENDIF
```

And rendered as:

```
<dl>
...
<dt>metadata</dt>
<dd>
<dl>
<dt>foo</dt>
<dd>bar</dd>
<dt>baz</dt>
<dd>
<ul>
<li>null</li>
<li>true</li>
<li>false</li>
<li>0</li>
</ul>
</dd>
</dl>
</dd>
...
</dl>
```

Metadata longer than 520 bytes must be split into multiple fields:

```
OP_FALSE
OP_IF
...
OP_PUSH 0x05 OP_PUSH '{"very":"long","metadata":'
OP_PUSH 0x05 OP_PUSH '"is","finally":"done"}'
...
OP_ENDIF
```

Which would then be concatinated into
`{"very":"long","metadata":"is","finally":"done"}`.
47 changes: 47 additions & 0 deletions src/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(crate) const PROTOCOL_ID: [u8; 3] = *b"ord";
pub(crate) const BODY_TAG: [u8; 0] = [];
pub(crate) const CONTENT_TYPE_TAG: [u8; 1] = [1];
pub(crate) const PARENT_TAG: [u8; 1] = [3];
pub(crate) const METADATA_TAG: [u8; 1] = [5];
pub(crate) const METAPROTOCOL_TAG: [u8; 1] = [7];

type Result<T> = std::result::Result<T, script::Error>;
Expand All @@ -34,6 +35,19 @@ fn remove_field(fields: &mut BTreeMap<&[u8], Vec<&[u8]>>, field: &[u8]) -> Optio
}
}

fn remove_and_concatenate_field(
fields: &mut BTreeMap<&[u8], Vec<&[u8]>>,
field: &[u8],
) -> Option<Vec<u8>> {
let value = fields.remove(field)?;

if value.is_empty() {
None
} else {
Some(value.into_iter().flatten().cloned().collect())
}
}

impl From<RawEnvelope> for ParsedEnvelope {
fn from(envelope: RawEnvelope) -> Self {
let body = envelope
Expand All @@ -58,6 +72,7 @@ impl From<RawEnvelope> for ParsedEnvelope {
let content_type = remove_field(&mut fields, &CONTENT_TYPE_TAG);
let parent = remove_field(&mut fields, &PARENT_TAG);
let metaprotocol = remove_field(&mut fields, &METAPROTOCOL_TAG);
let metadata = remove_and_concatenate_field(&mut fields, &METADATA_TAG);

let unrecognized_even_field = fields
.keys()
Expand All @@ -78,6 +93,7 @@ impl From<RawEnvelope> for ParsedEnvelope {
duplicate_field,
incomplete_field,
metaprotocol,
metadata,
},
input: envelope.input,
offset: envelope.offset,
Expand Down Expand Up @@ -689,4 +705,35 @@ mod tests {
}],
);
}

#[test]
fn metadata_is_parsed_correctly() {
assert_eq!(
parse(&[envelope(&[b"ord", &[5], &[]])]),
vec![ParsedEnvelope {
payload: Inscription {
metadata: Some(vec![]),
..Default::default()
},
input: 0,
offset: 0,
}]
);
}

#[test]
fn metadata_is_parsed_correctly_from_chunks() {
assert_eq!(
parse(&[envelope(&[b"ord", &[5], &[0], &[5], &[1]])]),
vec![ParsedEnvelope {
payload: Inscription {
metadata: Some(vec![0, 1]),
duplicate_field: true,
..Default::default()
},
input: 0,
offset: 0,
}]
);
}
}
Loading

0 comments on commit 26a755f

Please sign in to comment.