Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions docs/discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,30 @@ described in RFC 2119.
- **Content addressing locates as well as verifies.** A `BZZ_MANIFEST`
commitment already names where the bytes live, so a handler publishes no URI
and no gateway is written into a deployed contract. Retrieval is the host's
concern. URIs exist for `TAR_ZST`, where the digest locates nothing.
concern. URIs exist for `SHA256`, where the digest locates nothing.
- **Fail closed.** Unknown URI schemes, unsupported or reverting views, and
unverifiable documents are treated as "no discovery", never as errors to
retry aggressively and never as data to trust.

## 0. Commitments

Descriptors (§1) and modules (§2) are both committed to on-chain as a
`(bytes32 digest, PackageKind kind)` pair. `kind` names how the bytes are
packaged and therefore how the digest is computed and verified.
`(bytes32 digest, PackageKind kind)` pair. `kind` names how the digest is
computed, and therefore how it is verified. What the committed bytes *are* is
fixed per surface rather than by the kind: a module commits to a `.tar.zst`
package, a descriptor to its JSON document.

```solidity
enum PackageKind {
BZZ_MANIFEST, // mantaray manifest root
TAR_ZST // sha256 of a .tar.zst archive
BZZ_MANIFEST, // Swarm BMT root
SHA256 // sha256 over the published bytes
}
```

The two kinds trade the same two properties against each other, and a
publisher picks which one it wants:

| | `BZZ_MANIFEST` | `TAR_ZST` |
| | `BZZ_MANIFEST` | `SHA256` |
|---|---|---|
| locates itself | yes, the root is the Swarm reference | no, a URI is required |
| verification | per entry, from BMT roots in the manifest | whole archive, in one pass |
Expand All @@ -81,7 +83,7 @@ publisher picks which one it wants:

The asymmetry is structural rather than a preference. A mantaray root is a
hash over a *structure*, so only Swarm can recompute it; that is exactly what
buys per-entry verification and costs cross-scheme mirroring. A `TAR_ZST`
buys per-entry verification and costs cross-scheme mirroring. A `SHA256`
digest is a hash over *bytes*, so any transport can be checked by hashing what
it delivered, at the cost of verifying nothing until the whole archive is in
hand.
Expand All @@ -93,12 +95,13 @@ the exact bytes published.
### 0.1 Location

`BZZ_MANIFEST` is content-addressed: the commitment names the bytes, so it also
locates them. The URI list SHOULD be empty and a host resolves the digest
through whatever Swarm access it has. A handler that lists URIs anyway supplies
non-normative hints; a host MAY use them and MUST still verify against the
commitment.
locates them. Its URI list MUST be empty, and implementations reject a
commitment that carries one. A URI here would not merely be redundant: a
gateway serves resolved file content rather than chunks, so bytes fetched from
one cannot be checked against a structure root at all. Publishing one would
write a gateway into a deployed contract in exchange for nothing.

`TAR_ZST` is not self-locating. Its URI list MUST be non-empty. Any scheme may
`SHA256` is not self-locating. Its URI list MUST be non-empty. Any scheme may
appear there, including `ipfs://`, which verifies its own content in transit
but whose CID is not the commitment: a UnixFS CID depends on chunker and DAG
layout, which are publisher settings rather than properties of the content, so
Expand All @@ -113,8 +116,10 @@ absent rather than fetching anything.
the manifest carries for it. A single document (a descriptor) is committed to
as a leaf rather than a manifest.

`TAR_ZST`: fetch the archive from any URI, verify `sha256(archive)` equals the
digest, then extract. Verifying the archive authenticates every byte in it, so
`SHA256`: fetch the bytes from any URI and verify their `sha256` equals the
digest. For a descriptor those bytes are the document and verification ends
there. For a module they are the `.tar.zst` package, which is then extracted;
verifying the archive authenticates every byte in it, so
no per-entry digest is declared anywhere. Restating entry digests inside the
package would create a second source of truth and a disagreement to specify
around, without adding a check.
Expand All @@ -129,10 +134,12 @@ Consumers MUST, before writing anything:

### 0.3 Compression

