Skip to content

Aries RFC 0008: Message ID and Threading

Summary

Definition of the message @id field and the ~thread decorator.

Motivation

Referring to messages is useful in many interactions. A standard method of adding a message ID promotes good patterns in message families. When multiple messages are coordinated in a message flow, the threading pattern helps avoid having to re-roll the same spec for each message family that needs it.

Tutorial

Message IDs

Message IDs are specified with the @id attribute, which comes from JSON-LD. The sender of the message is responsible for creating the message ID, and any message can be identified by the combination of the sender and the message ID. Message IDs should be considered to be opaque identifiers by any recipients.

Message ID Requirements

  • A short stream of characters matching regex [-_./a-ZA-Z0-9]{8,64} (Note the special semantics of a dotted suffix on IDs, as described in the message tracing HIPE proposal)
  • Should be compared case-sensitive (no case folding)
  • Sufficiently unique
  • UUID recommended

Example

{
    "@type": "did:example:12345...;spec/example_family/1.0/example_type",
    "@id": "98fd8d72-80f6-4419-abc2-c65ea39d0f38",
    "example_attribute": "stuff"
}

The following was pulled from this document written by Daniel Hardman and stored in the Sovrin Foundation's protocol repository.

Threaded Messages

Message threading will be implemented as a decorator to messages, for example:

{
    "@type": "did:example:12345...;spec/example_family/1.0/example_type",
    "@id": "98fd8d72-80f6-4419-abc2-c65ea39d0f38",
    "~thread": {
        "thid": "98fd8d72-80f6-4419-abc2-c65ea39d0f38",
        "pthid": "1e513ad4-48c9-444e-9e7e-5b8b45c5e325",
        "sender_order": 3,
        "received_orders": {"did:sov:abcxyz":1},
        "goal_code": "aries.vc.issue"
    },
    "example_attribute": "example_value"
}

The ~thread decorator is generally required on any type of response, since this is what connects it with the original request.

While not recommended, the initial message of a new protocol instance MAY have an empty ({}) ~thread item. Aries agents receiving a message with an empty ~thread item MUST gracefully handle such a message.

Thread object

A thread object has the following fields discussed below:

  • thid: The ID of the message that serves as the thread start.
  • pthid: An optional parent thid. Used when branching or nesting a new interaction off of an existing one.
  • sender_order: A number that tells where this message fits in the sequence of all messages that the current sender has contributed to this thread.
  • received_orders: Reports the highest sender_order value that the sender has seen from other sender(s) on the thread. (This value is often missing if it is the first message in an interaction, but should be used otherwise, as it provides an implicit ACK.)
  • goal_code: Optional. See RFC 0519: Goal Codes.

Thread ID (thid)

Because multiple interactions can happen simultaneously, it's important to differentiate between them. This is done with a Thread ID or thid.

If the Thread object is defined and a thid is given, the Thread ID is the value given there. But if the Thread object is not defined in a message, the Thread ID is implicitly defined as the Message ID (@id) of the given message and that message is the first message of a new thread.

Sender Order (sender_order)

It is desirable to know how messages within a thread should be ordered. However, it is very difficult to know with confidence the absolute ordering of events scattered across a distributed system. Alice and Bob may each send a message before receiving the other's response, but be unsure whether their message was composed before the other's. Timestamping cannot resolve an impasse. Therefore, there is no unified absolute ordering of all messages within a thread--but there is an ordering of all messages emitted by a each participant.

In a given thread, the first message from each party has a sender_order value of 0, the second message sent from each party has a sender_order value of 1, and so forth. Note that both Alice and Bob use 0 and 1, without regard to whether the other party may be known to have used them. This gives a strong ordering with respect to each party's messages, and it means that any message can be uniquely identified in an interaction by its thid, the sender DID and/or key, and the sender_order.

Received Orders (received_orders)

In an interaction, it may be useful for the recipient of a message to know if their last message was received. A received_orders value addresses this need, and could be included as a best practice to help detect missing messages.

In the example above, if Alice is the sender, and Bob is identified by did:sov:abcxyz, then Alice is saying, "Here's my message with index 3 (sender_order=3), and I'm sending it in response to your message 1 (received_orders: {<bob's DID>: 1}. Apparently Alice has been more chatty than Bob in this exchange.

The received_orders field is plural to acknowledge the possibility of multiple parties. In pairwise interactions, this may seem odd. However, n-wise interactions are possible (e.g., in a doctor ~ hospital ~ patient n-wise relationship). Even in pairwise, multiple agents on either side may introduce other actors. This may happen even if an interaction is designed to be 2-party (e.g., an intermediate party emits an error unexpectedly).

