- Name: message-id-and-threading
- Authors: Daniel Bluhm daniel.bluhm@sovrin.org, Sam Curren (sam@sovin.org), Daniel Hardman (daniel.hardman@evernym.com)
- Start Date: 2018-08-03
- PR:
- Jira Issue:
Definition of the message id and threading decorators.
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.
Message IDs are specified with the @id attribute. 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.
- Not to exceed 64 characters
- Sufficiently Unique
- UUID recommended
{
"@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.
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",
"seqnum": 2,
"lrec": 1
},
"msg": "this is my message"
}
A thread object has the following fields discussed below:
thid
: The ID of the message that serves as the thread start.pthid
: An optional parentthid
. Used when branching or nesting a new interaction off of an existing one.seqnum
: A message sequence number unique to thethid
and sender.lseqnum
: A reference to the last message the sender received from the receiver (Missing if it is the first message in an interaction).
Because multiple interactions can happen simultaneously, it's important to
differentiate between them. This is done with a Thread ID or thid
.
The Thread ID is the Message ID (@id
) of the first message in the thread. The
first message may not declare the @thread
attribute block, but carries an
implicit thid
of its own @id
.
Each message in an interaction needs a way to be uniquely identified. This is
done with Sequence Num (seqnum
). The first message from each party has a
seqnum
of 0, the second message sent from each party is 1, and so forth. A
message is uniquely identified in an interaction by its thid
, the sender
DID and/or key, and the seqnum
. The combination of those three parts would
be a way to uniquely identify a message.
In an interaction, it may be useful to for the recipient of a message to know if their
last message was received. A Last Received or lrec
value addresses this need, and
could be included as a best practice to help detect missing messages.
In the example above, lrec
is a single integer value that gives the sequence number
of the last message that the sender received before composing their response.
This is the most common form of lrec
, and would be expected in pairwise
interactions. However, n-wise interactions are possible (e.g., in a doctor ~ hospital ~ patient n-wise
relationship), and 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). Thus, lrec
supports an extended notation where the value is a struct that operates as a form of vector clock.
In the extended form, lrec
is a struct and each key/value pair in the struct is an actor
/seqnum
, where actor
is a DID or a key for an agent:
"lrec": {"did:sov:abcxyz":1, "did:sov:defghi":14}
In the above example, the lrec
fragment makes a claim about the last sequence number
that was seen by the sender from did:sov:abcxyz
and did:sov:defghi
. The sender of
this fragment is presumably some other DID, implying that 3 parties are participating.
If there are more than 3 parties
in the interaction, the parties unnamed in lrec
have an undefined value for lrec
.
This is NOT the same as saying that they have made no observable contribution to the
thread. To make that claim, use the special lrec
value -1
, as in:
"lrec": {"did:sov:abcxyz":1, "did:sov:defghi":14, "did:sov:jklmno":-1}
Parties processing lrec
should support either the short form or the extended form
in all cases; it is always legal to use the extended array form even in a
pairwise interaction.
When lrec
is omitted, the value of lrec
for the thread is undefined.
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,seqnum
=0. - Bob responds with a CRED_REQUEST,
@id
=<uuid2>,thid
=98fd8d72-80f6-4419-abc2-c65ea39d0f38,seqnum
=0,lrec
=0. - Alice sends a CRED,
@id
=<uuid3>,thid
=98fd8d72-80f6-4419-abc2-c65ea39d0f38,seqnum
=1,lrec
=0. - Bob responds with an ACK,
@id
=<uuid4>,thid
=98fd8d72-80f6-4419-abc2-c65ea39d0f38,seqnum
=1,lrec
=1.
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.
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,seqnum
=0. - Bob responds with a CRED_REQUEST,
@id
=<uuid2>,thid
=98fd8d72-80f6-4419-abc2-c65ea39d0f38,seqnum
=0,lrec
=0. - Alice sends a PROOF_REQUEST,
@id
=<uuid3>,pthid
=98fd8d72-80f6-4419-abc2-c65ea39d0f38,seqnum
=0. - Bob sends a PROOF,
@id
=<uuid4>,thid
=<uuid3>,seqnum
=0,lrec
=0. - Alice sends a CRED,
@id
=<uuid5>,thid
=98fd8d72-80f6-4419-abc2-c65ea39d0f38,seqnum
=1,lrec
=0. - Bob responds with an ACK,
@id
=<uuid6>,thid
=98fd8d72-80f6-4419-abc2-c65ea39d0f38,seqnum
=1,lrec
=1.
All of the steps are the same, except the two bolded steps that are part of a nested interaction.
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>,
"seqnum": 0
}
Messages that contain a @thread
block with a thid
different from the outer message id, but no sequence numbers is considered an implicit reply. Implicit replies have a seqnum
of 0
and a lrec
of 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>"
"seqnum": 0,
"lrec": 0
}
}
- Message Packaging document from Sovrin Foundation Protocol Repo
- Very brief summary of discussion from Agent Summit on Decorators
Why should we not do this?
- Implement threading for each message type that needs it.
If you're aware of relevant prior-art, please add it here.
- 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?