SERVICE-DECLARATION-PROTOCOL
| Field | Value |
|---|---|
| Name | Service Declaration Protocol |
| Slug | 87 |
| Status | raw |
| Category | Standards Track |
| Editor | Marcin Pawlowski [email protected] |
| Contributors | Mehmet Gonen [email protected], Daniel Sanchez Quiros [email protected], Álvaro Castro-Castilla [email protected], Thomas Lavaur [email protected], Gusto Bacvinka [email protected], David Rusu [email protected], Filip Dimitrijevic [email protected] |
Timeline
- 2026-10-27 —
6b84187— [RFC] SDP: Identify declarations by service andzk_id(#407) - 2026-09-11 —
7c2ec59— [RFC] The Last Served Epoch Is Rewardable (#435) - 2026-09-03 —
e2e9e0f— [RFC] The Epochsactiveandwithdraw_atRecord (#422) - 2026-09-01 —
74f6f93— [RFC] Per-Servicezk_idUniqueness Is Normative for Reward Distribution (#406) - 2026-08-31 —
3cac48f— docs(blockchain): rename locked notes to service notes (#423) - 2026-08-26 —
5e42374— docs(blockchain): specify uint32 for epoch number (#420) - 2026-08-24 —
8b6f4f4— docs(blockchain): use one canonical encoding for ServiceType and Locator (#402) - 2026-07-30 —
87d9ae5— docs(blockchain): length-prefix the locators in the declaration_id preimage (#386) - 2026-07-13 —
d064449— RFC-Bedrock-SDP: Per-Service Uniqueness ofprovider_idandzk_id(#371) - 2026-07-03 —
709cf7f— Bedrock-RFC: Remove Concept of a Session (#365) - 2026-05-28 —
d45eed2— Chore: mirror blochain specs into github/mdbook (#347) - 2026-05-18 —
58b5698— chore(blockchain): migrate contributor emails to @logos.co (#338) - 2026-01-19 —
f24e567— Chore/updates mdbook (#262) - 2026-01-16 —
89f2ea8— Chore/mdbook updates (#258)
Revision History
| Version | Changes | Date |
|---|---|---|
| 1.0.0 | Initial revision. | 2026-04-09 |
| 1.1.0 | [RFC] Remove Concept of a Session | 2026-06-22 |
| 1.2.0 | [RFC] Per-service uniqueness of provider_id and zk_id | 2026-07-08 |
| 1.3.0 | Length-prefix the locators list in the declaration_id preimage | 2026-07-30 |
| 1.4.0 | [RFC] One canonical encoding for ServiceType and Locator | 2026-08-14 |
| 1.4.1 | Replaced the uint64 width given to the epoch fields with a reference to EpochNumber, which is 32 bits | 2026-08-25 |
| 1.4.2 | Renamed locked notes into service notes: locked_note_id becomes service_note_id in the declaration and withdraw messages and in DeclarationInfo | 2026-08-27 |
| 1.4.3 | Identifier uniqueness covers every stored declaration, not only activated ones, matching the implementation | 2026-09-01 |
| 1.5.0 | Defined active as the epoch of the block that contained the latest accepted active message, initialised to created + 2, and withdraw_at as the epoch at which the node stops providing the service, matching the implementation. Added the participant-set exclusion rule and Message Timing | 2026-09-02 |
| 1.6.0 | Declarations are removed at withdraw_at + 1, one epoch after the node stops, making the last served epoch rewardable | 2026-09-03 |
| 1.7.0 | [RFC] The declaration_id is the hash of the service and the zk_id; a declaration covers one service, a note backs one declaration per service, and a nonce carries the created epoch of its declaration | 2026-10-01 |
Introduction
This document defines a mechanism enabling validators to declare their participation in specific protocols that require a known and agreed-upon list of participants. One example of this is the Blend Network. We create a single repository of identifiers which is used to establish secure communication between validators and provide services. Before being admitted to the repository, the validator proves that it locked at least a minimum stake through a service note.
Requirements
The requirements for the protocol are defined as follows:
- A declaration must be backed by a confirmation that the sender of the declaration owns a certain value of the stake.
- A declaration is valid until it is withdrawn or is not actively used for a service-specific amount of time.
Overview
The SDP enables nodes to declare their eligibility to provide a specific service in the system, and withdraw their declarations.
Protocol
The protocol defines the following actions:
- Declare: A node sends a declaration that confirms its willingness to provide a specific service, which is confirmed by locking a stake above a certain threshold in a service note.
- Active: A node marks that its participation in the protocol is active according to the service-specific activity logic. This action enables the protocol to monitor the node’s activity. We utilize this as a non-intrusive differentiator of node activity. It is crucial to exclude inactive nodes from the active set, as it enhances the stability of services.
- Withdraw: A node withdraws its declaration and stops providing a service.
The logic of the protocol is straightforward.
- A node sends a declaration message for a specific service and proves it has a minimum stake.
- The declaration is registered on the Ledger, and the node can commence its service according to the service-specific service logic.
- After a service-specific service-providing time, the node confirms its activity.
- The node must confirm its activity with a service-specific minimum frequency; otherwise, its declaration is inactive.
- The node sends a withdrawal message. It provides the service for one more epoch. Its declaration is removed and its service note released once that epoch has been rewarded.
The protocol messages are subject to a finality that means messages become part of the immutable ledger after a delay. The delay at which it happens is defined by the consensus. Therefore, the protocol’s progress must be tracked from the perspective of the latest finalized block, not the tip of the chain. Otherwise, the protocol and services using it would need to handle chain reorganizations, which we must avoid due to their potential to break services. Hence, the services must use a snapshot from a fully finalized epoch:
finalized_epoch = current_epoch - 2. For more details about finalization, refer to Cryptarchia Protocol.
Construction
In this section, we present the main constructions of the protocol. First, we start with data definitions. Second, we describe the protocol actions. Finally, we present part of the Bedrock Mantle design responsible for storing and processing SDP-related messages and data.
Data
In this section, we discuss and define data types, messages, and their storage.
Service Types
We define the following service type:
BN: for Blend Network service.
class ServiceType(Enum):
BN=0 # Blend Network
A declaration can be generated for any of the services above. Any declaration that is not one of the above must be rejected. The number of services might grow in the future.
Each service type is assigned a one-byte discriminant, given by the enum value above. This byte is the canonical encoding of a ServiceType and is used wherever a ServiceType is serialized or hashed: the transaction wire form (Mantle Transaction Encoding), the declaration_id preimage (Declaration Storage), and the reward op_id preimage (Service Reward Distribution).
Minimum Stake
The minimum stake is a global value that defines the minimum stake a node must have to perform any service.
The MinStake is a structure that holds the value of the stake stake_threshold and the epoch, which is an epoch number at which the threshold was set; it is an EpochNumber.
class MinStake:
stake_threshold: StakeThreshold
epoch: EpochNumber
The stake_thresholds is a structure aggregating all defined MinStake values.
stake_thresholds: list[MinStake]
For more information on how the minimum stake is calculated, please refer to the [Analysis] Static Minimum Stake Estimation for Service Declaration Protocol.
Service Parameters
The service parameters structure defines the parameters set necessary for correctly handling interaction between the protocol and services. Each of the service types defined above must be mapped to a set of the following parameters:
inactivity_perioddefines the number of epochs after the epoch recorded inactivefor which the declaration remains active; after that, it is inactive and excluded from the service's participant set in the same way as a withdrawn declaration (Withdraw). A value of2 + ktolerateskconsecutive missed reports (Message Timing). Any value below 2 excludes every declaration.epochdefines the epoch number at which the parameter was set; it is anEpochNumber.
class ServiceParameters:
inactivity_period: NumberOfEpochs
epoch: EpochNumber
The parameters is a structure aggregating all defined ServiceParameters values.
parameters: list[ServiceParameters]
Snapshots
At the start of epoch , each node takes a snapshot of the SDP registry at the last block from the finalized epoch.
Each snapshot updates the common view of the registry. Changes to the declaration registry take effect with up to a two-epoch delay: messages sent during epoch n are included in the next snapshot (for epoch n+2).
Epochs 0 and 1 read the snapshot at the genesis block, because the chain has not yet progressed far enough to provide a later finalized block. While at epoch 2, the last block of epoch 0 is read, and so forth according to the above logic.
Message Timing
The tables below trace each SDP message from the epoch e of the block that contained it. Reward timing follows the Service Reward Distribution Protocol.
Declaration message
| Epoch | What happens |
|---|---|
e | The message is included and the declaration is stored, with created = e. |
e+1 | The declaration is in no snapshot; the node does not provide the service. |
e+2 | The first snapshot containing the declaration is taken; the node starts providing the service. |
e+3 | The node's first report is included; active = e+3. |
e+4 | The epoch-e+2 reward is distributed in the first block. |
Active message
| Epoch | What happens |
|---|---|
e-1 | The node provides the service. |
e | The message is included, attesting to epoch e-1; on acceptance, active = e. |
e+1 | The epoch-e-1 reward is distributed in the first block. |
e+2 | The report is included in a snapshot for the first time. |
At any epoch n, the most recent report a snapshot can contain was included in epoch n-2 and attests to epoch n-3.
Withdraw message
| Epoch | What happens |
|---|---|
e | The message is included, with withdraw_at = e+2. |
e+1 | The node is in the participant set (n < withdraw_at) and provides the service. It reports its epoch-e activity. |
e+2 | Every service excludes the declaration (n >= withdraw_at). The node reports its epoch-e+1 activity. The epoch-e reward is distributed in the first block. |
e+3 | The epoch-e+1 reward is distributed and the declaration is removed in the first block (SDP Epoch Finalization). |
Identifiers
We define the following set of identifiers which are used for service-specific cryptographic operations:
provider_id: used to sign the SDP messages and to establish secure links between validators; it isEd25519PublicKey.zk_id: used for zero-knowledge operations by the validator that includes rewarding (Zero Knowledge Signature Scheme (ZkSignature)).
Locators
A Locator is the address of a validator which is used to establish secure communication between validators. It follows the multiaddr addressing scheme from libp2p, but it must contain only the location part and must not contain the node identity (peer_id).
The provider_id must be used as the node identity. Therefore, the Locator must be completed by adding the provider_id at the end of it, which makes the Locator usable in the context of libp2p.
The canonical form of a Locator is the multiaddr binary (byte) form. Wherever a Locator is serialized — the transaction wire form (Mantle Transaction Encoding) — its binary form is used. The human-readable string form (e.g. /ip4/203.0.113.10/tcp/4001) is presentational only and must never appear in an encoding.
The length of the binary form of a Locator is restricted to 329 bytes.
Implementations that accept the string form as input must parse it into the binary form before serialization, and every part of the address must be explicit (no implicit defaults).
Declaration Message
The construction of the declaration message is as follows.
class DeclarationMessage:
service_type: ServiceType
locators: list[Locator]
provider_id: Ed25519PublicKey
zk_id: ZkPublicKey
service_note_id: NoteId
The locators list must be non-empty and its length must be limited to reduce the potential for abuse. Therefore, the length of the list cannot be longer than 8.
The message must be signed by the provider_id key to prove ownership of the key that is used for network-level authentication of the validator.
The service_note_id points to the service note the validator puts up as collateral.
The message is also signed by the zk_id key.
Declaration Storage
Only valid declaration messages can be stored on the ledger. A declaration covers exactly one service. We define the DeclarationInfo as follows:
class DeclarationInfo:
service: ServiceType
provider_id: Ed25519PublicKey
service_note_id: NoteId
locators: list[Locator]
zk_id: ZkPublicKey
created: EpochNumber
active: EpochNumber
withdraw_at: EpochNumber | None
nonce: Nonce
Where:
serviceis the service the declaration covers;provider_idis theEd25519PublicKeythe validator signs its messages with;service_note_idis theNoteIdof the note that meets the minimum stake threshold;locatorsis a copy of thelocatorsfrom theDeclarationMessage;zk_idis theZkPublicKeyof the validator, which signs its active and withdraw messages and receives its rewards;createdis the epoch of the block that contained the declaration;activeis the epoch of the block that contained the latest accepted active message, initialised tocreated + 2(Message Timing);withdraw_atis the epoch at which the node stops providing the service (Withdraw), and isNoneuntil the declaration is withdrawn;nonceis thenonceof the latest accepted active or withdraw message, initialised withcreatedas itslifecycle_epochand asequenceof 0.
A Nonce is a 64-bit unsigned integer, lifecycle_epoch · 2^32 + sequence:
lifecycle_epochis the high 32 bits, anEpochNumber;sequenceis the low 32 bits.
An active or withdraw message is valid only if its nonce:
- has the
createdof theDeclarationInfoas itslifecycle_epoch; - has a
sequencegreater than thesequenceof thenonceof theDeclarationInfo.
Two declarations with the same declaration_id never share a created epoch, because a declaration is removed no earlier than epoch created + 3 (Withdraw). If they could, a message signed for the earlier declaration would be valid for the later one.
The declaration_id (of a DeclarationId type) is the hash of the concatenation of service and zk_id:
declaration_id = Hash(service||zk_id)
where:
serviceis theServiceTypeproduction, one byte (Service Types);zk_idis theZkIdproduction, 32 bytes (Mantle Transaction Encoding);||is byte concatenation, so the preimage is 33 bytes;Hashis BLAKE2b with a digest length of 32 bytes, unkeyed, without salt or personalization.
The declaration_id is not stored as part of the DeclarationInfo but is used to index it. All DeclarationInfo entries are held in declarations, indexed by service and then by declaration_id.
declarations: dict[ServiceType, dict[DeclarationId, DeclarationInfo]]
Identifier Uniqueness
Within the declarations of one service, each of the following is bound to at most one DeclarationInfo:
- the
zk_id, so thedeclaration_idis unique; - the
provider_id; - the
service_note_id.
The same identifier may be bound in several services. An identifier becomes available for reuse in a service once its declaration in that service has been removed (see Withdraw).
Active Set
The active set of a service for an epoch is derived from that service's declarations in the snapshot read for that epoch (Snapshots) by keeping those for which both of the following hold:
- activity has been reported recently enough:
active + inactivity_period >= n; - the withdrawal has not taken effect:
withdraw_atisNone, orn < withdraw_at.
Both conditions are evaluated against the epoch the set is being derived for, not against the epoch the snapshot was taken in.
Active Message
The construction of the active message is as follows:
class ActiveMessage:
declaration_id: DeclarationId
nonce: Nonce
metadata: Metadata
where metadata is service-specific node activeness metadata.
The message must be signed by the zk_id key of the declaration.
The nonce follows Declaration Storage.
An active message attests to a single past epoch during which the node provided the service. The service defines when the message may be sent (see Active Message for the Blend Network).
The metadata layout is service-defined; the SDP does not parse it.
Withdraw Message
The construction of the withdraw message is as follows:
class WithdrawMessage:
declaration_id: DeclarationId
nonce: Nonce
The message must be signed by the zk_id key of the declaration.
The nonce follows Declaration Storage.
Serialization
Each message is the payload of one Mantle Operation (Mantle), serialized as the production of the Mantle Transaction Encoding named below, its fields in the order the message lists them:
| Message | Operation | Production |
|---|---|---|
DeclarationMessage | SDP_DECLARE | SDPDeclare |
ActiveMessage | SDP_ACTIVE | SDPActive |
WithdrawMessage | SDP_WITHDRAW | SDPWithdraw |
The signatures a message requires are carried in the Operation's proof and cover the Mantle transaction hash.
Indexing
Every event must be correctly indexed to enable lighter synchronization of the changes. Therefore, we index every declaration_id according to EventType, ServiceType, and Epoch. Where EventType = { "created", "active", "withdrawn" } follows the type of the message.
The Epoch key is the epoch of the block that contained the message, for all three event types.
events = {
event_type: {
service_type: {
epoch: {
declarations: list[declaration_id]
}
}
}
}
Protocol
Declare
The Declare action associates a validator with a service it wants to provide. It requires sending a valid DeclarationMessage (as defined in Declaration Message), which is then processed (as defined below) and stored (as defined in Declaration Storage).
The declaration message is considered valid when all of the following are met:
- The
service_note_idnames an unspent note whose value meets the minimum stake threshold. - The
zk_id, theservice_note_idand theprovider_idare each unbound in theservice_typeof the message (Identifier Uniqueness). - The sender holds the private key corresponding to the
provider_id. - The
locatorslist is non-empty and not longer than 8 entries.
If all of the above conditions are fulfilled, then the declaration is stored on the ledger under its service and declaration_id; otherwise, the message is discarded.
Active
The Active action marks a provider as actively providing its service. It requires sending a valid ActiveMessage (as defined in Active Message), which is relayed to the service-specific node activity logic (as indicated by the service type in Common SDP Structures).
The Active action updates the active value of the DeclarationInfo. A declaration considered inactive is not removed from the registry, and an accepted active message makes it active again. A declaration is removed only by withdrawal (Withdraw); a removed declaration cannot become active again.
The SDP active action logic is:
- A node sends an
ActiveMessagetransaction. - The
ActiveMessageis verified by the SDP logic:- The
declaration_idreturns an existingDeclarationInfo. - The transaction containing
ActiveMessageis signed by thezk_id. - The
nonceis valid for theDeclarationInfo(Declaration Storage).
- The
- If any of these conditions fail, discard the message and stop processing.
- The message is processed by the service-specific activity logic, together with the epoch of the block that contained the message.
- If the service-specific activity logic rejects the message, discard the message and stop processing.
- The
activefield of theDeclarationInfois set to that epoch.
An active message is valid only while the current epoch is at most withdraw_at (see Withdraw).
Withdraw
The Withdraw action withdraws a service declaration. It requires sending a valid WithdrawMessage (as defined in Withdraw Message). The withdrawal marks the intent to stop providing the service.
Let e be the epoch of the block that contained the WithdrawMessage; withdraw_at records e+2 (Snapshots). A declaration whose withdraw_at an epoch has reached is excluded from that epoch's Active Set.
The node provides the service through epoch withdraw_at - 1, its last rewardable epoch. The declaration is removed and its service note released at epoch withdraw_at + 1 (SDP Epoch Finalization).
The logic of the withdraw action is:
- A node sends a
WithdrawMessagetransaction. - The
WithdrawMessageis verified by the SDP logic.- The
declaration_idreturns an existingDeclarationInfo. - The transaction containing
WithdrawMessageis signed by thezk_id. - The
withdraw_atof theDeclarationInfoisNone. - The
nonceis valid for theDeclarationInfo(Declaration Storage).
- The
- If any of the above is not correct, then discard the message and stop.
- Set the
withdraw_atof theDeclarationInfoto the current epoch number plus two. - At epoch
withdraw_at + 1, right after the final reward is paid out, the Mantle epoch finalization step removes theDeclarationInfoand releases itsservice_note_id(SDP Epoch Finalization).
Query
A node must answer the following queries from the finalized state (Snapshots):
GetDeclarationInfo(declaration_id), returns theDeclarationInfoheld under thedeclaration_id.GetAllDeclarationInfo(service_type), returns everyDeclarationInfoof theservice_type, keyed bydeclaration_id.
A query for a declaration_id or a service_type the finalized state does not hold returns an error.
Mantle and ZK Proofs
For more information about Mantle and ZK proofs, please refer to Mantle.
Default Service Parameters
Blend Network
class BlendNetworkServiceParameters:
inactivity_period: 2
epoch: 0