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-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 |
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.
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.
- 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 set of active nodes, 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.
- After the service-specific locking period, the node can send a withdrawal message, and its declaration is removed from the Ledger, which means that the node will no longer provide the service.
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="BN" # 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.
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 uint64.
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 maximum time (as a number of epochs) during which an activation message must be sent; otherwise, the declaration is considered inactive. It must be at least 2 epochs long due to finalization reasons.epochdefines the epoch number at which the parameter was set; it isuint64.
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.
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 length of the Locator is restricted to 329 characters.
The common formatting of every Locator must be applied to maintain its unambiguity, to make deterministic ID generation work consistently. The Locator must at least contain only lowercase letters 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
locked_note_id: NoteId
zk_id: ZkPublicKey
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 locked_note_id points to a locked note used for minimum stake threshold verification purposes.
The message is also signed by the zk_id key.
Declaration Storage
Only valid declaration messages can be stored on the ledger. We define the DeclarationInfo as follows:
class DeclarationInfo:
service: ServiceType
provider_id: Ed25519PublicKey
locked_note_id: NoteId
zk_id: ZkPublicKey
locators: list[Locator]
created: EpochNumber
active: EpochNumber | None
withdraw_at: EpochNumber | None
nonce: Nonce
Where:
servicedefines the service type of the declaration;provider_idis anEd25519PublicKeyused to sign the message by the validator;locked_note_idis aNoteIdused for minimum stake threshold verification purposes;zk_idis used for zero-knowledge operations by the validator that includes rewarding;locatorsis a copy of thelocatorsfrom theDeclarationMessage;createdrefers to the epoch number of the block that contained the declaration;activerefers to the latest epoch number for which the active message was sent (it is set toNoneby default);withdraw_atrefers to the epoch number for which the service declaration will be withdrawn (it is set toNoneby default);- The
noncemust be set to 0 for the declaration message and must increase monotonically by every message sent for thedeclaration_id.
We also define the declaration_id (of a DeclarationId type) that is the unique identifier of DeclarationInfo calculated as a hash of the concatenation of service, provider_id, zk_id and locators. The implementation of the hash function is blake2b using 256 bits of the output.
declaration_id = Hash(service||provider_id||zk_id||locators)
The declaration_id is not stored as part of the DeclarationInfo but it is used to index it.
All DeclarationInfo references are stored in the declarations and are indexed by declaration_id.
declarations: list[declaration_id]
Identifier Uniqueness
The SDP is responsible for enforcing the uniqueness of the provider_id and the zk_id in the context of a service. This means that, for a given service, each provider_id and each zk_id must appear in at most one active DeclarationInfo at a time.
The declaration_id uniqueness alone is insufficient to guarantee this property. Because declaration_id = Hash(service||provider_id||zk_id||locators), two declarations for the same service that reuse the same provider_id (or the same zk_id) but differ in any other component would produce distinct declaration_ids and would therefore not collide. The SDP must reject such declarations regardless.
Consequently, within a single service:
- A
provider_idmust not be bound to more than oneDeclarationInfo. - A
zk_idmust not be bound to more than oneDeclarationInfo.
The uniqueness is scoped per-service: the same provider_id or zk_id may be reused across different services, but never more than once within the same service. A provider_id or zk_id becomes available for reuse in a service only once its previous DeclarationInfo for that service has been withdrawn and removed (see Withdraw).
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 associated with the declaration_id.
The nonce must increase monotonically by every message sent for the declaration_id.
Withdraw Message
The construction of the withdraw message is as follows:
class WithdrawMessage:
declaration_id: DeclarationId
locked_note_id: NoteId
nonce: Nonce
The message must be signed by the zk_id key from the declaration_id.
The locked_note_id is a NoteId that was used for minimum stake threshold verification purposes and will be unlocked after withdrawal.
The nonce must increase monotonically by every message sent for the declaration_id.
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.
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 sender meets the stake requirements and its
locked_note_idis valid. - The
declaration_idis unique. - The
provider_idand thezk_idare each unique in the context of theservice(as defined in Identifier Uniqueness). - The sender knows the secret behind the
provider_ididentifier. - The length of the
locatorslist must not be longer than 8. - The
nonceincreases monotonically.
If all of the above conditions are fulfilled, then the message is stored on the ledger; otherwise, the message is discarded.
Active
The Active action enables marking the provider as actively providing a 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, which means that it also activates inactive (but not expired) providers.
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
nonceincreases monotonically.
- The
- If any of these conditions fail, discard the message and stop processing.
- The message is processed by the service-specific activity logic alongside the
activevalue indicating the period since the last active message was sent. Theactivevalue comes from theDeclarationInfo. - If the service-specific activity logic approves the node active message, then the
activefield of theDeclarationInfois set to the epoch number indicated by metadata.
Withdraw
The withdraw action enables a withdrawal of a service declaration. It requires sending a valid WithdrawMessage (as defined in Withdraw Message). The withdrawal marks the intent to stop providing the service: the node provides the service through the withdrawal epoch e and stops afterwards. The withdraw_at field records this withdrawal epoch e, which is the node's last rewardable epoch. The declaration is removed and the stake unlocked at epoch e+2, right after the epoch-e reward is paid out, by the Mantle epoch finalization step (see SDP Epoch Finalization). Removing the declaration only after its final reward is paid guarantees it is never removed before the payout.
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_atfromDeclarationInfois set toNone. - The
nonceincreases monotonically.
- The
- If any of the above is not correct, then discard the message and stop.
- Set the
withdraw_atfrom theDeclarationInfoto the current epoch number (the withdrawal epoche). - The
DeclarationInfois removed and the stake unlocked (releasing thelocked_note_id) at epoche+2by the Mantle epoch finalization step, right after the final reward is paid out.
Query
The protocol must enable querying the ledger in at least the following manner:
GetAllProviderId(epoch), returns allprovider_ids associated with theepoch.GetAllProviderIdSince(epoch), returns allprovider_ids since theepoch.GetAllDeclarationInfo(epoch), returns allDeclarationInfoentries associated with theepoch.GetAllDeclarationInfoSince(epoch), returns allDeclarationInfoentries since theepoch.GetDeclarationInfo(provider_id), returns theDeclarationInfoentry identified by theprovider_id.GetDeclarationInfo(declaration_id), returns theDeclarationInfoentry identified by thedeclaration_id.GetAllServiceParameters(epoch), returns all entries of theServiceParametersstore for the requestedepoch.GetAllServiceParametersSince(epoch), returns all entries of theServiceParametersstore since the requestedepoch.GetServiceParameters(service_type, epoch), returns the service parameter entry from theServiceParametersstore of aservice_typefor a specifiedepoch.GetMinStake(epoch), returns theMinStakestructure at the requestedepoch.GetMinStakeSince(epoch), returns a set ofMinStakestructures since the requestedepoch.
The query must return an error if the requested information is not available.
The list of queries may be extended.
Every query must return information for a finalized state only.
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