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:
-
The disclosure package — from whoever is showing you the fact.
-
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--rpcis 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. -
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.jsonon 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 fromevix_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
--rpcthe verifier says "superseded: not checked"; pass--superseded-by <uid>(from the EAS GraphQL API or your own scan ofAttestedevents), or a record file withsuperseded_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
recipientfield 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| evix_verify-0.3.1.tar.gz | 130.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.