Attestra

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.

oracle.yaml
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.yaml

On 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

FieldRequiredDefaultWhat it is
listenno:8080Address the HTTP server binds to, in Go host:port form. :8080 binds every interface; 127.0.0.1:8080 binds loopback only.
signernosee belowNames the environment variable holding the signing key.
adaptersyesnoneOne entry per chain this node reads. At least one is required.
quorumnoabsentPresent turns the node into a collector. Absent means single-node mode, where this node's own signature is the whole answer.

signer

FieldRequiredDefaultWhat it is
keyEnvnoORACLE_SIGNER_KEYName of the environment variable holding the hex private key. Not the key.

adapters[]

FieldRequiredDefaultWhat it is
methodyesnoneThe DID method to register this adapter under, for example eid. Requests for did:eid:... dispatch here.
kindyesnoneThe driver to use: eidchain, ethr or fabric. Anything else fails at startup with unknown adapter kind.
endpointyesnoneThe RPC or WebSocket URL, or for Fabric the peer's gRPC address.
registrydepends on kindemptyContract address, for chains that need one. On ethr this is the ERC-1056 identity registry.
schemaRegistrynoemptyThe 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.
fabricrequired when kind: fabricabsentThe mTLS identity and channel details, below.
pushnoabsentEnables 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.

FieldWhat it is
mspIdThe MSP the client identity belongs to, for example Org1MSP.
channelThe channel to query, for example mychannel.
chaincodeThe chaincode to call, for example registry.
gatewayPeerTLS 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.
certPathPath to the client identity certificate, PEM.
keyPathPath to the client identity private key, PEM.
tlsCertPathPath 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.

FieldRequiredDefaultWhat it is
storeAddressyes, on EthereumemptyThe target AttestationStore contract.
keyEnvyesnoneName 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.

FieldRequiredDefaultWhat it is
peersyesnoneBase 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.
thresholdyesnoneHow many peers must return the same digest. Must fall within 1..len(peers).
timeoutno5sHow 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 adapters list fails with no adapters configured. A node with no adapters could answer nothing.
  • Every adapter needs method. Reported as adapters[i]: method is required.
  • Every adapter needs kind. Reported as adapters[i] (method): kind is required.
  • Every adapter needs endpoint. Reported as adapters[i] (method): endpoint is required.
  • No two adapters may share a method. Reported as adapters[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 threshold within 1..len(peers), no duplicate peer, and every peer an http or https URL 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

kindEndpointAlso requiredOptional
eidchainWebSocket URL (wss:// or ws://)nothingpush
ethrHTTP RPC URLregistry, the ERC-1056 addressschemaRegistry, push
fabricPeer gRPC addressthe whole fabric blockpush

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.

ScriptWhat it does
scripts/run.shBrings 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.shRuns 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.shThe cross-chain demonstration: a fact read from one chain, signed by the oracle, accepted by a contract on another.
scripts/verify-all.shThe acceptance gate. Regenerates the shared fixture and proves the same signature verifies on all three chains.
scripts/stop.shThe 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.

On this page