Skip to main content

evix-verify

Check an Evix attestation yourself — offline, with nothing but Python, and without trusting Evix's servers.

pip install evix-verify     # or copy one file: src/evix_verify/verify.py
evix-verify attestation.json --disclosure disclosure.json \
    --attester did:pkh:eip155:42161:0x… --authority authority.json
matches

This repository holds the format (a specification, a reference library and test vectors) and an independent verifier for attestations published by Evix, a KYB/KYC platform. Python 3.10+, no dependencies.

What an Evix attestation is

When Evix derives a fact from documents — say, that a person's effective ownership of a company is between 25 and 50 % — it can publish a commitment to that fact on a public ledger, so that the fact can later be proven to a bank, an auditor or a regulator without anyone having to trust Evix's word or its servers being up.

What goes on chain is deliberately almost nothing: an Ethereum Attestation Service record holding five 32-byte values —

Field What it is
root the root of a Merkle tree of salted commitments, one per field of the fact
method a hash of the rule and engine versions the fact was computed with
inputs the root of a tree over the hashes of the documents it was computed from
subjectRef a per-case salted pseudonym of the person — unlinkable across cases
level who asserts it: 1 self-asserted, 2 machine-derived, 3 adopted by a regulated person

No names, no values, not even the kind of fact: those live in the tree, under per-field secret salts. The holder of the fact (the company, the person, or the obliged entity they gave it to) can then disclose one field — say the share range — by handing over a small JSON disclosure package with the value, its salt and the Merkle path. Everything else in the tree stays hidden.

Each publication batch is also timestamped in Bitcoin through OpenTimestamps, so "when" survives even if the L2 chain stops.

What is attested

Three kinds of derived fact, and only these — screening findings (sanctions, PEP status, adverse media) are never attested:

Kind Fields in its tree
effective_share — a person's effective share in a company along the whole ownership chain subject, company, basis_points, as_of
ownership_path — the chain that share was computed along subject, company, path, as_of
identity_resolution — whether two spellings of a name are the same person subject, alias, same, as_of

Each field has one published value format, whatever kind it is in:

Field Value
subject, company, alias a node key — a non-empty string, opaque (do not parse it)
as_of the date of the data, "YYYY-MM-DD"
basis_points {"min_bp": 2500, "max_bp": 5000, "fully_traced": true} — a range in hundredths of a percent, 0 ≤ min_bp ≤ max_bp ≤ 10000; fully_traced: false means part of the chain was not traced
path every chain the share was computed along, each from the company out to the person: [[{"holder", "company", "min_bp", "max_bp", "kind": "known" | "inferred" | "unknown"}, …], …]
same true (one person) or false (different people)

The list and the formats are in src/evix_ledger/catalogue.py — validate(kind, values) returns what is wrong with a fact — and in SPEC §4.1, with examples. The commitment format itself does not depend on the catalogue; the verifier checks a disclosed value against its field's format once the cryptography holds.

The commitment and tree, in brief

commit(field, value, salt) = sha256( u32 len ‖ 00"kyc/commit/v1"
                                   ‖ u32 len ‖ field
                                   ‖ u32 len ‖ salt (16 random bytes)
                                   ‖ u32 len ‖ canonical JSON of value )   # RFC 8785 subset, no floats
node(a, b)                 = sha256( 01"kyc/node/v1" ‖ a ‖ b )
filler(i)                  = sha256( 02"kyc/filler/v1" ‖ tree_salt ‖ u32 i )