Each container compresses at the level it can, so the component is stored
differently in each and `nexum.toml` is byte-identical across both:
This concerns module packages; a descriptor is a single document and is
published as-is. Each container compresses at the level it can, so the
component is stored differently in each while `nexum.toml` stays
byte-identical across both:

- `TAR_ZST`: entries are stored uncompressed; the archive compresses them.
- `SHA256`: entries are stored uncompressed; the archive compresses them.
- `BZZ_MANIFEST`: mantaray has no archive-level compression, so the component
is stored zstd-compressed at `<component>.zst`, which is worth doing on a
network that charges per chunk. `nexum.toml` is stored uncompressed: it is
Expand All @@ -157,7 +164,7 @@ interface IOrderDescriptor {
/**
* @notice Locations of the handler descriptor document.
* @dev Empty for `BZZ_MANIFEST`, which the commitment locates. Required
* for `TAR_ZST`. Any URI listed is a hint: all MUST resolve to the
* for `SHA256`. Any URI listed is a hint: all MUST resolve to the
* same document bytes, never alternative content.
*/
function descriptorURI() external view returns (string[] memory uris);
Expand Down Expand Up @@ -196,17 +203,17 @@ governs retrieval only. Where a URI is used it MUST be one of:
| Scheme | Used with |
|---|---|
| `bzz://` | `BZZ_MANIFEST`, as a hint; the commitment already locates the document |
| `ipfs://` | `TAR_ZST`, as a location; the CID is not the commitment |
| `ipfs://` | `SHA256`, as a location; the CID is not the commitment |
| `data:` | either kind, for a document published in-band |
| `https:` | `TAR_ZST`, the common case |
| `https:` | `SHA256`, the common case |

`http:`, `file:`, and any URI resolving to loopback, link-local, or private
address ranges are prohibited. Fetchers SHOULD disable redirects (or re-validate
every hop against this policy), enforce a size cap (256 KiB RECOMMENDED), and
enforce a total timeout.

An `https:` URI is never trusted on its own: bytes fetched from one are used
only after they verify against the commitment, which for `TAR_ZST` is the whole
only after they verify against the commitment, which for `SHA256` is the whole
of the integrity argument.

### 1.3 Document
Expand Down Expand Up @@ -298,7 +305,7 @@ interface IOrderModule {
/**
* @notice Locations of the module package.
* @dev Empty for `BZZ_MANIFEST`, which the commitment locates. Required
* for `TAR_ZST`. Any URI listed is a hint.
* for `SHA256`. Any URI listed is a hint.
*/
function moduleURI() external view returns (string[] memory uris);

Expand Down Expand Up @@ -494,9 +501,9 @@ pair = "WETH/USDC"
#### Execution

1. Resolve the commitment (§0.1) and verify it (§0.2), applying the extraction
rules for `TAR_ZST`.
rules for `SHA256`.
2. Read `nexum.toml`. Under `BZZ_MANIFEST` this is one entry, verified and
parsed before the component is fetched at all; under `TAR_ZST` the whole
parsed before the component is fetched at all; under `SHA256` the whole
archive is already in hand.
3. Fetch the component, decompressing it under `BZZ_MANIFEST` (§0.3), and
instantiate it in the `videre:ccow/order-module` world with HTTP restricted
Expand Down
7 changes: 4 additions & 3 deletions script/deploy_AnvilStack.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {TWAP} from "../src/types/twap/TWAP.sol";
import {GoodAfterTime} from "../src/types/GoodAfterTime.sol";
import {PerpetualStableSwap} from "../src/types/PerpetualStableSwap.sol";
import {TradeAboveThreshold} from "../src/types/TradeAboveThreshold.sol";
import {PackageKind} from "../src/interfaces/PackageKind.sol";

contract DeployAnvilStack is Script {
// --- constants
Expand Down Expand Up @@ -61,9 +62,9 @@ contract DeployAnvilStack is Script {

// deploy the Composable CoW
ComposableCow composableCow = new ComposableCow(address(settlement));
new TWAP(composableCow);
new GoodAfterTime();
new PerpetualStableSwap();
new TWAP(composableCow, new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new GoodAfterTime(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new PerpetualStableSwap(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new TradeAboveThreshold();

vm.stopBroadcast();
Expand Down
7 changes: 4 additions & 3 deletions script/deploy_OrderTypes.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ import {TWAP} from "../src/types/twap/TWAP.sol";
import {GoodAfterTime} from "../src/types/GoodAfterTime.sol";
import {PerpetualStableSwap} from "../src/types/PerpetualStableSwap.sol";
import {TradeAboveThreshold} from "../src/types/TradeAboveThreshold.sol";
import {PackageKind} from "../src/interfaces/PackageKind.sol";

contract DeployOrderTypes is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address composableCow = vm.envAddress("COMPOSABLE_COW");
vm.startBroadcast(deployerPrivateKey);

new TWAP(ComposableCow(composableCow));
new GoodAfterTime();
new PerpetualStableSwap();
new TWAP(ComposableCow(composableCow), new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new GoodAfterTime(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new PerpetualStableSwap(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new TradeAboveThreshold();

vm.stopBroadcast();
Expand Down
17 changes: 9 additions & 8 deletions script/deploy_ProdStack.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {StopLoss} from "../src/types/StopLoss.sol";

// Value factories
import {CurrentBlockTimestampFactory} from "../src/value_factories/CurrentBlockTimestampFactory.sol";
import {PackageKind} from "../src/interfaces/PackageKind.sol";

contract DeployProdStack is Script {
function run() external {
Expand All @@ -27,19 +28,19 @@ contract DeployProdStack is Script {
vm.startBroadcast(deployerPrivateKey);

// Deploy ExtensibleFallbackHandler
new ExtensibleFallbackHandler{salt: "v1.0.0"}();
new ExtensibleFallbackHandler{salt: bytes32(0)}();

// Deploy ComposableCow
ComposableCow composableCow = new ComposableCow{salt: "v1.0.0"}(settlement);
ComposableCow composableCow = new ComposableCow{salt: bytes32(0)}(settlement);

// Deploy order types
new TWAP{salt: "v1.0.0"}(composableCow);
new GoodAfterTime{salt: "v1.0.0"}();
new PerpetualStableSwap{salt: "v1.0.0"}();
new TradeAboveThreshold{salt: "v1.0.0"}();
new StopLoss{salt: "v1.0.0"}();
new TWAP{salt: bytes32(0)}(composableCow, new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new GoodAfterTime{salt: bytes32(0)}(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new PerpetualStableSwap{salt: bytes32(0)}(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);
new TradeAboveThreshold{salt: bytes32(0)}();
new StopLoss{salt: bytes32(0)}(new string[](0), bytes32(0), PackageKind.BZZ_MANIFEST);

// Deploy value factories
new CurrentBlockTimestampFactory{salt: "v1.0.0"}();
new CurrentBlockTimestampFactory{salt: bytes32(0)}();
}
}
2 changes: 1 addition & 1 deletion src/BaseConditionalOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ abstract contract BaseConditionalOrder is IConditionalOrderGenerator, IOrderMani
/**
* @inheritdoc IERC165
*/
function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) {
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IConditionalOrderGenerator).interfaceId
|| interfaceId == type(IOrderManifest).interfaceId || interfaceId == type(IERC165).interfaceId;
}
Expand Down
84 changes: 84 additions & 0 deletions src/OrderDescriptor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import {IOrderDescriptor} from "./interfaces/IOrderDescriptor.sol";
import {PackageKind} from "./interfaces/PackageKind.sol";
import {BaseConditionalOrder} from "./BaseConditionalOrder.sol";

/**
* @title Order Descriptor mixin - opt-in descriptor commitment for handlers
* @author mfw78 <mfw78@nxm.rs>
* @dev Immutable by omission: there is no setter, and `DescriptorUpdate` is
* emitted exactly once, from the constructor. Deployments that support
* rotation add their own access-controlled setter and re-emit.
*
* A handler constructed with no URIs does NOT advertise
* `IOrderDescriptor` - feature detection stays honest for deployments
* that predate their descriptor document; committing requires a
* redeployment (the descriptor digest is per-deployment anyway).
*/
abstract contract OrderDescriptor is IOrderDescriptor, BaseConditionalOrder {
/**
* @dev URIs cannot be published without a commitment to verify them against
*/
error UncommittedDescriptorURI();

/**
* @dev `SHA256` does not locate the document, so it requires a URI
*/
error DescriptorURIRequired();

/**
* @dev A `BZZ_MANIFEST` commitment locates its own document; a URI could not
* be verified against a structure root anyway
*/
error DescriptorURINotUsed();

string[] private _descriptorUris;
bytes32 private immutable _DESCRIPTOR_DIGEST;
PackageKind private immutable _DESCRIPTOR_KIND;

constructor(string[] memory uris, bytes32 digest, PackageKind kind) {
if (digest != bytes32(0)) {
if (kind == PackageKind.SHA256) {
require(uris.length > 0, DescriptorURIRequired());
} else {
require(uris.length == 0, DescriptorURINotUsed());
}
emit DescriptorUpdate(uris, digest, kind);
} else {
require(uris.length == 0, UncommittedDescriptorURI());
}
_descriptorUris = uris;
_DESCRIPTOR_DIGEST = digest;
_DESCRIPTOR_KIND = kind;
}

/**
* @inheritdoc IOrderDescriptor
*/
function descriptorURI() external view returns (string[] memory uris) {
return _descriptorUris;
}

/**
* @inheritdoc IOrderDescriptor
*/
function descriptorCommitment() external view returns (bytes32 digest, PackageKind kind) {
return (_DESCRIPTOR_DIGEST, _DESCRIPTOR_KIND);
}

/**
* @dev Advertise `IOrderDescriptor` only when a descriptor is committed:
* claiming the interface while returning empty values is
* non-conformant per the discovery specification. The commitment is
* the gate, not the URI list, since a `BZZ_MANIFEST` commitment
* locates its own document and publishes no URI.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
if (interfaceId == type(IOrderDescriptor).interfaceId) {
return _DESCRIPTOR_DIGEST != bytes32(0);
}
return super.supportsInterface(interfaceId);
}
}
76 changes: 76 additions & 0 deletions src/OrderModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import {IOrderModule} from "./interfaces/IOrderModule.sol";
import {PackageKind} from "./interfaces/PackageKind.sol";
import {BaseConditionalOrder} from "./BaseConditionalOrder.sol";

/**
* @title Order Module mixin - opt-in module commitment for handlers
* @author mfw78 <mfw78@nxm.rs>
* @dev Immutable by omission, as for `OrderDescriptor`. The commitment is what
* advertises `IOrderModule`, not the URI list: a `BZZ_MANIFEST`
* commitment locates its own package and so publishes no URI. Constructed
* with a zero digest, the handler does not advertise the interface.
*/
abstract contract OrderModule is IOrderModule, BaseConditionalOrder {
/**
* @dev URIs cannot be published without a commitment to verify them against
*/
error UncommittedModuleURI();

/**
* @dev `SHA256` does not locate the package, so it requires a URI
*/
error ModuleURIRequired();

/**
* @dev A `BZZ_MANIFEST` commitment locates its own package; a URI could not
* be verified against a structure root anyway
*/
error ModuleURINotUsed();

string[] private _moduleUris;
bytes32 private immutable _MODULE_DIGEST;
PackageKind private immutable _MODULE_KIND;

constructor(string[] memory uris, bytes32 digest, PackageKind kind) {
if (digest != bytes32(0)) {
if (kind == PackageKind.SHA256) {
require(uris.length > 0, ModuleURIRequired());
} else {
require(uris.length == 0, ModuleURINotUsed());
}
emit ModuleUpdate(uris, digest, kind);
} else {
require(uris.length == 0, UncommittedModuleURI());
}
_moduleUris = uris;
_MODULE_DIGEST = digest;
_MODULE_KIND = kind;
}

/**
* @inheritdoc IOrderModule
*/
function moduleURI() external view returns (string[] memory uris) {
return _moduleUris;
}

/**
* @inheritdoc IOrderModule
*/
function moduleCommitment() external view returns (bytes32 digest, PackageKind kind) {
return (_MODULE_DIGEST, _MODULE_KIND);
}

/**
* @dev Advertise `IOrderModule` only when a module is committed
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
if (interfaceId == type(IOrderModule).interfaceId) {
return _MODULE_DIGEST != bytes32(0);
}
return super.supportsInterface(interfaceId);
}
}
Loading
Loading