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
17 changes: 17 additions & 0 deletions ob-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,15 @@ 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) 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 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.

```
orders:
Expand Down Expand Up @@ -307,6 +316,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
Expand Down
141 changes: 141 additions & 0 deletions signed-context-oracle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Signed Context Oracle Spec

## Motivation

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).

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.

## Protocol

### Endpoint

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.
Comment on lines +13 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Transport security for the oracle URL is unspecified — recommend mandating HTTPS.

Every request includes the full OrderV4 struct and the counterparty address. Transmitting these over plaintext HTTP leaks pre-confirmation execution intent (taker identity, which order is being taken, timing), which is actionable information for front-running or taker-suppression even though the same data eventually appears on-chain. The signature protects response integrity but not request confidentiality.

The spec should add a SHOULD/MUST constraint:

📝 Proposed addition
 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)).

+The `oracle-url` MUST use the `https://` scheme. HTTP endpoints MUST NOT be used, as the
+request body contains sensitive execution intent (taker address, full order struct) that
+would be exposed in plaintext to network observers.
+
 There is no `GET` fallback. The request body is always required.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.
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)).
The `oracle-url` MUST use the `https://` scheme. HTTP endpoints MUST NOT be used, as the request body contains sensitive execution intent (taker address, full order struct) that would be exposed in plaintext to network observers.
There is no `GET` fallback. The request body is always required.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@signed-context-oracle.md` around lines 13 - 15, The spec currently leaves
transport security for the oracle-url unspecified; update the document to
require TLS by mandating that the oracle POST endpoint (the oracle-url) use
HTTPS (require the https:// scheme) and that clients and servers validate
certificates (recommend use of CA-signed certs and modern TLS versions), and add
a MUST/SHOULD statement referencing the POST endpoint, OrderV4, and counterparty
to ensure request confidentiality (e.g., "oracle-url MUST be an https:// URL and
oracle servers MUST support TLS with certificate validation"); also note that
implementations SHOULD enforce secure defaults (no plaintext HTTP fallback) and
consider HSTS and recommended cipher suites.


### Request

**Method:** `POST`

**Content-Type:** `application/octet-stream`

**Body:** Raw ABI-encoded bytes of an array of tuples:

```solidity
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 |
| `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 |

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:

```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>"
}
]
```

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 |
| `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[]))` |

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)**

Content-Type: `application/json`

```json
{
"error": "<error_code>",
"detail": "<human-readable description>"
}
```

### Signing

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)`
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`.

## Onchain Discovery

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.

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.

The metadata item contains the oracle URL as a UTF-8 encoded string.

## Security Model

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.

It is the order owner's responsibility to:

- 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

The contract enforces only that the signature is valid for the declared signer address. All other validation is up to the Rainlang expression.

Comment on lines +127 to +138
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security model should explicitly require signer-address verification in the Rainlang expression.

The signer field is a runtime input supplied by the taker when constructing SignedContextV1 for takeOrders4(). The contract verifies only that ECDSA.recover(context_hash, signature) == signer — it does not check that signer matches any address the order owner intended. Without an explicit signer check in the expression, a taker can generate their own key pair, sign arbitrary context values, set signer to their own address, and the contract will accept it.

Line 127 mentions this indirectly ("All other validation is up to the Rainlang expression"), but an order owner who misses this will deploy a fully open oracle socket. Add an explicit bullet to the responsibility list:

📝 Proposed addition
 It is the order owner's responsibility to:

 - Choose an oracle and signer they trust
+- **Verify the signer address in the Rainlang expression.** The contract does not enforce
+  which signer is used; it only checks that the signature is valid for the declared signer.
+  The expression MUST compare the runtime signer against a trusted address hardcoded at
+  order-placement time, otherwise any party can inject arbitrary context.
 - Include any onchain protections they want in their Rainlang expression (e.g. expiry checks,
   zero ratio guards, price bounds, or any other validation)
🧰 Tools
🪛 LanguageTool

[grammar] ~124-~124: Ensure spelling is correct
Context: ...cle and signer they trust - Include any onchain protections they want in their Rainlang...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~125-~125: Ensure spelling is correct
Context: ... server has full knowledge of the order struct, IO pair, and counterparty for every re...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@signed-context-oracle.md` around lines 117 - 128, Update the Security Model
text to explicitly require that Rainlang expressions validate the declared
signer: when using SignedContextV1 with takeOrders4(), the expression must check
that ECDSA.recover(context_hash, signature) == signer and that signer matches
the order owner's intended oracle address (or a value derived from it);
reference the SignedContextV1/takeOrders4() flow and the
signer/context_hash/signature symbols so the guidance clearly instructs order
owners to add an explicit signer-address check in their 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, signs the context with EIP-191, and demonstrates price direction handling based on the order's input/output tokens.