Skip to content

Conversation

@joshdoman
Copy link

@joshdoman joshdoman commented Jul 27, 2025

Disclaimer: This PR was created during the PlebFi hackathon. It would benefit from additional tests.

TLDR

This PR adds support for indexing inscriptions in the annex. It conforms to Peter Todd's proposal to begin the annex with $0x00$, and it uses an efficient TLV format to support multiple protocol tags and multiple uses of a protocol in a single annex.

Encoding

TLV is implemented using the bitcoin-embed rust library. This library can encode an arbitrary number of tags and messages, supporting up to 2^127-1 tags. To minimize bytes, tags and message lengths are variable-length encoded using LEB128, and repeating consecutive tags are encoded as zero. In addition, the final message does not contain an explicit length. Instead, tags are encoded as 2 * tag + (1 if terminal tag else 0), explicitly indicating the final tag. More details can be found here.

ord messages in the annex use a protocol tag of $0x37$, which equates to $0x6F$ when there is a single message in the annex. This was chosen to make the starting byte the same as that of an inscription in a witness envelope.

Within an ord message in the annex, bytes are formatted as [tag1][length1][data1][tag2][length2][data2]...[0x00][body_data]. Lengths are variable-length encoded as LEB128 integers, with a maximum value of 2^32 - 1. Malformed ord messages are skipped but do not invalidate the rest of the annex.

Backward compatibility

Activation of this PR is gated behind a hardcoded block height (950,000 on Mainnet) to provide sufficient time for users to upgrade. Failure to upgrade will not change past or future inscription IDs or result in invalid inscriptions, but it will change future inscription numbering.

To prevent changes to past or future inscription IDs, ord annex messages are indexed after script envelopes. This ensures that the index number of an inscription made in a script envelope is unaffected by the presence of an annex. An inscription made in an annex is assigned an offset as though it were an additional envelope in the input.

Usage

This PR updates ord so that it indexes inscriptions made in the annex. It also adds a new annex subcommand so that users can generate inscription-containing annexes on the command line.

Creating an annex

wallet annex --batch batch.yaml

This outputs an annex containing the inscriptions defined in the batch file. As an example, inscribing the text "Hello World" with metadata "123" and metaprotocol "foo" produces the annex:

50006f0118746578742f706c61696e3b636861727365743d7574662d380703666f6f02000502187b0048656c6c6f20576f726c64

Creating a transaction with an annex

Unfortunately, Bitcoin Core does not support signing annexes, so this PR does not support annex-based inscribing via the ord wallet.

Users who wish to produce a transaction containing an annex will need to do so manually until ord is able to add support.

let mut invalid = message.body.len() == 1;
while index + 1 < message.body.len() {
if message.body[index] == BODY_ANNEX_TAG {
payload.push(BODY_TAG.to_vec());
Copy link

@stutxo stutxo Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is something i noticed when working on a fork that also uses bitcoin_embed in a similar way to this. I haven't tested with this code directly, but anyway i thought i would mention it.

As far as i know, if a valid body is passed but has zero data then it should still make a valid annex inscription? I think that scenario is not being handled here

for example if you were to setup a test for the annex which is similar to inscriptions::envelope::tests::valid_body_in_zero_pushes, then it would fail.

i have to add the changes below to get it to pass this test in my control block protocol fork, which is also using the bitcoin_embed crate

     while index < message.body.len() {
        if message.body[index] == BODY_ANNEX_TAG {
          let body = message.body.get(index + 1..).unwrap_or(&[]).to_vec();

          payload.push(BODY_TAG.to_vec());
          payload.push(body);
          break;
        }

index += size + length + 1;
}

if invalid {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this check i dont think the incomplete_field flag ever gets set. not sure if that is an issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants