Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9a72c1a
Add DNS oracle protocol
Jim8y Nov 17, 2025
6aa0a8e
Align oracle DNS protocol with RFC 4501
Jim8y Nov 20, 2025
4118a19
Guard DoH certificate parsing against oversized payloads
Jim8y Nov 20, 2025
a40b561
Merge branch 'master' into feature/dns-doh
erikzhang Nov 21, 2025
9e27fda
Merge branch 'master' into feature/dns-doh
ajara87 Nov 26, 2025
2b9f772
Merge branch 'master' into feature/dns-doh
ajara87 Nov 30, 2025
55a5023
Merge branch 'master' into feature/dns-doh
ajara87 Dec 2, 2025
ba3ed0f
Merge branch 'master' into feature/dns-doh
Jim8y Dec 3, 2025
769f160
Update plugins/OracleService/OracleSettings.cs
Jim8y Dec 3, 2025
9071047
Update docs/oracle-dns-protocol.md
Jim8y Dec 3, 2025
6ec4813
Update DNS oracle to RFC 8484 application/dns-message format
Jim8y Dec 3, 2025
22f4f0e
Simplify DNS DoH handling and tighten safeguards
Jim8y Dec 3, 2025
12fcdbf
Merge branch 'master' into feature/dns-doh
Jim8y Dec 6, 2025
ba19dba
Align oracle DNS protocol with RFC4501
Jim8y Dec 6, 2025
1f4d451
Merge branch 'master' into feature/dns-doh
ajara87 Dec 6, 2025
27e07cd
Oracle DNS: return struct result for contracts
Jim8y Dec 17, 2025
6b23c71
Merge branch 'master' into feature/dns-doh
Jim8y Dec 24, 2025
5ee06cc
Update plugins/OracleService/OracleService.cs
Jim8y Jan 4, 2026
37604ce
Merge branch 'master' into feature/dns-doh
Jim8y Mar 17, 2026
9b87928
style: format oracle dns files
Jim8y Mar 17, 2026
a1c1bf1
fix: restore compatibility with current Neo package
Jim8y Mar 17, 2026
3e5c77f
Merge branch 'master' into feature/dns-doh
Jim8y May 9, 2026
122f490
Use PipeReader for DoH responses
Jim8y May 9, 2026
4725a47
check bool result
shargon May 10, 2026
d4c4380
Address Oracle DNS review comments
Jim8y May 15, 2026
09d4a4b
Address DNS oracle review feedback
Jim8y May 18, 2026
4c4583f
Pin DNS resolver connections
Jim8y May 18, 2026
93c5c00
Merge branch 'master' into feature/dns-doh
shargon May 23, 2026
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
125 changes: 125 additions & 0 deletions docs/oracle-dns-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# 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",
"Timeout": 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.
- `Timeout` is the maximum milliseconds the oracle will wait for a DoH response before returning `OracleResponseCode.Timeout`.

> 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 a NeoVM-serialized **Struct** (use `StdLib.Deserialize(result)` in contracts).

Struct schema:

- `Envelope` (Struct, 3 items): `[Name, Type, Answers]`
- `Answer` (Struct, 4 items): `[Name, Type, Ttl, Data]`

Notes:

