Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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
124 changes: 124 additions & 0 deletions docs/oracle-dns-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Oracle DNS Protocol

The Oracle plugin resolves RFC 4501 `dns:` URIs through a DNS-over-HTTPS (DoH) gateway. This lets oracle nodes read authoritative DNS data (TXT for DKIM/SPF/DIDs, CERT/TLSA, etc.) without sending plaintext DNS queries.

> **When should I use it?**
> Whenever you need DNS data on-chain and want the request to stay encrypted end-to-end.

## Enable and configure

1. Install or build the `OracleService` plugin and copy `OracleService.json` next to the plugin binary.
2. Add the `Dns` section (defaults shown):

```jsonc
{
"PluginConfiguration": {
// ...
"Dns": {
"EndPoint": "https://cloudflare-dns.com/dns-query",
"TimeoutMilliseconds": 5000
}
}
}
```

- `EndPoint` must point to a DoH resolver that supports [RFC 8484](https://www.rfc-editor.org/rfc/rfc8484.html) with `application/dns-message` format.
- `TimeoutMilliseconds` is the maximum milliseconds the oracle will wait for a DoH response before returning `OracleResponseCode.Timeout` (falls back to `Timeout` for backward compatibility).

> You can run your own DoH gateway and point the oracle to it if you need custom trust anchors or strict egress controls.

### RFC 8484 Compliance

This implementation uses the standard `application/dns-message` content type as defined in RFC 8484. DNS queries are sent as POST requests with binary DNS wire format (RFC 1035). Compatible DoH endpoints include:

| Provider | Endpoint |
|----------|----------|
| Cloudflare | `https://cloudflare-dns.com/dns-query` |
| Google | `https://dns.google/dns-query` |
| Quad9 | `https://dns.quad9.net/dns-query` |

Any RFC 8484-compliant DoH server should work with this oracle protocol.

## RFC 4501 URI format

```
dns:[//authority/]domain[?CLASS=class;TYPE=type]
```

- `domain` is the DNS owner name (relative or absolute). Percent-encoding and escaped dots (`%5c.`) follow RFC 4501 rules.
- `domain` must not include additional path segments; only the owner name belongs here.
- `authority` is the optional DoH server to use for this query (RFC 4501). When specified, the oracle connects to `https://{authority}/dns-query`. If omitted, the configured `EndPoint` is used.
- `CLASS` is optional and case-insensitive. Only `IN` (`1`) is supported; other classes are rejected.
- `TYPE` is optional and case-insensitive. Use mnemonics (`TXT`, `TLSA`, `CERT`, `A`, `AAAA`, …) or numeric values. Defaults to `A` per RFC 4501.

Query parameters can be separated by `;` (RFC style) or `&`.

Examples:

- `dns:1alhai._domainkey.icloud.com?TYPE=TXT` — DKIM TXT record.
- `dns:simon.example.org?TYPE=CERT` — CERT RDATA is returned as-is (type, key tag, algorithm, base64).
- `dns://dns.google/ftp.example.org?TYPE=A` — uses Google's DoH server (`https://dns.google/dns-query`) instead of the configured endpoint.
- `dns://cloudflare-dns.com/example.org?TYPE=TXT` — uses Cloudflare's DoH server for this specific query.

## Response schema

Successful queries return UTF-8 JSON. Attributes correspond to the `ResultEnvelope` produced by the oracle:

```jsonc
{
"Name": "1alhai._domainkey.icloud.com",
"Type": "TXT",
"Answers": [
{
"Name": "1alhai._domainkey.icloud.com",
"Type": "TXT",
"Ttl": 299,
"Data": "\"k=rsa; p=...IDAQAB\""
}
]
}
```

- `Answers` mirrors the DoH response but normalizes record types and names.
- CERT records are returned verbatim in `Answers[].Data` (type, key tag, algorithm, base64 payload). Contracts can parse the certificate themselves if needed.
- If the DoH server responds with NXDOMAIN, the oracle returns `OracleResponseCode.NotFound`.
- Responses exceeding `OracleResponse.MaxResultSize` yield `OracleResponseCode.ResponseTooLarge`.

## Contract usage example

```csharp
public static void RequestAppleDkim()
{
const string url = "dns:1alhai._domainkey.icloud.com?TYPE=TXT";
Oracle.Request(url, "", nameof(OnOracleCallback), Runtime.CallingScriptHash, 5_00000000);
}

public static void OnOracleCallback(string url, byte[] userData, int code, byte[] result)
{
if (code != (int)OracleResponseCode.Success) throw new Exception("Oracle query failed");

var envelope = (Neo.SmartContract.Framework.Services.Neo.Json.JsonObject)StdLib.JsonDeserialize(result);
var answers = (Neo.SmartContract.Framework.Services.Neo.Json.JsonArray)envelope["Answers"];
var txt = (Neo.SmartContract.Framework.Services.Neo.Json.JsonObject)answers[0];
Storage.Put(Storage.CurrentContext, "dkim", txt["Data"].AsString());
}
```

Tips:

1. Always set `TYPE` when you need anything other than an A record.
2. Budget enough `gasForResponse` to cover JSON payload size (TXT records are often kilobytes).
3. Validate TTL or fingerprint data before trusting it.
4. Combine oracle DNS data with existing filters (e.g., `Helper.JsonPath`/`OracleService.Filter`) if you only need a slice of the result.

## Manual testing

Use the same resolver the oracle will contact to inspect responses:

```bash
curl -s \
-H 'accept: application/dns-json' \
'https://cloudflare-dns.com/dns-query?name=1alhai._domainkey.icloud.com&type=TXT'
```

Compare the JSON payload with the data returned by your contract callback to ensure parity.
1 change: 1 addition & 0 deletions plugins/OracleService/OracleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ public Task Start(Wallet wallet)

this.wallet = wallet;
protocols["https"] = new OracleHttpsProtocol();
protocols["dns"] = new OracleDnsProtocol();
protocols["neofs"] = new OracleNeoFSProtocol(wallet, oracles);
status = OracleStatus.Running;
timer = new Timer(OnTimer, null, RefreshIntervalMilliSeconds, Timeout.Infinite);
Expand Down
4 changes: 4 additions & 0 deletions plugins/OracleService/OracleService.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"EndPoint": "http://127.0.0.1:8080",
"Timeout": 15000
},
"Dns": {
"EndPoint": "https://cloudflare-dns.com/dns-query",
"TimeoutMilliseconds": 5000
Comment on lines 15 to +19
Copy link
Member

Choose a reason for hiding this comment

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

L15 and L19, one is Timeout and the other is TimeoutMilliseconds. It's best to keep them consistent.

Copy link
Member

Choose a reason for hiding this comment

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

},
"AutoStart": false
},
"Dependency": [
Expand Down
16 changes: 16 additions & 0 deletions plugins/OracleService/OracleSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ public NeoFSSettings(IConfigurationSection section)
}
}

