Run an oracle
The cmd/oracle binary, its YAML configuration in full, and the validation that stops startup.
What runs
One binary, cmd/oracle, configured by one YAML file. The binary names no
chain: which chains it serves, and under which DID methods, comes entirely from
the config. It loads the file, reads its signing key from the environment,
builds one adapter per entry in adapters, and serves HTTP on the configured
address until it is stopped.
go build -o oracle ./cmd/oracle
./oracle -config /etc/attestra/oracle.yaml-config defaults to oracle.yaml in the working directory. There are no other
flags: everything else is in the file or in the environment.
Startup is strict. A configuration error, a missing signing key, or an adapter that cannot be built (an unreachable endpoint, an unreadable Fabric certificate) ends the process rather than starting a node that answers some requests and not others. The one deliberate exception is a push target, covered below.
A minimal configuration
Two adapters, one reading eIDChain and one reading Ethereum, with no quorum and no push. Substitute your own endpoints and addresses.
listen: ":8080"
signer:
keyEnv: ORACLE_SIGNER_KEY
adapters:
- method: eid
kind: eidchain
endpoint: wss://<your-eidchain-node>
- method: ethr
kind: ethr
endpoint: https://<your-ethereum-rpc>
registry: "<erc1056-registry-address>"
schemaRegistry: "<schema-registry-address>"Run it with the key in the environment:
export ORACLE_SIGNER_KEY=... # hex private key, never in the file
./oracle -config oracle.yamlOn success the node logs one line per registered adapter, one per registered presentation verifier, and finally its listen address and the public address its signing key resolves to. That last address is the one that has to appear in each target chain's signer set before anything it signs will be accepted on chain. See Deploy the contracts.
Configuration reference
Top level
| Field | Required | Default | What it is |
|---|---|---|---|
listen | no | :8080 | Address the HTTP server binds to, in Go host:port form. :8080 binds every interface; 127.0.0.1:8080 binds loopback only. |
signer | no | see below | Names the environment variable holding the signing key. |
adapters | yes | none | One entry per chain this node reads. At least one is required. |
quorum | no | absent | Present turns the node into a collector. Absent means single-node mode, where this node's own signature is the whole answer. |
signer
| Field | Required | Default | What it is |
|---|---|---|---|
keyEnv | no | ORACLE_SIGNER_KEY | Name of the environment variable holding the hex private key. Not the key. |
adapters[]
| Field | Required | Default | What it is |
|---|---|---|---|
method | yes | none | The DID method to register this adapter under, for example eid. Requests for did:eid:... dispatch here. |
kind | yes | none | The driver to use: eidchain, ethr or fabric. Anything else fails at startup with unknown adapter kind. |
endpoint | yes | none | The RPC or WebSocket URL, or for Fabric the peer's gRPC address. |
registry | depends on kind | empty | Contract address, for chains that need one. On ethr this is the ERC-1056 identity registry. |
schemaRegistry | no | empty | The on-chain schema store address, for chains that resolve full credential schemas by hash. Ethereum only in practice. Without it, schema lookups on this adapter have nothing to read. |
fabric | required when kind: fabric | absent | The mTLS identity and channel details, below. |
push | no | absent | Enables writing attestations onto this chain, below. |
adapters[].fabric
Required whenever kind is fabric, ignored otherwise. All seven fields are
needed for the connection to come up.
| Field | What it is |
|---|---|
mspId | The MSP the client identity belongs to, for example Org1MSP. |
channel | The channel to query, for example mychannel. |
chaincode | The chaincode to call, for example registry. |
gatewayPeer | TLS server-name override for the peer, for example peer0.org1.example.com. It has to match the name in the peer's certificate, which is often not the host in endpoint. |
certPath | Path to the client identity certificate, PEM. |
keyPath | Path to the client identity private key, PEM. |
tlsCertPath | Path to the peer's TLS CA certificate, PEM. |
The certificate and key paths are machine-specific. That is the usual reason a config file that works on one host fails on another.
adapters[].push
Present only on adapters that should also write. Without it the adapter is
read-only and POST /v1/push for that target returns 404.
| Field | Required | Default | What it is |
|---|---|---|---|
storeAddress | yes, on Ethereum | empty | The target AttestationStore contract. |
keyEnv | yes | none | Name of the environment variable holding the funded key that signs and pays for the transaction. As with signer.keyEnv, the name goes in the file and the key does not. |
Two properties are worth knowing before you rely on push. First, the pushing key
is separate from the signing key: one authorises an attestation, the other pays
gas, and they are not required to be the same account. Second, a push target
that cannot be built is not fatal. If the funded key is missing, the node logs a
warning, skips that pusher and carries on serving reads, and POST /v1/push for
that target returns 404. A node that appears healthy but refuses pushes has
usually lost its push key, not its configuration. On Fabric, push reuses the
adapter's existing gateway connection, so it needs no key of its own.
quorum
Present turns this node into a collector that gathers signatures from peer oracles. See Join a quorum for the operational side and The oracle network for why the collector is shaped this way.
| Field | Required | Default | What it is |
|---|---|---|---|
peers | yes | none | Base URLs of the peer oracles, for example http://127.0.0.1:8081. Each must be an http or https URL with a host. At least one is required, and duplicates are rejected. |
threshold | yes | none | How many peers must return the same digest. Must fall within 1..len(peers). |
timeout | no | 5s | How long the fan-out to peers waits. A peer that has not answered by then is treated as absent rather than waited on: the point of k-of-n is that a slow or dead minority does not hold up the majority. |
threshold must also match the threshold configured in each target chain's
signer registry. A bundle this node considers sufficient is rejected on chain
otherwise, and the failure surfaces at push time rather than at startup.
The signing key never appears in the config file
The config file names an environment variable through signer.keyEnv, and the
oracle reads the key from that variable at startup. If the field is omitted the
name defaults to ORACLE_SIGNER_KEY. If the named variable is empty, the
process exits with signer key env var ... is empty.
The reason is stated in the source: the config file is committed, and secrets
are not. Keeping the key out of the file means the file can live in version
control, be templated, be diffed in a review and be copied between hosts,
without any of those operations moving key material. The same rule applies to
push.keyEnv.
How the variable gets its value is your deployment's problem, and the oracle
does not care: a systemd EnvironmentFile with restricted permissions, a
secrets manager, an init container. What it should not be is a shell history
entry or a line in the same repository as the config.
Validation that stops startup
The loader validates before anything is built, so these produce a fatal error at startup rather than a failure on the first request.
- At least one adapter. An empty or missing
adapterslist fails withno adapters configured. A node with no adapters could answer nothing. - Every adapter needs
method. Reported asadapters[i]: method is required. - Every adapter needs
kind. Reported asadapters[i] (method): kind is required. - Every adapter needs
endpoint. Reported asadapters[i] (method): endpoint is required. - No two adapters may share a
method. Reported asadapters[i]: duplicate method. The resolver keeps one adapter per method in a map, so a second registration under the same method would silently replace the first, and requests meant for one chain would land on another without any error. Refusing to start is the safer outcome. - Quorum, when present, needs at least one peer, a
thresholdwithin1..len(peers), no duplicate peer, and every peer anhttporhttpsURL with a host. A repeated peer would be polled twice and could satisfy the threshold on its own, which is the same one-key-many-votes hole the on-chain verifiers close by requiring distinct signers.
Errors after that point come from the drivers rather than the loader: an
unreachable endpoint, a Fabric certificate that cannot be read, an adapter
kind that no driver claims.
What each kind needs
kind | Endpoint | Also required | Optional |
|---|---|---|---|
eidchain | WebSocket URL (wss:// or ws://) | nothing | push |
ethr | HTTP RPC URL | registry, the ERC-1056 address | schemaRegistry, push |
fabric | Peer gRPC address | the whole fabric block | push |
eidchain subscribes over a WebSocket, so an HTTP RPC URL will not do. The
adapter is given the endpoint and the method and needs nothing else: identity,
schemas and revocation all live in the chain's own pallets.
ethr resolves did:ethr over ERC-1056, so it needs both an RPC endpoint
and the registry contract address on that network. Point it at a registry
deployed on a different network from the RPC and every resolution returns the
default owner, which looks like a valid answer and is not. schemaRegistry is
separate and optional: it is only consulted for schema lookups by hash.
fabric needs the entire fabric block, not just a URL. A Fabric peer
answers only a mutually authenticated client presenting a valid MSP identity, so
the adapter has to supply a client certificate and key, the peer's TLS CA to
verify the peer in return, the TLS server name to check against, and the channel
and chaincode to address. An endpoint on its own gives the driver no identity to
present, and the connection is refused before any query is made.
One Fabric property belongs in an operator's head from the start: a freshly deployed registry chaincode has no signer set, and an unseeded chaincode accepts nothing. That is deliberate, and it is covered in Deploy the contracts.
method versus kind
These two fields are the config's one genuinely confusing pair, and they are not interchangeable.
kind selects the driver: the Go code that knows how to talk to a
particular kind of ledger. There are three, eidchain, ethr and fabric, and
the list only changes when the software does. Adding a fourth means writing an
adapter and rebuilding.
method is only the label the driver is registered under. It is the DID
method that routes requests to this adapter: with method: eid, a request for
did:eid:... reaches this adapter, and nothing else does.
The DID method a chain uses is a property of that chain's deployment, not of the
oracle's code, which is why it is data rather than a compiled-in constant. A
chain can change its method, or the same driver can be pointed at a second
deployment under a different method, without a rebuild. Two ethr adapters on
two networks, registered under two methods, is a valid configuration; two
adapters under one method is not, for the reason given above.
For how the method is parsed out of an incoming DID and dispatched, see Chains and DID methods.
Helper scripts
The repository carries several scripts under scripts/. They exist to bring up
a full local environment quickly, and they are worth reading before you write
your own deployment.
| Script | What it does |
|---|---|
scripts/run.sh | Brings up all three chains and runs the oracle against them in one command, including standing up a local Fabric network, deploying the chaincode and filling in the certificate paths. |
scripts/run-quorum.sh | Runs a multi-node quorum locally and exercises it: nodes killed, a node lying, and what the on-chain result is in each case. |
scripts/demo.sh | The cross-chain demonstration: a fact read from one chain, signed by the oracle, accepted by a contract on another. |
scripts/verify-all.sh | The acceptance gate. Regenerates the shared fixture and proves the same signature verifies on all three chains. |
scripts/stop.sh | The reverse of run.sh: shuts the oracle and the local networks down. |
scripts/run.sh and scripts/run-quorum.sh contain development credentials and
are marked in their own comments as never to be committed. They also write a
config file containing machine-specific paths. Read them as worked examples of
the sequence a deployment has to perform, then write your own, with your own key
handling. Do not run them against anything you care about, and do not copy their
credentials anywhere.