Leaves are sorted by commitment value (so a leaf's position does not reveal which field it is) and padded with fillers to 32, so the root does not reveal how many fields there are and every path has exactly five steps. The SPEC.md gives every byte, and vectors/v2/ has test vectors for each step, with one tree per kind of fact.

How to verify without Evix

You need three things, none of which come from Evix's servers:

  1. The disclosure package — from whoever is showing you the fact.

  2. The attestation record — from the chain, fetched by you with any Ethereum client. With Foundry's cast:

    cast call 0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458 \
        'getAttestation(bytes32)((bytes32,bytes32,uint64,uint64,uint64,bytes32,address,address,bool,bytes))' \
        <uid> --rpc-url <your Arbitrum One node>
    

    and save it as JSON (uid, schema, time, attester, revocationTime, refUID, data). Or let the verifier ask a node you name:

    evix-verify <uid> --rpc https://your-node --disclosure disclosure.json --save record.json
    

    --rpc is off by default; without it the verifier makes no network request at all. A node's answer is that node's word — use your own, or ask two. The verifier checks that the record is under the Evix attestation schema (0x9c5330df…e64bc5, see SPEC §6.1) and refuses anything else.

  3. Who the signer should be — the address or DID the issuer publishes as its attester (Evix lists current and retired signers, with dates, at /.well-known/attester.json on its API host), as of the attestation's time.

Then:

evix-verify record.json --disclosure disclosure.json --attester did:pkh:eip155:42161:0x…
Answer Exit Meaning
matches 0 the disclosed value is in the tree, and the attestation stands
mismatch 1 it is not — wrong value, or the salt was destroyed (erasure)
superseded 2 it matches, but a later attestation replaced it
revoked 3 it matches, but the attestation was revoked
unusable 4 wrong format, wrong schema, unreadable input
foreign 5 it matches, but someone other than the expected signer signed it
unauthorised 8 the signer was not authorised at that moment (with --authority)
malformed 9 it matches and stands, but the disclosed value is not in its field's published format

Is the value what its field says? A root proves which bytes were committed to, not that they make sense: an inverted range reaches the root as well as a sane one. So when everything else holds, the verifier checks the disclosed value against SPEC §4.1 and answers malformed (exit 9) if it is out of format, listing the problems on stderr. A stronger answer (revoked, foreign, …) is never replaced by it; the problems are still listed. --no-check-values skips the check and says so.

Was the signer authorised then? Pass the signer-authority records (a second EAS schema, SPEC §9) with --authority file.json or, with --rpc, --authority-uid <uid>. The verifier answers for the attestation's own time, prints who stated each authority, and leaves the trust decision to you. Without --authority it says on stderr that this question was not asked.

When was it published? Pass the batch's .ots file and either the 32-byte batch root or the list of attestation roots in the batch:

evix-verify record.json --timestamp batch.ots --batch batch-roots.json \
    --bitcoin-header <80-byte header hex from your Bitcoin node>

This checks that the attestation is in the batch, that the proof is about that batch, and — with --bitcoin-header — that the block header holds the merkle root the proof leads to. For a fuller OpenTimestamps tool (stamp, upgrade, Esplora lookups) see the companion project otskit.

Try it on the worked example — an effective_share fact (Jane Doe holds 25–50 % of Example Trading Ltd through Example Holdings Ltd, chain fully traced) whose basis_points field was disclosed to a bank:

evix-verify vectors/v2/example/attestation.json --disclosure vectors/v2/example/disclosure.json \
    --attester "$(cat vectors/v2/example/attester.txt)" --authority vectors/v2/example/authority.json
matches

The package (vectors/v2/example/disclosure.json) carries the one field:

{
  "version": "kyc/disclosure/v1",
  "uid": "0x9a50…c56f",
  "field": "basis_points",
  "value": {"fully_traced": true, "max_bp": 5000, "min_bp": 2500},
  "salt": "297df04ccf0dbbf20badccfae6589c9f",
  "path": ["1407…1235", "… 5 entries …"],
  "directions": [true, false, false, false, false],
  "recipient": "Example Bank",
  "reason": ""
}

The other three fields of the fact (subject, company, as_of) stay hidden in the tree; the bank learns the range, that it is fully traced, and that it is what was attested.

Two implementations, on purpose

  • src/evix_ledger/ — the reference library: canonical form, commitments, trees, payload, disclosure packages, authority records, DIDs, batch roots. It is what an issuer uses to produce attestations.
  • src/evix_verify/verify.py — the verifier. It imports nothing from evix_ledger, only the standard library, and re-implements the format from the specification. The test suite checks that the two agree on every value and on every vector. If they agree, the format is reproducible from its description — which is the claim that matters.

Both include plain-Python Keccak-256 (and the verifier RIPEMD-160), so they work on Python builds whose OpenSSL lacks those hashes.

Limits — what this does not prove

  • The chain is only as trustworthy as your node. With --rpc, the record is whatever that node returns. Use your own node or cross-check two.
  • Supersession needs an indexer. EAS keeps no index of which attestations point at a given one, so a node cannot be asked. With --rpc the verifier says "superseded: not checked"; pass --superseded-by <uid> (from the EAS GraphQL API or your own scan of Attested events), or a record file with superseded_by.
  • Authority records are the publisher's statement. They add a timestamp and immutability, not an independent source.
  • The Bitcoin check is not SPV. The verifier compares the proof with a header you supply; it does not check that the header is in the Bitcoin chain.
  • Disclosed means disclosed. A package can be forwarded; the recipient field records whom it was given to, but nothing enforces it.
  • Deployment status (2026-10-20): the attestation schema is registered on Arbitrum Sepolia (a testnet — reset, not evidence); it is not yet registered on Arbitrum One, and the authority schema is not yet registered on either.

Tests

pip install pytest && pytest        # offline, ~5 seconds
pytest -m network                   # also asks a public Arbitrum Sepolia node
python tools/make_vectors.py --check

tests/vectors/ots/ holds example proofs from the OpenTimestamps repositories and real Bitcoin block headers (see the PROVENANCE file there).

Licence

Copyright 2026 Lelimo Capital sp. z o.o. Licensed under the Apache License, Version 2.0. Contributions are accepted under the same licence.

Metadata

Release files for evix-verify 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for evix-verify 0.3.1
File Size Uploaded Provenance
evix_verify-0.3.1.tar.gz 130.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for evix-verify 0.3.1
File Interpreter ABI Platform
evix_verify-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 197.8 kB

Release files / evix_verify-0.3.1.tar.gz

Download URL evix_verify-0.3.1.tar.gz
Size 130.2 kB
Tags Source
SHA-256 checksum
How to use checksums
5e350a0ea105da10efaeb949e9883ef320558132ed2b77edeab611db1690c673
BLAKE2b-256 checksum
How to use checksums
5274ebc4838dc35d576540454f511a51ca6466947e95f61484768836aeaaab49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / evix_verify-0.3.1-py3-none-any.whl

Download URL evix_verify-0.3.1-py3-none-any.whl
Size 67.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
498b4847cfb6848b03fb2ef8ab6d9c92e21fc5e80a0a55d28ac006db14ba066c
BLAKE2b-256 checksum
How to use checksums
dbff04288841b816fadea1f74bd5710371794f50f6708409b166a4f6d02e71ff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for evix-verify 0.3.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page