diff --git a/pages/_meta.ts b/pages/_meta.ts index b700a1c..bc6845f 100644 --- a/pages/_meta.ts +++ b/pages/_meta.ts @@ -68,6 +68,10 @@ const meta: Meta = { title: "Protocol", type: "page", }, + "release-notes": { + title: "Release Notes", + type: "page", + }, }; export default meta; diff --git a/pages/developers/api/reference/IBlueprintServiceManager.mdx b/pages/developers/api/reference/IBlueprintServiceManager.mdx index 91a4cb6..6d206d4 100644 --- a/pages/developers/api/reference/IBlueprintServiceManager.mdx +++ b/pages/developers/api/reference/IBlueprintServiceManager.mdx @@ -186,6 +186,63 @@ _Defines minimum commitment and exit queue timing_ | exitQueueDuration | uint64 | Time between scheduling exit and completing it (seconds) | | forceExitAllowed | bool | Whether service owner can force-exit operators | +#### getNonPaymentTerminationPolicy + +```solidity +function getNonPaymentTerminationPolicy(uint64 serviceId) external view returns (bool useDefault, uint64 graceIntervals) +``` + +Get the non-payment termination policy for subscription services. + +Core computes eligibility as +`lastPaymentAt + subscriptionInterval + (subscriptionInterval * graceIntervals)`. +`graceIntervals = 0` means termination is eligible immediately at the first missed +billing tick. Implementations should return `useDefault = true` unless they need +custom grace behavior. Default implementation in `BlueprintServiceManagerBase` returns +`(true, 0)`. + +##### Parameters + +| Name | Type | Description | +| --------- | ------ | -------------- | +| serviceId | uint64 | The service ID | + +##### Return Values + +| Name | Type | Description | +| -------------- | ------ | ---------------------------------------------------------------- | +| useDefault | bool | True to use the protocol default policy | +| graceIntervals | uint64 | Additional full intervals to wait after the first missed payment | + +#### forceRemoveAllowsBelowMin + +```solidity +function forceRemoveAllowsBelowMin(uint64 serviceId) external view returns (bool ok) +``` + +Whether `forceRemoveOperator` may drop the service below `minOperators`. + +By default the protocol enforces `operatorCount > minOperators` even when a blueprint +manager calls `forceRemoveOperator`. A blueprint that genuinely needs +emergency-eviction-below-min must self-document by returning `true`. Reverting or +unimplemented => the protocol enforces the floor (fail-closed). The default +implementation in `BlueprintServiceManagerBase` returns `false`; custom managers that +do **not** inherit `BlueprintServiceManagerBase` MUST implement this hook explicitly +or `forceRemoveOperator` will revert as soon as the eviction would push the operator +count below `minOperators`. + +##### Parameters + +| Name | Type | Description | +| --------- | ------ | -------------- | +| serviceId | uint64 | The service ID | + +##### Return Values + +| Name | Type | Description | +| ---- | ---- | ------------------------------------------------------- | +| ok | bool | True to allow eviction below the minimum operator count | + #### onRequest ```solidity diff --git a/pages/developers/api/reference/IMBSMRegistry.mdx b/pages/developers/api/reference/IMBSMRegistry.mdx index 4d8e110..49859fd 100644 --- a/pages/developers/api/reference/IMBSMRegistry.mdx +++ b/pages/developers/api/reference/IMBSMRegistry.mdx @@ -89,7 +89,13 @@ Get the latest revision number registered in the registry function pinBlueprint(uint64 blueprintId, uint32 revision) external ``` -Pin a blueprint to a specific revision (0 disallowed) +Pin a blueprint to a specific revision (0 disallowed). + +Reverts if `revision` is currently inside the deprecation grace window. Pinning a blueprint +to a revision that is itself deprecated would defeat the deprecation flow: `getMBSM` would +return `address(0)` the moment `completeDeprecation` ran, breaking every BSM call for the +pinned blueprint. Choose a revision that is not scheduled for deprecation, or wait until +the deprecation has fully completed before pinning to a different revision. #### unpinBlueprint diff --git a/pages/developers/api/reference/ITangleSlashing.mdx b/pages/developers/api/reference/ITangleSlashing.mdx index 2b93591..efc9a65 100644 --- a/pages/developers/api/reference/ITangleSlashing.mdx +++ b/pages/developers/api/reference/ITangleSlashing.mdx @@ -11,12 +11,15 @@ Source: https://github.com/tangle-network/tnt-core/blob/main/src/interfaces/ITan Slashing interface for Tangle protocol +The event declarations on this interface mirror what the protocol actually emits from `SlashingLib`. +Off-chain consumers (Rust bindings, indexers) MUST decode against the shapes documented below. + #### Functions #### proposeSlash ```solidity -function proposeSlash(uint64 serviceId, address operator, uint256 amount, bytes32 evidence) external returns (uint64 slashId) +function proposeSlash(uint64 serviceId, address operator, uint16 slashBps, bytes32 evidence) external returns (uint64 slashId) ``` Propose a slash against an operator @@ -27,8 +30,8 @@ Propose a slash against an operator | --------- | ------- | ------------------------------------ | | serviceId | uint64 | The service where violation occurred | | operator | address | The operator to slash | -| amount | uint256 | Amount to slash | -| evidence | bytes32 | Evidence hash | +| slashBps | uint16 | Slash percentage in basis points | +| evidence | bytes32 | Evidence hash (must be non-zero) | ##### Return Values @@ -39,10 +42,14 @@ Propose a slash against an operator #### disputeSlash ```solidity -function disputeSlash(uint64 slashId, string reason) external +function disputeSlash(uint64 slashId, string reason) external payable ``` -Dispute a slash proposal +Dispute a slash proposal. + +`payable` because the implementation requires `msg.value == config.disputeBond` when the bond +is non-zero (and zero otherwise). Typed callers MUST use a payable reference so +`disputeSlash{value: bond}(...)` compiles. #### executeSlash @@ -79,11 +86,22 @@ Cancel a slash proposal #### setSlashConfig ```solidity -function setSlashConfig(uint64 disputeWindow, bool instantSlashEnabled, uint16 maxSlashBps) external +function setSlashConfig(uint64 disputeWindow, bool instantSlashEnabled, uint16 maxSlashBps, uint64 disputeResolutionDeadline, uint256 disputeBond, uint16 maxPendingSlashesPerOperator) external ``` Update slashing configuration +##### Parameters + +| Name | Type | Description | +| ---------------------------- | ------- | ---------------------------------------------------------------- | +| disputeWindow | uint64 | Time after `proposeSlash` during which the operator can dispute | +| instantSlashEnabled | bool | Reserved emergency toggle (no effect through the standard API) | +| maxSlashBps | uint16 | Hard cap on any single slash proposal | +| disputeResolutionDeadline | uint64 | Time `SLASH_ADMIN` has to resolve a dispute before it auto-fails | +| disputeBond | uint256 | Native asset bond required to dispute (0 = disabled) | +| maxPendingSlashesPerOperator | uint16 | Cap on concurrent pending slashes per operator (anti-spam) | + #### getSlashProposal ```solidity @@ -92,16 +110,93 @@ function getSlashProposal(uint64 slashId) external view returns (struct Slashing Get slash proposal details +#### getSlashConfig + +```solidity +function getSlashConfig() external view returns (struct SlashingLib.SlashConfig) +``` + +Get the current slashing configuration. Returns the live `SlashConfig` tuple containing +`disputeWindow`, `instantSlashEnabled`, `maxSlashBps`, `disputeResolutionDeadline`, +`disputeBond`, and `maxPendingSlashesPerOperator`. + #### Events #### SlashProposed ```solidity -event SlashProposed(uint64 serviceId, address operator, uint256 amount, bytes32 evidence) +event SlashProposed(uint64 indexed slashId, uint64 indexed serviceId, address indexed operator, address proposer, uint16 slashBps, uint16 effectiveSlashBps, bytes32 evidence, uint64 executeAfter) ``` +Emitted when a new slash proposal is created. + +##### Parameters + +| Name | Type | Description | +| ----------------- | ------- | ----------------------------------------------------------------------- | +| slashId | uint64 | The new slash ID (indexed) | +| serviceId | uint64 | The service where the violation occurred (indexed) | +| operator | address | The slashed operator (indexed) | +| proposer | address | The address that called `proposeSlash` | +| slashBps | uint16 | Requested slash percentage in basis points | +| effectiveSlashBps | uint16 | Slash percentage after exposure scaling (what will actually be applied) | +| evidence | bytes32 | Evidence hash (non-zero, enforced by `proposeSlash`) | +| executeAfter | uint64 | Earliest UNIX timestamp at which the slash can be executed | + +#### SlashDisputed + +```solidity +event SlashDisputed(uint64 indexed slashId, address indexed disputer, string reason) +``` + +Emitted when a slash proposal is disputed by the operator or by `SLASH_ADMIN_ROLE`. + +##### Parameters + +| Name | Type | Description | +| -------- | ------- | ------------------------------------------ | +| slashId | uint64 | The disputed slash ID (indexed) | +| disputer | address | The address that called `disputeSlash` | +| reason | string | Human-readable rationale (free-form input) | + #### SlashExecuted ```solidity -event SlashExecuted(uint64 serviceId, address operator, uint256 amount) +event SlashExecuted(uint64 indexed slashId, uint64 indexed serviceId, address indexed operator, uint256 actualSlashed) +``` + +Emitted when a slash is executed. + +##### Parameters + +| Name | Type | Description | +| ------------- | ------- | -------------------------------------------------- | +| slashId | uint64 | The executed slash ID (indexed) | +| serviceId | uint64 | The service the slash was applied to (indexed) | +| operator | address | The slashed operator (indexed) | +| actualSlashed | uint256 | Total stake actually burned in the underlying call | + +#### SlashCancelled + +```solidity +event SlashCancelled(uint64 indexed slashId, address indexed canceller, string reason) ``` + +Emitted when a slash proposal is cancelled by `SLASH_ADMIN_ROLE`. + +##### Parameters + +| Name | Type | Description | +| --------- | ------- | ------------------------------------------- | +| slashId | uint64 | The cancelled slash ID (indexed) | +| canceller | address | Address that called `cancelSlash` (indexed) | +| reason | string | Human-readable rationale (free-form input) | + +#### SlashConfigUpdated + +```solidity +event SlashConfigUpdated(uint64 disputeWindow, bool instantSlashEnabled, uint16 maxSlashBps, uint64 disputeResolutionDeadline, uint256 disputeBond, uint16 maxPendingSlashesPerOperator) +``` + +Emitted when `setSlashConfig` updates the slashing configuration. The full new +configuration is included in the event. diff --git a/pages/developers/api/reference/generated/IBlueprintServiceManager.mdx b/pages/developers/api/reference/generated/IBlueprintServiceManager.mdx index 91a4cb6..6d206d4 100644 --- a/pages/developers/api/reference/generated/IBlueprintServiceManager.mdx +++ b/pages/developers/api/reference/generated/IBlueprintServiceManager.mdx @@ -186,6 +186,63 @@ _Defines minimum commitment and exit queue timing_ | exitQueueDuration | uint64 | Time between scheduling exit and completing it (seconds) | | forceExitAllowed | bool | Whether service owner can force-exit operators | +#### getNonPaymentTerminationPolicy + +```solidity +function getNonPaymentTerminationPolicy(uint64 serviceId) external view returns (bool useDefault, uint64 graceIntervals) +``` + +Get the non-payment termination policy for subscription services. + +Core computes eligibility as +`lastPaymentAt + subscriptionInterval + (subscriptionInterval * graceIntervals)`. +`graceIntervals = 0` means termination is eligible immediately at the first missed +billing tick. Implementations should return `useDefault = true` unless they need +custom grace behavior. Default implementation in `BlueprintServiceManagerBase` returns +`(true, 0)`. + +##### Parameters + +| Name | Type | Description | +| --------- | ------ | -------------- | +| serviceId | uint64 | The service ID | + +##### Return Values + +| Name | Type | Description | +| -------------- | ------ | ---------------------------------------------------------------- | +| useDefault | bool | True to use the protocol default policy | +| graceIntervals | uint64 | Additional full intervals to wait after the first missed payment | + +#### forceRemoveAllowsBelowMin + +```solidity +function forceRemoveAllowsBelowMin(uint64 serviceId) external view returns (bool ok) +``` + +Whether `forceRemoveOperator` may drop the service below `minOperators`. + +By default the protocol enforces `operatorCount > minOperators` even when a blueprint +manager calls `forceRemoveOperator`. A blueprint that genuinely needs +emergency-eviction-below-min must self-document by returning `true`. Reverting or +unimplemented => the protocol enforces the floor (fail-closed). The default +implementation in `BlueprintServiceManagerBase` returns `false`; custom managers that +do **not** inherit `BlueprintServiceManagerBase` MUST implement this hook explicitly +or `forceRemoveOperator` will revert as soon as the eviction would push the operator +count below `minOperators`. + +##### Parameters + +| Name | Type | Description | +| --------- | ------ | -------------- | +| serviceId | uint64 | The service ID | + +##### Return Values + +| Name | Type | Description | +| ---- | ---- | ------------------------------------------------------- | +| ok | bool | True to allow eviction below the minimum operator count | + #### onRequest ```solidity diff --git a/pages/developers/api/reference/generated/IMBSMRegistry.mdx b/pages/developers/api/reference/generated/IMBSMRegistry.mdx index 4d8e110..49859fd 100644 --- a/pages/developers/api/reference/generated/IMBSMRegistry.mdx +++ b/pages/developers/api/reference/generated/IMBSMRegistry.mdx @@ -89,7 +89,13 @@ Get the latest revision number registered in the registry function pinBlueprint(uint64 blueprintId, uint32 revision) external ``` -Pin a blueprint to a specific revision (0 disallowed) +Pin a blueprint to a specific revision (0 disallowed). + +Reverts if `revision` is currently inside the deprecation grace window. Pinning a blueprint +to a revision that is itself deprecated would defeat the deprecation flow: `getMBSM` would +return `address(0)` the moment `completeDeprecation` ran, breaking every BSM call for the +pinned blueprint. Choose a revision that is not scheduled for deprecation, or wait until +the deprecation has fully completed before pinning to a different revision. #### unpinBlueprint diff --git a/pages/developers/api/reference/generated/ITangleSlashing.mdx b/pages/developers/api/reference/generated/ITangleSlashing.mdx index 2b93591..efc9a65 100644 --- a/pages/developers/api/reference/generated/ITangleSlashing.mdx +++ b/pages/developers/api/reference/generated/ITangleSlashing.mdx @@ -11,12 +11,15 @@ Source: https://github.com/tangle-network/tnt-core/blob/main/src/interfaces/ITan Slashing interface for Tangle protocol +The event declarations on this interface mirror what the protocol actually emits from `SlashingLib`. +Off-chain consumers (Rust bindings, indexers) MUST decode against the shapes documented below. + #### Functions #### proposeSlash ```solidity -function proposeSlash(uint64 serviceId, address operator, uint256 amount, bytes32 evidence) external returns (uint64 slashId) +function proposeSlash(uint64 serviceId, address operator, uint16 slashBps, bytes32 evidence) external returns (uint64 slashId) ``` Propose a slash against an operator @@ -27,8 +30,8 @@ Propose a slash against an operator | --------- | ------- | ------------------------------------ | | serviceId | uint64 | The service where violation occurred | | operator | address | The operator to slash | -| amount | uint256 | Amount to slash | -| evidence | bytes32 | Evidence hash | +| slashBps | uint16 | Slash percentage in basis points | +| evidence | bytes32 | Evidence hash (must be non-zero) | ##### Return Values @@ -39,10 +42,14 @@ Propose a slash against an operator #### disputeSlash ```solidity -function disputeSlash(uint64 slashId, string reason) external +function disputeSlash(uint64 slashId, string reason) external payable ``` -Dispute a slash proposal +Dispute a slash proposal. + +`payable` because the implementation requires `msg.value == config.disputeBond` when the bond +is non-zero (and zero otherwise). Typed callers MUST use a payable reference so +`disputeSlash{value: bond}(...)` compiles. #### executeSlash @@ -79,11 +86,22 @@ Cancel a slash proposal #### setSlashConfig ```solidity -function setSlashConfig(uint64 disputeWindow, bool instantSlashEnabled, uint16 maxSlashBps) external +function setSlashConfig(uint64 disputeWindow, bool instantSlashEnabled, uint16 maxSlashBps, uint64 disputeResolutionDeadline, uint256 disputeBond, uint16 maxPendingSlashesPerOperator) external ``` Update slashing configuration +##### Parameters + +| Name | Type | Description | +| ---------------------------- | ------- | ---------------------------------------------------------------- | +| disputeWindow | uint64 | Time after `proposeSlash` during which the operator can dispute | +| instantSlashEnabled | bool | Reserved emergency toggle (no effect through the standard API) | +| maxSlashBps | uint16 | Hard cap on any single slash proposal | +| disputeResolutionDeadline | uint64 | Time `SLASH_ADMIN` has to resolve a dispute before it auto-fails | +| disputeBond | uint256 | Native asset bond required to dispute (0 = disabled) | +| maxPendingSlashesPerOperator | uint16 | Cap on concurrent pending slashes per operator (anti-spam) | + #### getSlashProposal ```solidity @@ -92,16 +110,93 @@ function getSlashProposal(uint64 slashId) external view returns (struct Slashing Get slash proposal details +#### getSlashConfig + +```solidity +function getSlashConfig() external view returns (struct SlashingLib.SlashConfig) +``` + +Get the current slashing configuration. Returns the live `SlashConfig` tuple containing +`disputeWindow`, `instantSlashEnabled`, `maxSlashBps`, `disputeResolutionDeadline`, +`disputeBond`, and `maxPendingSlashesPerOperator`. + #### Events #### SlashProposed ```solidity -event SlashProposed(uint64 serviceId, address operator, uint256 amount, bytes32 evidence) +event SlashProposed(uint64 indexed slashId, uint64 indexed serviceId, address indexed operator, address proposer, uint16 slashBps, uint16 effectiveSlashBps, bytes32 evidence, uint64 executeAfter) ``` +Emitted when a new slash proposal is created. + +##### Parameters + +| Name | Type | Description | +| ----------------- | ------- | ----------------------------------------------------------------------- | +| slashId | uint64 | The new slash ID (indexed) | +| serviceId | uint64 | The service where the violation occurred (indexed) | +| operator | address | The slashed operator (indexed) | +| proposer | address | The address that called `proposeSlash` | +| slashBps | uint16 | Requested slash percentage in basis points | +| effectiveSlashBps | uint16 | Slash percentage after exposure scaling (what will actually be applied) | +| evidence | bytes32 | Evidence hash (non-zero, enforced by `proposeSlash`) | +| executeAfter | uint64 | Earliest UNIX timestamp at which the slash can be executed | + +#### SlashDisputed + +```solidity +event SlashDisputed(uint64 indexed slashId, address indexed disputer, string reason) +``` + +Emitted when a slash proposal is disputed by the operator or by `SLASH_ADMIN_ROLE`. + +##### Parameters + +| Name | Type | Description | +| -------- | ------- | ------------------------------------------ | +| slashId | uint64 | The disputed slash ID (indexed) | +| disputer | address | The address that called `disputeSlash` | +| reason | string | Human-readable rationale (free-form input) | + #### SlashExecuted ```solidity -event SlashExecuted(uint64 serviceId, address operator, uint256 amount) +event SlashExecuted(uint64 indexed slashId, uint64 indexed serviceId, address indexed operator, uint256 actualSlashed) +``` + +Emitted when a slash is executed. + +##### Parameters + +| Name | Type | Description | +| ------------- | ------- | -------------------------------------------------- | +| slashId | uint64 | The executed slash ID (indexed) | +| serviceId | uint64 | The service the slash was applied to (indexed) | +| operator | address | The slashed operator (indexed) | +| actualSlashed | uint256 | Total stake actually burned in the underlying call | + +#### SlashCancelled + +```solidity +event SlashCancelled(uint64 indexed slashId, address indexed canceller, string reason) ``` + +Emitted when a slash proposal is cancelled by `SLASH_ADMIN_ROLE`. + +##### Parameters + +| Name | Type | Description | +| --------- | ------- | ------------------------------------------- | +| slashId | uint64 | The cancelled slash ID (indexed) | +| canceller | address | Address that called `cancelSlash` (indexed) | +| reason | string | Human-readable rationale (free-form input) | + +#### SlashConfigUpdated + +```solidity +event SlashConfigUpdated(uint64 disputeWindow, bool instantSlashEnabled, uint16 maxSlashBps, uint64 disputeResolutionDeadline, uint256 disputeBond, uint16 maxPendingSlashesPerOperator) +``` + +Emitted when `setSlashConfig` updates the slashing configuration. The full new +configuration is included in the event. diff --git a/pages/developers/auth-surface.mdx b/pages/developers/auth-surface.mdx index bfb69d9..9643f08 100644 --- a/pages/developers/auth-surface.mdx +++ b/pages/developers/auth-surface.mdx @@ -66,14 +66,14 @@ Most admin setters in `Base.sol` are `whenNotPaused`. The two exceptions, both i ### Slashing -| Function | Caller | Source | -| ------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| `proposeSlash(serviceId, op, slashBps, evidence)` | Service owner, blueprint owner, or BSM-declared origin | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `disputeSlash(slashId, reason)` `payable` | Slashed operator (must post bond), or `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `executeSlash(slashId)` | Anyone (gated by `isExecutable`) | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `executeSlashBatch(ids)` | Anyone | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `cancelSlash(slashId, reason)` | `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `setSlashConfig(...)` | `ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| Function | Caller | Source | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `proposeSlash(serviceId, op, slashBps, evidence)` | Service owner, blueprint owner, or BSM-declared origin | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `disputeSlash(slashId, reason)` `payable` | Slashed operator (must post bond), or `SLASH_ADMIN_ROLE` (admin cannot self-dispute their own proposal) | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `executeSlash(slashId)` | Anyone (gated by `isExecutable`) | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `executeSlashBatch(ids)` | Anyone | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `cancelSlash(slashId, reason)` | `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `setSlashConfig(...)` | `ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | See [Slashing](/developers/slashing) for the full lifecycle and runbooks. @@ -104,8 +104,8 @@ See [Slashing](/developers/slashing) for the full lifecycle and runbooks. | `approveService(ApprovalParams)` | Operator listed in the request (request not expired) | | `rejectService(requestId)` | Operator listed in the request | | `expireServiceRequest(requestId)` | Anyone, after grace period (when not activated) | -| `terminateService(serviceId)` | Service owner | -| `forceRemoveOperator(serviceId, operator)` | Blueprint manager only | +| `terminateService(serviceId)` | Service owner, `nonReentrant` | +| `forceRemoveOperator(serviceId, operator)` | Blueprint manager only, `nonReentrant` | `approveService` is a single entrypoint. Optional capabilities are opt-in via empty/zero fields on `ApprovalParams`: empty `securityCommitments`, zero `blsPubkey`, or empty `teeCommitments` each means "skip this capability." See [the slashing doc](/developers/slashing) for the per-asset commitment contract. @@ -175,7 +175,7 @@ Staking and delegation are a separate proxy with its own role registry. Function | Function | Caller | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `addVersion(mbsm)` | `MANAGER_ROLE`. Rejects EOA _targets_ (`mbsmAddress.code.length != 0`); the caller can be any holder of the role. | -| `pinBlueprint(blueprintId, revision)` | `MANAGER_ROLE` | +| `pinBlueprint(blueprintId, revision)` | `MANAGER_ROLE`. Reverts if `revision` is currently in the deprecation grace window. | | `unpinBlueprint(blueprintId)` | `MANAGER_ROLE` | | `initiateDeprecation(revision)` | `MANAGER_ROLE` | | `completeDeprecation(revision)` | `MANAGER_ROLE`, after grace period | @@ -198,12 +198,38 @@ Staking and delegation are a separate proxy with its own role registry. Function ### `L2SlashingReceiver` -| Function | Caller | -| -------------------------------------------------- | ------------------------------------------- | -| `receiveMessage(sourceChainId, sender, payload)` | The configured cross-chain messenger only | -| `setAuthorizedSender(chainId, sender, authorized)` | Owner; authorization is timelocked (2 days) | -| `activateAuthorizedSender(chainId, sender)` | Owner, after `SENDER_ACTIVATION_DELAY` | -| `setMessenger(addr)` | Owner | +| Function | Caller | +| -------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `receiveMessage(sourceChainId, sender, payload)` | The configured cross-chain messenger only | +| `setAuthorizedSender(chainId, sender, authorized)` | Owner. Revocation is immediate; authorization is timelocked (`SENDER_ACTIVATION_DELAY`) | +| `activateAuthorizedSender(chainId, sender)` | Owner, after `SENDER_ACTIVATION_DELAY` (2 days) | +| `setMessenger(addr)` | Owner. Bootstrap (current = `address(0)`) is immediate; subsequent swaps are queued | +| `activateMessenger()` | Owner, after `SENDER_ACTIVATION_DELAY` | +| `setSlasher(addr)` | Owner. Bootstrap (current = `address(0)`) is immediate; subsequent swaps are queued | +| `activateSlasher()` | Owner, after `SENDER_ACTIVATION_DELAY` | + +**Two-step swap (v0.13.0).** `setMessenger` and `setSlasher` no longer take effect on the +write that calls them. The owner queues the new address and the activation timestamp; after +`SENDER_ACTIVATION_DELAY` (2 days) elapses, the same owner calls `activateMessenger()` / +`activateSlasher()` to flip the live pointer. The bootstrap exemption (when the current +value is `address(0)`) lets deploy scripts wire the bridge without a 2-day deadlock; the +exemption is consumed the first time the slot is set. A typical rotation in production +looks like: + +```solidity +// t = 0: queue the new messenger +receiver.setMessenger(newMessenger); +// ... 2 days later (`SENDER_ACTIVATION_DELAY` has elapsed) +receiver.activateMessenger(); +``` + +**Receiver enforcement (v0.13.0).** `receiveMessage` now reverts if the L2 slasher returns +`canSlash == false` for the target operator (e.g. paused, unknown operator) or if +`slashBps == 0`, **before** consuming the bridge nonce. Previously the nonce was marked +processed first and a transient failure silently dropped the slash. With the new ordering +the bridge keeps the message available for retry until the slash actually applies. Relayers +must distinguish "already processed" (revert) from "still pending" (revert with +`SlashingNotPossible`) and re-deliver the latter. ### `L2SlashingConnector` diff --git a/pages/developers/blueprints/execution-confidentiality.mdx b/pages/developers/blueprints/execution-confidentiality.mdx index 27d5074..0d48b76 100644 --- a/pages/developers/blueprints/execution-confidentiality.mdx +++ b/pages/developers/blueprints/execution-confidentiality.mdx @@ -96,6 +96,10 @@ Important consequences: - All quotes in a single RFQ service creation or extension must agree on confidentiality. - Service extensions preserve the active service's confidentiality mode. If you need to switch modes, create a new service. +- RFQ quotes are bound to a specific consumer via the `requester` field of `QuoteDetails` + (and `JobQuoteDetails` for per-job quotes). The contract enforces + `requester == msg.sender` and rejects wildcard `requester == address(0)`, so confidential + service quotes cannot be lifted from the mempool and redeemed by a different caller. ## Tooling Status diff --git a/pages/developers/blueprints/pricing-and-payments.mdx b/pages/developers/blueprints/pricing-and-payments.mdx index 35d4626..9010ce8 100644 --- a/pages/developers/blueprints/pricing-and-payments.mdx +++ b/pages/developers/blueprints/pricing-and-payments.mdx @@ -76,15 +76,50 @@ Use Job RFQ when the price should be negotiated per request (LLM model selection High-level flow: 1. User requests a per-job quote from one or more operators (off-chain). -2. Operators respond with an EIP-712 signed `JobQuoteDetails { serviceId, jobIndex, price, timestamp, expiry }`. +2. Operators respond with an EIP-712 signed + `JobQuoteDetails { requester, serviceId, jobIndex, price, timestamp, expiry, confidentiality }`. 3. User submits the job on-chain with the signed quote(s) via `submitJobFromQuote`. Key properties: +- **Quote binding (v0.13.0).** `requester` is the FIRST field of `JobQuoteDetails` and is part + of the EIP-712 typehash. The contract enforces `requester == msg.sender` and rejects + wildcard `requester == address(0)`. Operators MUST sign per-caller quotes and populate + `requester` with the address they expect to consume the quote; an attacker who lifts a + signed digest off the wire cannot redirect it to themselves. The new typehash string is: + `JobQuoteDetails(address requester,uint64 serviceId,uint8 jobIndex,uint256 price,uint64 timestamp,uint64 expiry,uint8 confidentiality)`. - `msg.value` must equal `sum(quote.price)` across quotes. - Quotes are replay-protected and expire (protocol also enforces `maxQuoteAge`). - Only quoted operators can submit results for RFQ jobs. +A minimal Solidity-side struct literal an off-chain signer should hash: + +```solidity +Types.JobQuoteDetails({ + requester: 0xCAFE..., // MUST match the on-chain msg.sender + serviceId: 42, + jobIndex: 0, + price: 1_000_000 gwei, + timestamp: uint64(block.timestamp), + expiry: uint64(block.timestamp + 5 minutes), + confidentiality: 0 // 0=Any, 1=Required, 2=Preferred +}); +``` + +The equivalent EIP-712 typed-data payload (`message`): + +```json +{ + "requester": "0xCAFE000000000000000000000000000000000000", + "serviceId": "42", + "jobIndex": 0, + "price": "1000000000000000", + "timestamp": "1746700000", + "expiry": "1746700300", + "confidentiality": 0 +} +``` + ## Off-Chain Settlement (x402, optional) Some services want an HTTP-first, stablecoin settlement path for paid job execution. The Blueprint SDK includes an @@ -104,12 +139,25 @@ operator-run gateway. See: Service RFQ lets a user create a service instantly using operator quotes: 1. User requests service quotes from operators for `(blueprintId, ttlBlocks, confidentiality, security requirements)`. -2. Operators sign EIP-712 `QuoteDetails { blueprintId, ttlBlocks, totalCost, timestamp, expiry, confidentiality, ... }`. +2. Operators sign EIP-712 + `QuoteDetails { requester, blueprintId, ttlBlocks, totalCost, timestamp, expiry, confidentiality, securityCommitments[], resourceCommitments[] }`. 3. User calls `createServiceFromQuotes(...)` (or `extendServiceFromQuotes(...)`) with quotes and payment. All quotes for a single RFQ service creation or extension must agree on confidentiality. Changing confidentiality mode is treated as a new service agreement, not an in-place extension. +**Quote binding (v0.12.0+).** `requester` is the FIRST field of `QuoteDetails` and is part of the +EIP-712 typehash. The contract enforces `requester == msg.sender` on +`createServiceFromQuotes` / `extendServiceFromQuotes`, and rejects wildcard +`requester == address(0)`. Off-chain signers MUST populate `requester` with the address they +expect to redeem the quote; previously the field existed on the struct but was excluded from +the typehash, so a mempool observer could rewrite `details.requester` and the operator's +signature still recovered. The new typehash string is: +`QuoteDetails(address requester,uint64 blueprintId,uint64 ttlBlocks,uint256 totalCost,uint64 timestamp,uint64 expiry,uint8 confidentiality,AssetSecurityCommitment[] securityCommitments,ResourceCommitment[] resourceCommitments)`. + +Pre-fix signatures (without `requester` in the typehash) are invalid against the new +typehash and will fail signature recovery. + This is separate from the request and approval service lifecycle flow that the CLI exposes under `cargo tangle blueprint service`. For renewal and expiry semantics (including limitations of `extendServiceFromQuotes` and migration recommendations), see: diff --git a/pages/developers/blueprints/pricing-engine.mdx b/pages/developers/blueprints/pricing-engine.mdx index fc67cf9..17ac3ad 100644 --- a/pages/developers/blueprints/pricing-engine.mdx +++ b/pages/developers/blueprints/pricing-engine.mdx @@ -88,6 +88,13 @@ Important environment variables: - `GetPrice` uses proof-of-work to rate-limit RPC abuse. - Quotes include TTL + expiry and are signed with the operator ECDSA key to prevent replay. +- **Quote requester binding (v0.13.0).** Both service quotes (`QuoteDetails`) and per-job + quotes (`JobQuoteDetails`) include `requester` as the FIRST field of the EIP-712 typed + data. The contract enforces `requester == msg.sender` and rejects wildcard + `requester == address(0)`. Operator software MUST populate `requester` with the address + the customer intends to redeem the quote with; signing wildcard quotes (or omitting the + field from the typehash) makes the quote unusable on-chain and exposes operators to + mempool front-running of the signed digest. ## x402 Settlement Options (Optional) @@ -113,3 +120,7 @@ gRPC server to return x402 settlement options, you need to construct the pricing 6. Keep quote details intact. Never modify quote payloads after they are signed. 7. Generate fresh proof-of-work per request if your deployment uses PoW gating. 8. Expect price variation across operators. +9. **Always populate `requester` with the on-chain caller you intend to redeem the quote + with.** The contract enforces `requester == msg.sender` and rejects `address(0)`. If + the redeemer is a smart contract (paymaster, multisig), `requester` MUST be that + contract's address. diff --git a/pages/developers/blueprints/service-lifecycle.mdx b/pages/developers/blueprints/service-lifecycle.mdx index cb7f5cb..5d942ad 100644 --- a/pages/developers/blueprints/service-lifecycle.mdx +++ b/pages/developers/blueprints/service-lifecycle.mdx @@ -72,6 +72,13 @@ Options depend on the blueprint membership model: Blueprint managers can also force-remove operators from a service for emergency response. Blueprint developers should treat this as a last-resort tool and design for the possibility that the operator set changes unexpectedly. +By default the protocol enforces `operatorCount > minOperators` even when a blueprint manager calls +`forceRemoveOperator`. A blueprint that genuinely needs emergency-eviction-below-min must opt in by overriding +the new `forceRemoveAllowsBelowMin(serviceId)` BSM hook to return `true` (default in +`BlueprintServiceManagerBase` is `false`). Custom managers that do not inherit `BlueprintServiceManagerBase` MUST +implement this hook explicitly; an unimplemented or reverting view fails closed and the eviction will revert as soon +as it would push the service below `minOperators`. + ## What Expiry Means Expiry is enforced by function-level checks. When a service is expired (`block.timestamp > createdAt + ttl`), critical @@ -80,6 +87,31 @@ paths like job submission and subscription billing revert. Expiry does not necessarily mean "terminated" in the protocol's service status. If you need explicit cleanup or refund behavior, ensure your operational tooling calls `terminateService(serviceId)` when a service should end. +## Exit Gates (v0.13.0) + +The protocol tightened lifecycle gating so a stale operator cannot continue to mutate a service whose +agreement is already over, and so a request cannot be rescued past its expiry grace window. + +- **Active-service gate on every exit entrypoint.** `scheduleExit`, `executeExit`, `forceExit`, + `leaveService`, and `forceRemoveOperator` all reject when `service.status != Active`. Previously + a stale operator could keep firing exit paths against a `Terminated` service, double-decrementing + counts and emitting `OperatorLeftService` for a service that no longer exists. +- **`terminateService` is `nonReentrant`.** State writes already preceded external calls + (CEI), but the entrypoint now matches the rest of the lifecycle on defense-in-depth. The + same guard applies to `terminateServiceForNonPayment`. +- **`approveService` rejects past-grace requests.** Once a request is past + `createdAt + requestExpiryGracePeriod`, operators can no longer activate it. This closes the + race where an operator could front-run the requester's `expireServiceRequest` cleanup tx and + quietly spawn a stale service. +- **`requestService*` rejects duplicate operators.** Submitting the same operator address more + than once now reverts. With duplicates `operatorCount` exceeded the unique approver count, + so `approvalCount == operatorCount` was unreachable and the request could only be cleaned up + via `expireServiceRequest`. +- **`forceRemoveOperator` respects `minOperators` by default.** A blueprint manager can no + longer evict honest operators below `minOperators` unless the BSM explicitly opts in via + the new `forceRemoveAllowsBelowMin(serviceId)` hook (default: `false`). Blueprints that + genuinely need emergency-eviction-below-min must override that hook to return `true`. + ## Safety Recommendations For Stateful Or Custodial Blueprints If your blueprint holds user funds, tokens, or state that must remain withdrawable, assume that: diff --git a/pages/developers/slashing.mdx b/pages/developers/slashing.mdx index 91bd87e..1530e7f 100644 --- a/pages/developers/slashing.mdx +++ b/pages/developers/slashing.mdx @@ -35,7 +35,7 @@ The slashing path is implemented in: Each transition has a single responsible caller and a single state effect. -### `proposeSlash(serviceId, operator, slashBps, evidence)` +### `proposeSlash(serviceId, operator, slashBps, evidence)` (`nonReentrant`) Anyone the protocol accepts as a slasher creates a `SlashProposal`. The proposal: @@ -47,18 +47,29 @@ Anyone the protocol accepts as a slasher creates a `SlashProposal`. The proposal If the operator already has `maxPendingSlashesPerOperator` pending, the call reverts. Default cap is 32. -### `disputeSlash(slashId, reason)` `payable` +`evidence` MUST be non-zero. The contract rejects `bytes32(0)` evidence so off-chain monitors keying off +non-zero evidence never observe silently-zero entries. + +### `disputeSlash(slashId, reason)` `payable` (`nonReentrant`) The operator (or `SLASH_ADMIN_ROLE`) contests the slash within the dispute window. The operator must post `config.disputeBond` in native asset. `SLASH_ADMIN` posts no bond. The dispute snapshots `config.disputeResolutionDeadline` onto the proposal (`disputeDeadline = block.timestamp + deadline`), so a later admin-driven shrink of the live config cannot retroactively shorten the operator's review window. +A `SLASH_ADMIN_ROLE` holder that is also the `proposer` of a slash CANNOT dispute their own +proposal. Without this, a single role-holder could propose, immediately self-dispute (no bond), +and freeze operator stake for the full `disputeResolutionDeadline` window — and (when +`treasury == admin`) capture the operator's bond on auto-execution. To dispute as +`SLASH_ADMIN`, route the dispute through a different admin-keyed account or the operator. + ### `executeSlash(slashId)` (permissionless) Anyone calls this. The proposal's `isExecutable` check accepts: - A `Pending` proposal whose `executeAfter + TIMESTAMP_BUFFER` has elapsed -- A `Disputed` proposal whose snapshotted `disputeDeadline` has elapsed +- A `Disputed` proposal whose `disputeDeadline + TIMESTAMP_BUFFER` has elapsed (symmetric 15s + buffer with the dispute window, so a sequencer with timestamp influence cannot sandwich the + deadline tick to land an `executeSlash` before the operator's dispute window closes) Execution routes through `_executeSlashOnStaking`: diff --git a/pages/infrastructure/protocol-deployment.mdx b/pages/infrastructure/protocol-deployment.mdx index 862f2cb..17290cf 100644 --- a/pages/infrastructure/protocol-deployment.mdx +++ b/pages/infrastructure/protocol-deployment.mdx @@ -36,7 +36,13 @@ If you operate a production environment, keep these items in your internal runbo - the `FULL_DEPLOY_CONFIG` JSON you used (and any env var overrides) - the manifest output path and the produced manifest JSON - role handoff targets (timelock, multisig, treasury) and whether bootstrap roles were revoked -- any cross-chain slashing wiring (Hyperlane or LayerZero) and the connector and receiver manifests +- any cross-chain slashing wiring (Hyperlane or LayerZero) and the connector and receiver manifests. On + `L2SlashingReceiver`, the bootstrap `setMessenger` / `setSlasher` calls (when the current value is + `address(0)`) take effect immediately. **Subsequent rotations are timelocked**: the owner queues the new + address with `setMessenger` / `setSlasher`, then calls `activateMessenger()` / + `activateSlasher()` after `SENDER_ACTIVATION_DELAY` (2 days) elapses. Document the queue/activate + pair as two transactions in your runbook so operators know to schedule the activation tx before the + delay window closes. ## Related Docs diff --git a/pages/network/metrics-and-scoring.mdx b/pages/network/metrics-and-scoring.mdx index dc0bb6c..a20ffb7 100644 --- a/pages/network/metrics-and-scoring.mdx +++ b/pages/network/metrics-and-scoring.mdx @@ -22,7 +22,11 @@ Depending on what is configured on-chain, the protocol can record: - **Service activity**: service creation/termination, job calls, job completion success rates. - **Payments**: total fees paid by customers. - **Operator liveness**: heartbeats for active services. -- **Slashing**: executed slash events (and the slashed amount). +- **Slashing**: the full slashing lifecycle is observable via `ITangleSlashing` events: + `SlashProposed`, `SlashDisputed`, `SlashExecuted`, `SlashCancelled`, and + `SlashConfigUpdated`. `TangleMetrics` records executed slashes (count and slashed amount) + for incentive accounting; indexers that need full lifecycle context (proposers, + dispute reasons, config history) should subscribe to all five events directly. ## Staker “Exposure” Scoring diff --git a/pages/operators/pricing/overview.mdx b/pages/operators/pricing/overview.mdx index 3226d60..980da19 100644 --- a/pages/operators/pricing/overview.mdx +++ b/pages/operators/pricing/overview.mdx @@ -44,6 +44,14 @@ These flows require operators to generate and sign EIP-712 quotes off-chain: If the customer requests a specific confidentiality mode for the service, that confidentiality intent is part of the service RFQ agreement and should be reflected in your quote and operating cost assumptions. +**Quote requester binding (v0.13.0).** Both `QuoteDetails` (service quotes) and +`JobQuoteDetails` (per-job quotes) now have `requester` as the FIRST field of the EIP-712 +typed data. The protocol enforces `requester == msg.sender` and rejects wildcard +`requester == address(0)`. Your quote server MUST sign per-caller quotes with `requester` +populated to the customer's address, otherwise the customer cannot redeem your quote +on-chain. This also defeats mempool front-running: another `permittedCaller` (or anyone +watching the mempool) can no longer lift your signed digest and consume it. + To participate in RFQ, you need: - a quote serving endpoint (typically the `pricing-engine` gRPC server) diff --git a/pages/release-notes/0.13.0.mdx b/pages/release-notes/0.13.0.mdx new file mode 100644 index 0000000..e82f70a --- /dev/null +++ b/pages/release-notes/0.13.0.mdx @@ -0,0 +1,204 @@ +--- +title: tnt-core v0.13.0 +description: Quote requester binding, slashing event reshape, BSM hooks, and L2 slashing-receiver timelocks. +--- + +# tnt-core v0.13.0 + +This release cuts in two breaking changes for any off-chain service that signs RFQ +quotes or decodes slashing events, plus a set of lifecycle and cross-chain hardening +fixes from the round-2 audit pass. Upstream PRs: +[tnt-core#124](https://github.com/tangle-network/tnt-core/pull/124), +[tnt-core#125](https://github.com/tangle-network/tnt-core/pull/125). + +If you maintain operator software that signs quotes, an indexer that decodes slash +events, a custom BSM, or an L2 slashing receiver, treat this as a required upgrade. + +## Breaking Changes + +### EIP-712 quote binding (`QuoteDetails`, `JobQuoteDetails`) + +Both quote structs now carry `address requester` as the **first** field of the EIP-712 +typed data. The contract enforces `requester == msg.sender` on +`createServiceFromQuotes` / `extendServiceFromQuotes` / `submitJobFromQuote` and rejects +wildcard `requester == address(0)`. + +The new typehash strings are: + +``` +QuoteDetails(address requester,uint64 blueprintId,uint64 ttlBlocks,uint256 totalCost,uint64 timestamp,uint64 expiry,uint8 confidentiality,AssetSecurityCommitment[] securityCommitments,ResourceCommitment[] resourceCommitments) + +JobQuoteDetails(address requester,uint64 serviceId,uint8 jobIndex,uint256 price,uint64 timestamp,uint64 expiry,uint8 confidentiality) +``` + +Previously `requester` lived on `QuoteDetails` but was excluded from the typehash, so a +mempool observer could rewrite `details.requester` to themselves and the operator's +signature still recovered. `JobQuoteDetails` had no `requester` at all and any +`permittedCaller` could lift another caller's signed digest. Both are now bound at the +typehash level. + +**Action for operator software:** + +- Add `requester` to the `QuoteDetails` typed-data hash as the first member. +- Add `requester` to the `JobQuoteDetails` typed-data hash as the first member. +- Sign per-caller quotes; do **not** emit wildcard quotes — they are rejected on-chain. +- Pre-fix signatures fail signature recovery against the new typehash and must be regenerated. + +See [pricing & payments](/developers/blueprints/pricing-and-payments) for the updated +struct shapes and a copyable typed-data example. + +### `Types.ServiceRequest.activated` reordered + +The `activated` flag was moved to the **end** of the `ServiceRequest` struct so a +hypothetical upgrade from a pre-`activated` storage layout cannot accidentally read a +non-zero byte from a different field as `activated == true`. ABI consumers regenerate +from the new bindings. + +### `ITangleSlashing` event shapes + +`ITangleSlashing` now declares the events the protocol actually emits from `SlashingLib`. +Before this release, Rust bindings and indexers wired to `ITangleSlashing` could not decode +any slash event because the interface declared smaller, legacy shapes. + +| Event | Old fields | New fields | +| -------------------- | -------------- | --------------------------------------------------------------------------------------------------------------- | +| `SlashProposed` | 4 | 8 — `slashId`, `serviceId`, `operator`, `proposer`, `slashBps`, `effectiveSlashBps`, `evidence`, `executeAfter` | +| `SlashExecuted` | 3 | 4 — `slashId`, `serviceId`, `operator`, `actualSlashed` | +| `SlashDisputed` | (not declared) | `slashId`, `disputer`, `reason` | +| `SlashCancelled` | (not declared) | `slashId`, `canceller`, `reason` | +| `SlashConfigUpdated` | (not declared) | full `SlashConfig` tuple (6 fields) | + +`getSlashConfig()` returns the full 6-field `SlashConfig`. `setSlashConfig` takes 6 args: +the existing `(disputeWindow, instantSlashEnabled, maxSlashBps)` plus +`(disputeResolutionDeadline, disputeBond, maxPendingSlashesPerOperator)`. + +`proposeSlash` parameter `uint256 amount` is now `uint16 slashBps` (basis points). +`disputeSlash` is `external payable` (so callers can pass `disputeBond` as native value). + +See [`ITangleSlashing`](/developers/api/reference/ITangleSlashing) and +[Slashing](/developers/slashing) for the updated lifecycle and config matrix. + +### BSM hook: `forceRemoveAllowsBelowMin(uint64) -> bool` + +`IBlueprintServiceManager` adds a new view: + +```solidity +function forceRemoveAllowsBelowMin(uint64 serviceId) external view returns (bool ok); +``` + +Default in `BlueprintServiceManagerBase` is `false` — the protocol enforces +`operatorCount > minOperators` on `forceRemoveOperator`. Custom BSMs that do **not** +inherit `BlueprintServiceManagerBase` MUST implement this hook explicitly; an +unimplemented or reverting view fails closed and the eviction reverts as soon as it +would push the service below `minOperators`. + +Without this gate, a malicious blueprint manager could evict honest operators below +the configured floor and bias the operator set toward sybils. + +## Hardening (non-breaking, behavior-changing) + +### Slashing + +- `proposeSlash` and `disputeSlash` now carry `nonReentrant`. Previously only + `executeSlash`, `executeSlashBatch`, and `cancelSlash` were guarded. +- `proposeSlash` rejects `bytes32(0)` evidence so off-chain monitors keying off + non-zero evidence don't see silently-zero entries. +- Disputed slashes use the same 15-second `TIMESTAMP_BUFFER` as Pending slashes. + `executeSlash` for a `Disputed` proposal now requires + `disputeDeadline + TIMESTAMP_BUFFER` to have elapsed. +- A `SLASH_ADMIN_ROLE` holder that is also the `proposer` of a slash CANNOT dispute + their own proposal. + +### Service lifecycle + +- Every operator-exit entrypoint (`scheduleExit`, `executeExit`, `forceExit`, + `leaveService`, `forceRemoveOperator`) reverts when the service is no longer + `Active`. +- `terminateService` and `terminateServiceForNonPayment` carry `nonReentrant`. +- `approveService` rejects requests past `createdAt + requestExpiryGracePeriod`. +- `requestService*` rejects duplicate operator entries. + +### MBSM registry + +- `MBSMRegistry.pinBlueprint` rejects revisions currently inside the deprecation + grace window. Pinning to a deprecated revision would break every BSM call for the + pinned blueprint the moment `completeDeprecation` ran. + +### L2 slashing receiver + +- `setMessenger` and `setSlasher` are timelock-gated for non-bootstrap rotations + (2-day `SENDER_ACTIVATION_DELAY`). The first write (when the current value is + `address(0)`) is a bootstrap exemption so deploy scripts can wire the bridge + without a 2-day deadlock. +- New `activateMessenger()` / `activateSlasher()` consume the queued swap after the + delay elapses. +- `receiveMessage` reverts when the L2 slasher returns `canSlash == false` or when + `slashBps == 0`, **before** consuming the bridge nonce. Previously the nonce was + marked processed first and a transient failure silently dropped the slash with no + retry path. With CEI fixed the bridge keeps the message available for retry. +- New `SlashingNotPossible(address operator)` error distinguishes the + retry-after-condition-clears case from a real misconfiguration. + +### Beacon SSZ encoding + +`BeaconChainProofs` `getEffectiveBalanceGwei`, `getActivationEpoch`, `getExitEpoch`, +`getWithdrawableEpoch`, and `_extractBalanceFromLeaf` now perform the canonical +little-endian byte-swap on SSZ-packed `uint64` fields. EigenPod-CLI fixtures decode +correctly out of the box; hand-rolled proof builders that pack values into the low 8 +bytes of the chunk (or use big-endian) will be rejected. Real EigenPod proofs would +silently mis-account every `uint64` field — every effective balance, exit epoch, and +validator balance — under the previous code. + +If you maintain a proof builder, regenerate fixtures with the canonical SSZ packing +and pin the 32-ETH leaf regression test that ships with v0.13.0. + +### Other fixes + +- `TNTLockFactory.getOrCreateLock` requires `msg.sender == beneficiary`. Without this + gate, a third party could front-run the victim's first interaction with a lock, + supply themselves as `delegatee`, and persistently capture the victim's voting + power for every future inbound TNT transfer to the deterministic lock address. +- `_distributePaymentWithEffectiveExposure` reverts (instead of silently retaining + funds) when there are zero active operators at billing time. +- `fundService`, `billSubscription`, and `billSubscriptionBatch` respect the global + pause. Reward / refund claim paths remain unguarded so users can always exit. +- `OperatorStatusRegistry.registerOperator` resets all per-`(serviceId, operator)` + heartbeat / metrics state on (re-)register. +- `LiquidDelegationVault.requestRedeem` rejects `controller == address(0)`. + +## Migration Checklist + +1. **Operator quote servers**: regenerate signatures with `requester` populated as the + first field of `QuoteDetails` and `JobQuoteDetails`. Stop signing wildcard quotes. +2. **Indexers / Rust binding consumers**: regenerate from + [`tnt-core-bindings` v0.13.0](https://crates.io/crates/tnt-core-bindings) and + re-decode slash events against the new shapes (`SlashProposed` 8 fields, + `SlashExecuted` 4 fields, plus `SlashDisputed`/`SlashCancelled`/`SlashConfigUpdated`). +3. **Custom BSMs not inheriting `BlueprintServiceManagerBase`**: implement + `forceRemoveAllowsBelowMin(uint64) -> bool` (return `false` unless you genuinely need + emergency-eviction-below-min). +4. **MBSM operators**: do not pin a blueprint to a revision that is in the deprecation + grace window — `pinBlueprint` will revert. Wait until the deprecation completes or + pick a different revision. +5. **L2 slashing receiver operators**: budget for two-step rotations of `messenger` + and `slasher`. Queue with `setMessenger` / `setSlasher`, then schedule + `activateMessenger()` / `activateSlasher()` 2 days later. +6. **Beacon proof builders**: regenerate SSZ fixtures with canonical + little-endian packing for `uint64` fields. +7. **`SLASH_ADMIN` operators**: a SLASH_ADMIN that is also the proposer can no longer + self-dispute their own slash. Route disputes through a different admin-keyed + account or the operator. + +## Reference + +- [`ITangleSlashing`](/developers/api/reference/ITangleSlashing) +- [`IBlueprintServiceManager`](/developers/api/reference/IBlueprintServiceManager) +- [`IMBSMRegistry`](/developers/api/reference/IMBSMRegistry) +- [Slashing](/developers/slashing) +- [Auth Surface](/developers/auth-surface) +- [Service Lifecycle](/developers/blueprints/service-lifecycle) +- [Pricing and Payments](/developers/blueprints/pricing-and-payments) +- tnt-core PR [#124](https://github.com/tangle-network/tnt-core/pull/124), + [#125](https://github.com/tangle-network/tnt-core/pull/125) +- Full bindings changelog: + [`tnt-core-bindings/CHANGELOG.md`](https://github.com/tangle-network/tnt-core/blob/main/bindings/CHANGELOG.md) diff --git a/pages/release-notes/_meta.ts b/pages/release-notes/_meta.ts new file mode 100644 index 0000000..4ddcc52 --- /dev/null +++ b/pages/release-notes/_meta.ts @@ -0,0 +1,7 @@ +import type { Meta } from "nextra"; + +const meta: Meta = { + "0.13.0": "v0.13.0", +}; + +export default meta;