The TEE node supports VRF as a wallet signing algorithm, enabling generation of provably random values. The scheme is based on "Making NSEC5 Practical for DNSSEC" (Cryptology ePrint Archive, Report 2017/099, Section 2.3).
- Signing algorithm:
keccak256-secp256k1-vrf(wallets.VRFAlgo) - Curve: secp256k1
- Signature size: approximately 939 bytes (JSON-encoded proof)
- Core implementation:
pkg/wallets/vrf/vrf.go
F_WALLET / VRF (instruction, available in base mode only)
The instruction contains:
walletId(bytes32) — target walletkeyId(uint64) — key within the walletnonce(bytes) — input to the VRF, must be non-empty
ABI encoding uses vrfstruct.ITeeVrfVrfInstructionMessage from go-flare-common.
- Decode and validate the instruction payload via
types.ParseVRFInstruction - Load the wallet from storage and verify
SigningAlgo == VRFAlgo - Validate that the cosigners declared in the instruction match the stored wallet's configured cosigner set and threshold (
processorutils.CheckMatchingCosigners), mirroring the XRP signing path - Convert the wallet private key to an ECDSA key
- Call
vrf.VerifiableRandomness(key, nonce)to generate the proof with pre-computed witness points - Return
ProveRandomnessResponse(JSON) containing wallet metadata, nonce, and the VRF proof
No operation is performed. Rewarding data is generated by the instruction framework.
ProveRandomnessResponse (defined in pkg/types/vrf.go):
| Field | Description |
|---|---|
walletId |
Wallet identifier |
keyId |
Key identifier |
nonce |
Input nonce |
proof |
VRF proof (see below) |
| Field | Type | Description |
|---|---|---|
gamma |
Point (x, y) | gamma = sk * HashToCurve(nonce) |
c |
Scalar | Challenge: HashToZn(Pack(G, H, pk, gamma, u, v)) |
s |
Scalar | Response: s = k - sk * c (mod N) |
u |
Point (x, y) | Witness: u = k * G (equals c * pk + s * G) |
cGamma |
Point (x, y) | Witness: c * gamma |
v |
Point (x, y) | Witness: v = k * H (equals c * gamma + s * H) |
zInv |
Field element | modInv(cGamma.X - v.X, P) |
The witness points (u, cGamma, v, zInv) are pre-computed off-chain and required by the on-chain TeeVRFVerifier contract to avoid expensive secp256k1 scalar multiplications in the EVM.
Given private key x, public key Y = x * G, and nonce m:
H = HashToCurve(m)— iterative Keccak256 hashing until a valid curve point is foundgamma = x * H- Sample random
k u = k * G,v = k * Hc = HashToZn(abi.encode(G, H, Y, gamma, u, v))— Keccak256 reduced mod Ns = (k - c * x) mod N- Pre-compute:
cGamma = c * gamma,zInv = modInv(cGamma.X - v.X, P)
If the zInv denominator is zero (negligible probability), proof generation returns an error.
Off-chain verification performs five checks:
zInv * (cGamma.X - v.X) == 1 (mod P)— ZInv is validc * pk + s * G == u— prover knowssksuch thatpk = sk * Gc * gamma == cGamma— cGamma is correctly derivedcGamma + s * H == v— v is correctly derivedHashToZn(abi.encode(G, H, pk, gamma, u, v)) == c— challenge is consistent
Checks 1-4 verify the witness points independently. Check 5 binds c to all committed values. Together they prove gamma == sk * H.
The TeeVRFVerifier Solidity contract (from go-flare-common) implements the same verification logic using ecrecover to avoid expensive elliptic curve operations in the EVM.
Proof.RandomnessFromProof() derives the final randomness output:
keccak256(gamma.X || gamma.Y)
where gamma.X and gamma.Y are 32-byte big-endian encoded coordinates.
- Core VRF:
pkg/wallets/vrf/vrf_test.go - Contract compatibility:
pkg/wallets/vrf/vrf_contract_test.go - Instruction processor:
internal/processors/instructions/vrfutils/vrfutils_test.go - End-to-end:
internal/processors/processor_test.go