class DnsSettings
{
public Uri EndPoint { get; }
public TimeSpan Timeout { get; }

public DnsSettings(IConfigurationSection section)
{
string endpoint = section.GetValue("EndPoint", "https://cloudflare-dns.com/dns-query");
EndPoint = new Uri(endpoint, UriKind.Absolute);
int timeoutMs = section.GetValue("TimeoutMilliseconds", section.GetValue("Timeout", 5000));
Timeout = TimeSpan.FromMilliseconds(timeoutMs);
}
}

class OracleSettings : IPluginSettings
{
public uint Network { get; }
Expand All @@ -46,6 +60,7 @@ class OracleSettings : IPluginSettings
public string[] AllowedContentTypes { get; }
public HttpsSettings Https { get; }
public NeoFSSettings NeoFS { get; }
public DnsSettings Dns { get; }
public bool AutoStart { get; }

public static OracleSettings Default { get; private set; }
Expand All @@ -65,6 +80,7 @@ private OracleSettings(IConfigurationSection section)
AllowedContentTypes = AllowedContentTypes.Concat("application/json").ToArray();
Https = new HttpsSettings(section.GetSection("Https"));
NeoFS = new NeoFSSettings(section.GetSection("NeoFS"));
Dns = new DnsSettings(section.GetSection("Dns"));
AutoStart = section.GetValue("AutoStart", false);
}

Expand Down
Loading
Loading