- `Answers` mirrors the DoH answer section but normalizes record types and names.
- CERT records are returned verbatim in `Answer[3]` (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`.
- Results exceeding `OracleResponse.MaxResultSize` yield `OracleResponseCode.ResponseTooLarge`.
- Oracle `filter` is not supported for DNS responses in struct mode; pass an empty filter string.

## 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");

// Envelope = [Name, Type, Answers]
var envelope = (object[])StdLib.Deserialize(result);
var answers = (object[])envelope[2];

// Answer = [Name, Type, Ttl, Data]
var first = (object[])answers[0];
Storage.Put(Storage.CurrentContext, "dkim", (string)first[3]);
}
```

Tips:

1. Always set `TYPE` when you need anything other than an A record.
2. Budget enough `gasForResponse` to cover payload size (TXT records are often kilobytes).
3. Validate TTL or fingerprint data before trusting it.
4. DNS oracle responses do not support the oracle `filter`; request the record type you need and parse `Answers` in-contract.

## Manual testing

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

```bash
printf 'bR4BAAABAAAAAAAABjFhbGhhaQpfZG9tYWlua2V5BmljbG91ZANjb20AAAEAAQ==' | \
base64 -d | \
curl -s \
-X POST \
-H 'accept: application/dns-message' \
-H 'content-type: application/dns-message' \
--data-binary @- \
'https://cloudflare-dns.com/dns-query'
```

Compare the DNS answer content with `Answer[3]` returned by your contract callback (after `StdLib.Deserialize`).
66 changes: 64 additions & 2 deletions plugins/OracleService/OracleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
using Neo.Wallets;
using Serilog;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace Neo.Plugins.OracleService;
Expand Down Expand Up @@ -152,6 +153,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 Expand Up @@ -301,8 +303,20 @@ private async Task ProcessRequestAsync(DataCache snapshot, OracleRequest req)

(OracleResponseCode code, string? data) = await ProcessUrlAsync(req.Url);

bool dnsStackOutput = Uri.TryCreate(req.Url, UriKind.Absolute, out Uri? requestUri)
&& requestUri.Scheme.Equals("dns", StringComparison.OrdinalIgnoreCase);
byte[]? dnsStackBytes = null;
if (code == OracleResponseCode.Success && dnsStackOutput)
{
if (!TryDecodeDnsStackItemPayload(data, out dnsStackBytes))
{
code = OracleResponseCode.Error;
PluginLogger?.Warning("Invalid DNS stack item payload: {OriginalTxid}", req.OriginalTxid);
}
}

PluginLogger?.Information("Process oracle request end: {OriginalTxid} <{Url}>, responseCode:{ResponseCode}, response:{Response}",
req.OriginalTxid, req.Url, code, data);
req.OriginalTxid, req.Url, code, FormatResponseForLog(code, data, dnsStackOutput));

var oracleNodes = NativeContract.RoleManagement.GetDesignatedByRole(snapshot, Role.Oracle, height);
foreach (var (requestId, request) in NativeContract.Oracle.GetRequestsByUrl(snapshot, req.Url))
Expand All @@ -312,7 +326,20 @@ private async Task ProcessRequestAsync(DataCache snapshot, OracleRequest req)
{
try
{
result = Filter(data!, request.Filter);
if (dnsStackOutput)
{
if (!string.IsNullOrEmpty(request.Filter))
throw new InvalidOperationException("Filter is not supported for dns: requests.");
if (dnsStackBytes is null)
throw new InvalidOperationException("Missing DNS stack item payload.");
if (dnsStackBytes.Length > OracleResponse.MaxResultSize)
throw new InvalidOperationException("DNS stack item payload exceeds oracle maximum result size.");
result = dnsStackBytes;
}
else
{
result = Filter(data ?? string.Empty, request.Filter);
}
}
catch (Exception ex)
{
Expand Down Expand Up @@ -542,6 +569,41 @@ public static byte[] Filter(string input, string? filterArgs)
return afterObjects.ToByteArray(false);
}

internal static string? FormatResponseForLog(OracleResponseCode code, string? data, bool dnsStackOutput)
{
if (code != OracleResponseCode.Success)
return data;

if (dnsStackOutput)
return string.IsNullOrEmpty(data) ? "<stackitem:empty>" : $"<stackitem:base64:{data.Length} chars>";

if (string.IsNullOrEmpty(data))
return data;

const int maxLen = 2048;
return data.Length <= maxLen ? data : data[..maxLen] + "...";
}

internal static bool TryDecodeDnsStackItemPayload(string? payload, [NotNullWhen(true)] out byte[]? result)
{
if (payload is null)
{
result = null;
return false;
}

try
{
result = Convert.FromBase64String(payload);
return true;
}
catch (Exception)
{
result = null;
return false;
}
}

private bool CheckTxSign(DataCache snapshot, Transaction tx, ConcurrentDictionary<ECPoint, byte[]> OracleSigns)
{
uint height = NativeContract.Ledger.CurrentIndex(snapshot) + 1;
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",
"Timeout": 5000
},
"AutoStart": false
},
"Dependency": [
Expand Down
15 changes: 15 additions & 0 deletions plugins/OracleService/OracleSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ 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");
Comment thread
Jim8y marked this conversation as resolved.
EndPoint = new Uri(endpoint, UriKind.Absolute);
Timeout = TimeSpan.FromMilliseconds(section.GetValue("Timeout", 5000));
}
}

class OracleSettings : IPluginSettings
{
public uint Network { get; }
Expand All @@ -46,6 +59,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; } = null!;
Expand All @@ -65,6 +79,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