In an interaction with more parties, the received_orders object has a key/value pair for each actor/sender_order, where actor is a DID or a key for an agent:

"received_orders": {"did:sov:abcxyz":1, "did:sov:defghi":14}

Here, the received_orders fragment makes a claim about the last sender_order that the sender observed from did:sov:abcxyz and did:sov:defghi. The sender of this fragment is presumably some other DID, implying that 3 parties are participating. Any parties unnamed in received_orders have an undefined value for received_orders. This is NOT the same as saying that they have made no observable contribution to the thread. To make that claim, use the special value -1, as in:

"received_orders": {"did:sov:abcxyz":1, "did:sov:defghi":14, "did:sov:jklmno":-1}
Example

As an example, Alice is an issuer and she offers a credential to Bob.

  • Alice sends a CRED_OFFER as the start of a new thread, @id=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=0.
  • Bob responds with a CRED_REQUEST, @id=<uuid2>, thid=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=0, received_orders:{alice:0}.
  • Alice sends a CRED, @id=<uuid3>, thid=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=1, received_orders:{bob:0}.
  • Bob responds with an ACK, @id=<uuid4>, thid=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=1, received_orders:{alice:1}.

Nested interactions (Parent Thread ID or pthid)

Sometimes there are interactions that need to occur with the same party, while an existing interaction is in-flight.

When an interaction is nested within another, the initiator of a new interaction can include a Parent Thread ID (pthid). This signals to the other party that this is a thread that is branching off of an existing interaction.

Nested Example

As before, Alice is an issuer and she offers a credential to Bob. This time, she wants a bit more information before she is comfortable providing a credential.

  • Alice sends a CRED_OFFER as the start of a new thread, @id=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=0.
  • Bob responds with a CRED_REQUEST, @id=<uuid2>, thid=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=0, received_orders:{alice:0}.
  • Alice sends a PROOF_REQUEST, @id=<uuid3>, pthid=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=0. Note the subthread, the parent thread ID, and the reset sender_order value.
  • Bob sends a PROOF, @id=<uuid4>, thid=<uuid3>,sender_order=0, received_orders:{alice:0}.
  • Alice sends a CRED, @id=<uuid5>, thid=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=1, received_orders:{bob:0}.
  • Bob responds with an ACK, @id=<uuid6>, thid=98fd8d72-80f6-4419-abc2-c65ea39d0f38, sender_order=1, received_orders:{alice:1}.

All of the steps are the same, except the two bolded steps that are part of a nested interaction.

Implicit Threads

Threads reference a Message ID as the origin of the thread. This allows any message to be the start of a thread, even if not originally intended. Any message without an explicit ~thread attribute can be considered to have the following ~thread attribute implicitly present.

"~thread": {
    "thid": <same as @id of the outer message>,
    "sender_order": 0
}

Implicit Replies

A message that contains a ~thread block with a thid different from the outer message @id, but no sender_order is considered an implicit reply. Implicit replies have a sender_order of 0 and an received_orders:{other:0}. Implicit replies should only be used when a further message thread is not anticipated. When further messages in the thread are expected, a full regular ~thread block should be used.

Example Message with am Implicit Reply:

{
    "@id": "<@id of outer message>",
    "~thread": {
        "thid": "<different than @id of outer message>"
    }
}
Effective Message with defaults in place:
{
    "@id": "<@id of outer message>",
    "~thread": {
        "thid": "<different than @id of outer message>"
        "sender_order": 0,
        "received_orders": { "DID of sender":0 }
    }
}

Reference

Drawbacks

Why should we not do this?

Rationale and alternatives

  • Implement threading for each message type that needs it.

Prior art

If you're aware of relevant prior-art, please add it here.

Unresolved questions

  • Using a wrapping method for threading has been discussed but most seemed in favor of the annotated method. Any remaining arguments to be made in favor of the wrapping method?

Implementations

The following lists the implementations (if any) of this RFC. Please do a pull request to add your implementation. If the implementation is open source, include a link to the repo or to the implementation within the repo. Please be consistent in the "Name" field so that a mechanical processing of the RFCs can generate a list of all RFCs supported by an Aries implementation.

Name / Link Implementation Notes
Aries Cloud Agent - Python Contributed by the government of British Columbia.
Aries Static Agent - Python Useful for cron jobs and other simple, automated use cases.
Connect.Me Free mobile app from Evernym. Installed via app store on iOS and Android.
Verity Commercially licensed enterprise agent, SaaS or on-prem.
Aries Protocol Test Suite