From 785fd97dbeb836462a4255a15b6df3df3c8b4d39 Mon Sep 17 00:00:00 2001 From: Josh Hardy Date: Mon, 16 Feb 2026 14:51:32 +0000 Subject: [PATCH 1/6] feat: add oracle-url field to orders spec Orders can now specify an optional oracle-url that points to a signed context oracle server. When present, tooling encodes a SignedContextOracleV1 metadata item into the order's RainMetaDocumentV1, enabling oracle discovery by takers and indexers. --- ob-yaml.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/ob-yaml.md b/ob-yaml.md index bce1d22..c598003 100644 --- a/ob-yaml.md +++ b/ob-yaml.md @@ -280,6 +280,23 @@ Required fields: Optional fields: - `deployer` (defaults to network deployer if unambiguous, otherwise required) - `orderbook` (defaults to network orderbook if unambiguous, otherwise required) +- `oracle-url` (URL of a signed context oracle server, see below) + +### Oracle URL + +Orders that require external data (e.g. price feeds) at take-time can specify an `oracle-url`. This URL points to a server that returns `SignedContextV1` data, which takers fetch and include when taking the order. + +When `oracle-url` is specified, the tooling encodes a `SignedContextOracleV1` metadata item (magic `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1`. This allows the oracle endpoint to be discovered onchain by takers and indexers. + +The oracle server MUST respond to `GET` requests and return a JSON object matching the `SignedContextV1` struct: + +```json +{ + "signer": "0x...", + "context": ["0x...", "0x..."], + "signature": "0x..." +} +``` ``` orders: @@ -307,6 +324,14 @@ orders: vault-id: 99 - token: polygon-usdt vault-id: 0xabcd + oracle-order: + oracle-url: https://my-oracle-server.example.com/context + inputs: + - token: eth-weth + vault-id: 1 + outputs: + - token: eth-usdc + vault-id: 1 ``` ### front matter scenarios From 825a43f20c0d980583578379798810f7919c6458 Mon Sep 17 00:00:00 2001 From: Josh Hardy Date: Mon, 16 Feb 2026 16:05:18 +0000 Subject: [PATCH 2/6] fix: clarify oracle-url is used when taking or clearing orders The signed context is provided by the caller during take/clear, not just 'at take-time'. Updated wording to be precise about the usage context. --- ob-yaml.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ob-yaml.md b/ob-yaml.md index c598003..d60569a 100644 --- a/ob-yaml.md +++ b/ob-yaml.md @@ -284,9 +284,9 @@ Optional fields: ### Oracle URL -Orders that require external data (e.g. price feeds) at take-time can specify an `oracle-url`. This URL points to a server that returns `SignedContextV1` data, which takers fetch and include when taking the order. +Orders that require external data (e.g. price feeds) can specify an `oracle-url`. This URL points to a server that returns `SignedContextV1` data. The signed context is provided by the caller when taking or clearing the order — the order's rainlang can then read this data from the signed context columns during evaluation. -When `oracle-url` is specified, the tooling encodes a `SignedContextOracleV1` metadata item (magic `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1`. This allows the oracle endpoint to be discovered onchain by takers and indexers. +When `oracle-url` is specified, the tooling encodes a `SignedContextOracleV1` metadata item (magic `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1`. This allows the oracle endpoint to be discovered onchain by anyone who needs to take or clear the order (e.g. Raindex bots, the webapp, or other takers). The oracle server MUST respond to `GET` requests and return a JSON object matching the `SignedContextV1` struct: From eb414189f0769b42f5729e8dcf203cf9f43921d2 Mon Sep 17 00:00:00 2001 From: Josh Hardy Date: Mon, 23 Feb 2026 10:38:10 +0000 Subject: [PATCH 3/6] feat: add standalone signed context oracle spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a dedicated spec (signed-context-oracle.md) defining the full protocol for signed context oracle servers compatible with Rain orderbook: - POST endpoint with ABI-encoded request body - (OrderV4, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty) - SignedContextV1 JSON response format - EIP-191 signing requirements - Context layout conventions (Rain DecimalFloat) - Price direction, expiry, security considerations Updates ob-yaml.md oracle-url section to reference the standalone spec instead of inlining protocol details. Also fixes: - GET → POST (matches implementation) - SignedContextOracleV1 → RaindexSignedContextOracleV1 (matches rename) --- ob-yaml.md | 14 +--- signed-context-oracle.md | 177 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 signed-context-oracle.md diff --git a/ob-yaml.md b/ob-yaml.md index d60569a..8f1e41c 100644 --- a/ob-yaml.md +++ b/ob-yaml.md @@ -284,19 +284,11 @@ Optional fields: ### Oracle URL -Orders that require external data (e.g. price feeds) can specify an `oracle-url`. This URL points to a server that returns `SignedContextV1` data. The signed context is provided by the caller when taking or clearing the order — the order's rainlang can then read this data from the signed context columns during evaluation. +Orders that require external data (e.g. price feeds) can specify an `oracle-url`. This URL points to a server implementing the [Signed Context Oracle protocol](./signed-context-oracle.md). The signed context is provided by the caller when taking or clearing the order — the order's Rainlang expression can then read this data from the signed context columns during evaluation. -When `oracle-url` is specified, the tooling encodes a `SignedContextOracleV1` metadata item (magic `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1`. This allows the oracle endpoint to be discovered onchain by anyone who needs to take or clear the order (e.g. Raindex bots, the webapp, or other takers). +When `oracle-url` is specified, the tooling encodes a `RaindexSignedContextOracleV1` metadata item (magic `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1`. This allows the oracle endpoint to be discovered on-chain by anyone who needs to take or clear the order (e.g. Raindex bots, the webapp, or other takers). -The oracle server MUST respond to `GET` requests and return a JSON object matching the `SignedContextV1` struct: - -```json -{ - "signer": "0x...", - "context": ["0x...", "0x..."], - "signature": "0x..." -} -``` +See the [Signed Context Oracle spec](./signed-context-oracle.md) for full protocol details including the POST request format, response schema, signing requirements, and security considerations. ``` orders: diff --git a/signed-context-oracle.md b/signed-context-oracle.md new file mode 100644 index 0000000..a98aaa5 --- /dev/null +++ b/signed-context-oracle.md @@ -0,0 +1,177 @@ +# Signed Context Oracle Spec + +This spec defines the protocol for a **signed context oracle server** compatible with Rain orderbook. Any server implementing this protocol can serve as a price oracle (or any external data source) for Raindex orders. + +## Overview + +A signed context oracle server provides signed data that is passed into `takeOrders4()` or `clear2()` as a `SignedContextV1` struct. The orderbook contract verifies the EIP-191 signature on-chain using OpenZeppelin's `SignatureChecker`, then makes the signed data available to the order's Rainlang expression via the signed context columns. + +``` +External Data Source → Oracle Server → SignedContextV1 JSON + ↓ +Client (bot/webapp) → POST request → receives signed context + ↓ +Orderbook Contract → verifies EIP-191 signature → Rainlang evaluation +``` + +## Endpoint + +The oracle server MUST expose a single `POST` endpoint. The URL of this endpoint is the `oracle-url` referenced in [ob-yaml orders](./ob-yaml.md). + +There is no `GET` fallback. The request body is always required. + +## Request + +### Method + +`POST` + +### Content-Type + +`application/octet-stream` + +### Body + +The request body is the **raw ABI-encoded bytes** of the following tuple: + +```solidity +abi.encode(OrderV4 order, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty) +``` + +Where: + +- **`order`** — the full `OrderV4` struct of the order being taken/cleared +- **`inputIOIndex`** — index into `order.validInputs[]` for the current IO pair +- **`outputIOIndex`** — index into `order.validOutputs[]` for the current IO pair +- **`counterparty`** — the address of the taker/clearer. Use `address(0)` when the counterparty is unknown (e.g. at quote time) + +ABI encoding is used because it is canonical — there are no JSON key ordering ambiguities, and the `OrderV4` struct contains nested arrays and bytes fields that are complex to serialize otherwise. + +### Solidity types + +```solidity +struct IOV2 { + address token; + bytes32 vaultId; +} + +struct EvaluableV4 { + address interpreter; + address store; + bytes bytecode; +} + +struct OrderV4 { + address owner; + EvaluableV4 evaluable; + IOV2[] validInputs; + IOV2[] validOutputs; + bytes32 nonce; +} +``` + +These are the canonical Rain orderbook types. Implementations SHOULD use generated bindings from the Rain orderbook ABI rather than manual encoding. + +## Response + +### Success (200) + +Content-Type: `application/json` + +```json +{ + "signer": "0x<20-byte address>", + "context": ["0x<32-byte hex>", "0x<32-byte hex>", ...], + "signature": "0x<65-byte hex>" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `signer` | `address` | The address that produced the signature. The order's Rainlang expression should check that the signer is a trusted address. | +| `context` | `bytes32[]` | Array of 32-byte values. These become the signed context column available to the Rainlang expression during evaluation. Values are typically encoded as Rain DecimalFloats. | +| `signature` | `bytes` | 65-byte EIP-191 signature (r ∥ s ∥ v) over `keccak256(abi.encodePacked(context[]))`. | + +### Error (400) + +```json +{ + "error": "", + "detail": "" +} +``` + +Standard error codes: + +| Code | Meaning | +|------|---------| +| `invalid_body` | The request body could not be ABI-decoded | +| `invalid_index` | `inputIOIndex` or `outputIOIndex` is out of bounds for the order | +| `unsupported_token_pair` | The input/output token pair is not supported by this oracle | + +Servers MAY define additional error codes. + +### Error (500) + +Internal server errors (e.g. upstream data source failure) SHOULD return a 500 with a JSON body in the same `{error, detail}` format. + +## Signing + +The signature MUST be an EIP-191 "personal sign" signature: + +1. Compute the message: `abi.encodePacked(context[0], context[1], ..., context[n])` (raw concatenation of the bytes32 values) +2. Hash the message: `hash = keccak256(packed)` +3. Sign using EIP-191: `sign("\x19Ethereum Signed Message:\n32" + hash)` + +This matches how the Rain orderbook contract verifies signatures via `LibContext.build`, which uses OpenZeppelin's `ECDSA.recover` with `toEthSignedMessageHash`. + +## Context Layout + +The `context` array is an ordered list of `bytes32` values that the Rainlang expression reads by index. The layout is oracle-specific — different oracles may return different data. + +### Recommended layout for price oracles + +| Index | Value | Encoding | +|-------|-------|----------| +| 0 | Price | Rain DecimalFloat | +| 1 | Expiry timestamp | Rain DecimalFloat (unix seconds) | + +**Rain DecimalFloat** is the standard numeric encoding used throughout Rain. Implementations SHOULD use the `rain_math_float` crate (or equivalent) for encoding rather than manual bit packing. + +### Expiry + +Oracle servers SHOULD include an expiry timestamp in the context. The order's Rainlang expression SHOULD check that the expiry has not passed: + +```rainlang +oracle-expiry: signed-context<0 1>(), +:ensure(greater-than(oracle-expiry now())); +``` + +Short expiry windows (e.g. 5-30 seconds) are recommended to prevent stale price usage. + +## Price Direction + +When the oracle serves a price feed for a specific token pair (e.g. ETH/USD), the server SHOULD inspect the order's `validInputs[inputIOIndex].token` and `validOutputs[outputIOIndex].token` to determine the correct price direction: + +- If input is the quote token and output is the base token → return price as-is +- If input is the base token and output is the quote token → return the inverse (1/price) + +This ensures the Rainlang expression always receives the price in the correct orientation for the IO ratio, without needing to handle inversion in Rainlang. + +## On-chain Discovery + +When an order specifies an `oracle-url`, the tooling encodes a `RaindexSignedContextOracleV1` metadata item (magic number `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1` at deployment time. This allows anyone — bots, frontends, indexers — to discover the oracle endpoint for any on-chain order by reading its metadata. + +See the [ob-yaml spec](./ob-yaml.md) for how `oracle-url` is specified in order configuration. + +## Reference Implementation + +A reference implementation is available at [hardyjosh/rain-oracle-server](https://github.com/hardyjosh/rain-oracle-server). It fetches prices from Pyth Network, encodes them as Rain DecimalFloats, and signs the context using EIP-191. + +## Security Considerations + +- **Signer trust:** The Rainlang expression is responsible for checking the signer address. An oracle server's signer address should be published and verified out-of-band. +- **Expiry:** Always check expiry on-chain. Without expiry checks, a stale signed context could be replayed indefinitely. +- **Counterparty:** The counterparty address may be `address(0)` at quote time. Oracle servers SHOULD handle this gracefully (e.g. ignore the counterparty field for price-only oracles). +- **HTTPS:** Oracle URLs SHOULD use HTTPS in production. Tooling MAY reject non-HTTPS URLs. +- **CORS:** Oracle servers SHOULD allow cross-origin requests (permissive CORS) so that browser-based frontends can fetch signed contexts directly. From 117bbf389f27f98f5d3f1671714f8944f5a1bedf Mon Sep 17 00:00:00 2001 From: Josh Hardy Date: Mon, 23 Feb 2026 11:12:02 +0000 Subject: [PATCH 4/6] refactor: separate spec from implementation concerns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add motivation: why pull oracles, separation of placer and solver/taker - Remove implementation-specific SHOULDs (expiry, price direction, counterparty handling) - Clarify security model: order owner bears the risk, chooses their protections - Rain DecimalFloat encoding is a SHOULD for numeric context values - Onchain discovery via metadata is a MUST (core to the spec) - Fix on-chain → onchain - Move expiry, price direction, counterparty notes to reference implementation --- ob-yaml.md | 2 +- signed-context-oracle.md | 135 ++++++++++++--------------------------- 2 files changed, 43 insertions(+), 94 deletions(-) diff --git a/ob-yaml.md b/ob-yaml.md index 8f1e41c..e77a76f 100644 --- a/ob-yaml.md +++ b/ob-yaml.md @@ -286,7 +286,7 @@ Optional fields: Orders that require external data (e.g. price feeds) can specify an `oracle-url`. This URL points to a server implementing the [Signed Context Oracle protocol](./signed-context-oracle.md). The signed context is provided by the caller when taking or clearing the order — the order's Rainlang expression can then read this data from the signed context columns during evaluation. -When `oracle-url` is specified, the tooling encodes a `RaindexSignedContextOracleV1` metadata item (magic `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1`. This allows the oracle endpoint to be discovered on-chain by anyone who needs to take or clear the order (e.g. Raindex bots, the webapp, or other takers). +When `oracle-url` is specified, the tooling encodes a `RaindexSignedContextOracleV1` metadata item (magic `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1`. This allows the oracle endpoint to be discovered onchain by anyone who needs to take or clear the order (e.g. Raindex bots, the webapp, or other takers). See the [Signed Context Oracle spec](./signed-context-oracle.md) for full protocol details including the POST request format, response schema, signing requirements, and security considerations. diff --git a/signed-context-oracle.md b/signed-context-oracle.md index a98aaa5..c88350f 100644 --- a/signed-context-oracle.md +++ b/signed-context-oracle.md @@ -1,53 +1,41 @@ # Signed Context Oracle Spec -This spec defines the protocol for a **signed context oracle server** compatible with Rain orderbook. Any server implementing this protocol can serve as a price oracle (or any external data source) for Raindex orders. +## Motivation -## Overview +Onchain orders often need external data — prices, trading signals, portfolio weights, or anything else that cannot be found onchain. Push oracles (e.g. Chainlink) solve this for prices but are expensive in gas and limited to data that feed operators choose to publish. Pull oracles allow order owners to specify arbitrary data sources, including data they may want to keep offchain until execution time (e.g. proprietary trading signals). -A signed context oracle server provides signed data that is passed into `takeOrders4()` or `clear2()` as a `SignedContextV1` struct. The orderbook contract verifies the EIP-191 signature on-chain using OpenZeppelin's `SignatureChecker`, then makes the signed data available to the order's Rainlang expression via the signed context columns. +This spec defines a standard protocol for **signed context oracle servers** that serve data to Rain orderbook orders. The key benefit is the **separation of order placer and solver/taker**: an order owner deploys an order referencing an oracle URL, and any taker or solver has a standard way to discover that URL, fetch the required data, and pass it into the order at execution time. Without this standard, takers would need out-of-band coordination with each order owner to know where to get the data and how to format it. -``` -External Data Source → Oracle Server → SignedContextV1 JSON - ↓ -Client (bot/webapp) → POST request → receives signed context - ↓ -Orderbook Contract → verifies EIP-191 signature → Rainlang evaluation -``` +## Protocol -## Endpoint +### Endpoint -The oracle server MUST expose a single `POST` endpoint. The URL of this endpoint is the `oracle-url` referenced in [ob-yaml orders](./ob-yaml.md). +The oracle server MUST expose a `POST` endpoint. The URL of this endpoint is the `oracle-url` specified in the order configuration (see [ob-yaml spec](./ob-yaml.md)). There is no `GET` fallback. The request body is always required. -## Request - -### Method +### Request -`POST` +**Method:** `POST` -### Content-Type +**Content-Type:** `application/octet-stream` -`application/octet-stream` - -### Body - -The request body is the **raw ABI-encoded bytes** of the following tuple: +**Body:** Raw ABI-encoded bytes of the following tuple: ```solidity abi.encode(OrderV4 order, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty) ``` -Where: - -- **`order`** — the full `OrderV4` struct of the order being taken/cleared -- **`inputIOIndex`** — index into `order.validInputs[]` for the current IO pair -- **`outputIOIndex`** — index into `order.validOutputs[]` for the current IO pair -- **`counterparty`** — the address of the taker/clearer. Use `address(0)` when the counterparty is unknown (e.g. at quote time) +| Parameter | Type | Description | +|-----------|------|-------------| +| `order` | `OrderV4` | The full order struct being taken or cleared | +| `inputIOIndex` | `uint256` | Index into `order.validInputs[]` for the current IO pair | +| `outputIOIndex` | `uint256` | Index into `order.validOutputs[]` for the current IO pair | +| `counterparty` | `address` | The address of the taker or clearer | -ABI encoding is used because it is canonical — there are no JSON key ordering ambiguities, and the `OrderV4` struct contains nested arrays and bytes fields that are complex to serialize otherwise. +ABI encoding is used because it is canonical — there are no JSON key ordering ambiguities, and `OrderV4` contains nested arrays and bytes fields that are complex to serialize in other formats. -### Solidity types +The Solidity types are: ```solidity struct IOV2 { @@ -72,9 +60,9 @@ struct OrderV4 { These are the canonical Rain orderbook types. Implementations SHOULD use generated bindings from the Rain orderbook ABI rather than manual encoding. -## Response +### Response -### Success (200) +**Success (200)** Content-Type: `application/json` @@ -88,11 +76,13 @@ Content-Type: `application/json` | Field | Type | Description | |-------|------|-------------| -| `signer` | `address` | The address that produced the signature. The order's Rainlang expression should check that the signer is a trusted address. | -| `context` | `bytes32[]` | Array of 32-byte values. These become the signed context column available to the Rainlang expression during evaluation. Values are typically encoded as Rain DecimalFloats. | -| `signature` | `bytes` | 65-byte EIP-191 signature (r ∥ s ∥ v) over `keccak256(abi.encodePacked(context[]))`. | +| `signer` | `address` | The address that produced the signature | +| `context` | `bytes32[]` | Array of 32-byte values. These become the signed context column available to the order's Rainlang expression during evaluation | +| `signature` | `bytes` | 65-byte EIP-191 signature (r ∥ s ∥ v) over `keccak256(abi.encodePacked(context[]))` | -### Error (400) +Values in the `context` array SHOULD be encoded as Rain DecimalFloats where they represent numeric data, so that they can be read directly by Rainlang arithmetic operations. + +**Error (4xx/5xx)** ```json { @@ -101,77 +91,36 @@ Content-Type: `application/json` } ``` -Standard error codes: - -| Code | Meaning | -|------|---------| -| `invalid_body` | The request body could not be ABI-decoded | -| `invalid_index` | `inputIOIndex` or `outputIOIndex` is out of bounds for the order | -| `unsupported_token_pair` | The input/output token pair is not supported by this oracle | - -Servers MAY define additional error codes. - -### Error (500) - -Internal server errors (e.g. upstream data source failure) SHOULD return a 500 with a JSON body in the same `{error, detail}` format. - -## Signing +### Signing The signature MUST be an EIP-191 "personal sign" signature: -1. Compute the message: `abi.encodePacked(context[0], context[1], ..., context[n])` (raw concatenation of the bytes32 values) -2. Hash the message: `hash = keccak256(packed)` -3. Sign using EIP-191: `sign("\x19Ethereum Signed Message:\n32" + hash)` +1. Concatenate the context values: `packed = abi.encodePacked(context[0], context[1], ..., context[n])` +2. Hash: `hash = keccak256(packed)` +3. Sign with EIP-191: `sign("\x19Ethereum Signed Message:\n32" ++ hash)` This matches how the Rain orderbook contract verifies signatures via `LibContext.build`, which uses OpenZeppelin's `ECDSA.recover` with `toEthSignedMessageHash`. -## Context Layout - -The `context` array is an ordered list of `bytes32` values that the Rainlang expression reads by index. The layout is oracle-specific — different oracles may return different data. - -### Recommended layout for price oracles +## Onchain Discovery -| Index | Value | Encoding | -|-------|-------|----------| -| 0 | Price | Rain DecimalFloat | -| 1 | Expiry timestamp | Rain DecimalFloat (unix seconds) | +When an order specifies an `oracle-url`, the tooling MUST encode a `RaindexSignedContextOracleV1` metadata item (magic number `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1` at deployment time. -**Rain DecimalFloat** is the standard numeric encoding used throughout Rain. Implementations SHOULD use the `rain_math_float` crate (or equivalent) for encoding rather than manual bit packing. +This is how takers, solvers, bots, and frontends discover the oracle endpoint for any onchain order. Without this metadata, there would be no standard way for a third party to know where to fetch the signed context for an order they want to take. -### Expiry - -Oracle servers SHOULD include an expiry timestamp in the context. The order's Rainlang expression SHOULD check that the expiry has not passed: - -```rainlang -oracle-expiry: signed-context<0 1>(), -:ensure(greater-than(oracle-expiry now())); -``` +The metadata item contains the oracle URL as a UTF-8 encoded string. -Short expiry windows (e.g. 5-30 seconds) are recommended to prevent stale price usage. +## Security Model -## Price Direction +The order owner is the party who stands to lose funds if the oracle misbehaves — they are trusting the oracle server (identified by its signer address) with control over the data their order uses to calculate ratios, maximums, and any other logic. -When the oracle serves a price feed for a specific token pair (e.g. ETH/USD), the server SHOULD inspect the order's `validInputs[inputIOIndex].token` and `validOutputs[outputIOIndex].token` to determine the correct price direction: +It is the order owner's responsibility to: -- If input is the quote token and output is the base token → return price as-is -- If input is the base token and output is the quote token → return the inverse (1/price) +- Choose an oracle and signer they trust +- Include any onchain protections they want in their Rainlang expression (e.g. expiry checks, zero ratio guards, price bounds, or any other validation) +- Understand that the oracle server has full knowledge of the order struct, IO pair, and counterparty for every request -This ensures the Rainlang expression always receives the price in the correct orientation for the IO ratio, without needing to handle inversion in Rainlang. - -## On-chain Discovery - -When an order specifies an `oracle-url`, the tooling encodes a `RaindexSignedContextOracleV1` metadata item (magic number `0xff7a1507ba4419ca`) into the order's `RainMetaDocumentV1` at deployment time. This allows anyone — bots, frontends, indexers — to discover the oracle endpoint for any on-chain order by reading its metadata. - -See the [ob-yaml spec](./ob-yaml.md) for how `oracle-url` is specified in order configuration. +The contract enforces only that the signature is valid for the declared signer address. All other validation is up to the Rainlang expression. ## Reference Implementation -A reference implementation is available at [hardyjosh/rain-oracle-server](https://github.com/hardyjosh/rain-oracle-server). It fetches prices from Pyth Network, encodes them as Rain DecimalFloats, and signs the context using EIP-191. - -## Security Considerations - -- **Signer trust:** The Rainlang expression is responsible for checking the signer address. An oracle server's signer address should be published and verified out-of-band. -- **Expiry:** Always check expiry on-chain. Without expiry checks, a stale signed context could be replayed indefinitely. -- **Counterparty:** The counterparty address may be `address(0)` at quote time. Oracle servers SHOULD handle this gracefully (e.g. ignore the counterparty field for price-only oracles). -- **HTTPS:** Oracle URLs SHOULD use HTTPS in production. Tooling MAY reject non-HTTPS URLs. -- **CORS:** Oracle servers SHOULD allow cross-origin requests (permissive CORS) so that browser-based frontends can fetch signed contexts directly. +A reference implementation is available at [hardyjosh/rain-oracle-server](https://github.com/hardyjosh/rain-oracle-server). It fetches prices from Pyth Network, encodes them as Rain DecimalFloats, signs the context with EIP-191, and demonstrates price direction handling based on the order's input/output tokens. From 09b6b7653e5c4b1f01bfc65c5dd5d39f86bae34c Mon Sep 17 00:00:00 2001 From: Josh Hardy Date: Mon, 23 Feb 2026 11:34:24 +0000 Subject: [PATCH 5/6] fix: clarify EIP-191 signing steps, add error content-type - Break signing into 4 explicit steps so implementors don't miss the outer keccak256 from toEthSignedMessageHash - Note that personal_sign/sign_message handles it automatically - Add Content-Type: application/json to error response section --- signed-context-oracle.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/signed-context-oracle.md b/signed-context-oracle.md index c88350f..9869bf1 100644 --- a/signed-context-oracle.md +++ b/signed-context-oracle.md @@ -84,6 +84,8 @@ Values in the `context` array SHOULD be encoded as Rain DecimalFloats where they **Error (4xx/5xx)** +Content-Type: `application/json` + ```json { "error": "", @@ -97,7 +99,10 @@ The signature MUST be an EIP-191 "personal sign" signature: 1. Concatenate the context values: `packed = abi.encodePacked(context[0], context[1], ..., context[n])` 2. Hash: `hash = keccak256(packed)` -3. Sign with EIP-191: `sign("\x19Ethereum Signed Message:\n32" ++ hash)` +3. Apply the EIP-191 prefix: `eth_hash = keccak256("\x19Ethereum Signed Message:\n32" ++ hash)` (this is `toEthSignedMessageHash(hash)`) +4. Sign: `(r, s, v) = ECDSA.sign(eth_hash)` + +Most Web3 libraries handle steps 3-4 automatically via `personal_sign(hash)` or `sign_message(hash)`. This matches how the Rain orderbook contract verifies signatures via `LibContext.build`, which uses OpenZeppelin's `ECDSA.recover` with `toEthSignedMessageHash`. From 69e3dcc9becd84869fb20b7395c1e12bfbdd42cc Mon Sep 17 00:00:00 2001 From: Josh Hardy Date: Mon, 23 Feb 2026 20:17:11 +0000 Subject: [PATCH 6/6] Update oracle spec to batch format - Change request from single tuple to array of tuples - Change response from single JSON object to array - Array length must match between request and response - Each signature applies to its corresponding context array --- signed-context-oracle.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/signed-context-oracle.md b/signed-context-oracle.md index 9869bf1..04c95b6 100644 --- a/signed-context-oracle.md +++ b/signed-context-oracle.md @@ -20,12 +20,14 @@ There is no `GET` fallback. The request body is always required. **Content-Type:** `application/octet-stream` -**Body:** Raw ABI-encoded bytes of the following tuple: +**Body:** Raw ABI-encoded bytes of an array of tuples: ```solidity -abi.encode(OrderV4 order, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty) +abi.encode((OrderV4 order, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)[]) ``` +Each tuple contains: + | Parameter | Type | Description | |-----------|------|-------------| | `order` | `OrderV4` | The full order struct being taken or cleared | @@ -33,6 +35,8 @@ abi.encode(OrderV4 order, uint256 inputIOIndex, uint256 outputIOIndex, address c | `outputIOIndex` | `uint256` | Index into `order.validOutputs[]` for the current IO pair | | `counterparty` | `address` | The address of the taker or clearer | +A single request is simply an array of length 1. + ABI encoding is used because it is canonical — there are no JSON key ordering ambiguities, and `OrderV4` contains nested arrays and bytes fields that are complex to serialize in other formats. The Solidity types are: @@ -67,13 +71,19 @@ These are the canonical Rain orderbook types. Implementations SHOULD use generat Content-Type: `application/json` ```json -{ - "signer": "0x<20-byte address>", - "context": ["0x<32-byte hex>", "0x<32-byte hex>", ...], - "signature": "0x<65-byte hex>" -} +[ + { + "signer": "0x<20-byte address>", + "context": ["0x<32-byte hex>", "0x<32-byte hex>", ...], + "signature": "0x<65-byte hex>" + } +] ``` +The response is an array of signed context objects. The array length MUST match the request array length and be in the same order. + +Each object contains: + | Field | Type | Description | |-------|------|-------------| | `signer` | `address` | The address that produced the signature | @@ -95,7 +105,7 @@ Content-Type: `application/json` ### Signing -The signature MUST be an EIP-191 "personal sign" signature: +Each signature MUST be an EIP-191 "personal sign" signature over its corresponding context array: 1. Concatenate the context values: `packed = abi.encodePacked(context[0], context[1], ..., context[n])` 2. Hash: `hash = keccak256(packed)`