Skip to content

Milestone 2 · Callbacks

Nuvex maintainers
Macro photograph of a populated circuit board

What the request stores

A request holds a callback_program and at most 128 bytes of callback_data. The default pubkey means there is no callback, and fulfilment simply writes the output. Because Milestone 2 passes zero accounts, there is no account list to hash and store on the request.

“A fulfiller cannot choose the callee's accounts.”
ADR 0006: Callback model

The CPI, byte for byte

On fulfilment, oracle core writes the CallbackExecuted status and then performs one cross-program invocation. The instruction is named callback, and its data is fully determined by the request and the verified output.

callback instruction data
  [0..8)    Anchor discriminator of "callback"
  [8..72)   VRF output, 64 bytes
  [72..76)  callback_data length, u32 little endian
  [76..)    the stored callback_data bytes

accounts: none. The request account is not included.

The TypeScript SDK can build those bytes with buildCallbackInstruction, which returns the byte array and does not send a transaction. On the receiving side the Anchor handler takes the output and the data, and its accounts struct is empty:

pub fn callback(_ctx: Context<Callback>, output: [u8; 64], data: Vec<u8>) -> Result<()> {
    msg!("nuvex-callback {}", output[0]);
    Ok(())
}

#[derive(Accounts)]
pub struct Callback {}

That is the test consumer in tests/callback-consumer, which exists so the layout is asserted against the shared writer rather than described in prose. It is not a protocol program.

Why the account list is empty

The rejected alternative explains the decision better than the decision does. If the fulfiller could forward its own remaining accounts, it would be choosing which accounts the callee program sees, which turns a callback into an instruction the fulfiller controls.

“Rejected: forwarding remaining_accounts from the fulfiller. The fulfiller would choose the callee's accounts.”
ADR 0006: Callback model

Oracle accounts are not writable by the callee either, so a callback cannot reach back into protocol state. Account-bearing callbacks are out of scope and would need their own record.

Failure is a revert, not a status

If the callback returns an error, the whole transaction reverts, so the request is not left marked fulfilled with nothing delivered. There is no callback-failed status and no retry instruction, and the ADR names the reason: a retry path lets a fulfiller burn transactions on a callee that always fails.

VerificationRFC 9381
  1. COMMITTEDproof submitted with the output
  2. VERIFIEDECVRF checked by the program
  3. FINALIZED64-byte output written
  4. EXECUTEDcallback ran, no accounts passed
Illustration · not chain data
Illustration · not chain data

Building against an empty account list

A handler with no accounts cannot write state, so the callback is a notification, not a place to settle. The output is on-chain regardless of whether a callback is set, which is where a consumer reads it.

  • Keep the callback handler cheap and total: a failure reverts the fulfiller's transaction.
  • Use the 128 bytes of callback_data to carry the identifier your program needs to recognise the request.
  • Read the VrfResult account under seeds ["verification", request] for the stored output, proof and VRF public key. The request account itself keeps the status and the assigned node, not the output.
  • Settle in your own instruction, signed by whoever is allowed to settle, rather than inside the callback.
A night sky over a mountain ridge
Nuvex maintainersMilestone 5

Protocol

A price median is not a proof

Milestone 5 asks public venues for SOL, BTC, and ETH, drops anything stale or failed, and returns the median only when enough sources remain. The programs still reject a Price request.

Read the post
Earth from orbit beside a spacecraft
Nuvex maintainersMilestone 4

Protocol

The read model copies accounts. It does not decide them

Milestone 4 adds an indexer, a PostgreSQL store, and a read API the console and CLI can query. An empty list is a real answer. A row in that list is still not a finalized result.

Read the post