AetherNet Canonicalization Specification
AetherNet uses RFC 8785 — JSON Canonicalization Scheme (JCS) for all deterministic JSON serialization. Every operation that signs, hashes, or fingerprints JSON data passes through JCS first. Both the Go server and Python SDK implement JCS independently and must produce identical bytes for identical inputs.
Where JCS is used
| Operation | What is canonicalized | Defined in |
|---|---|---|
| AETHERNET-TX-V1 signing | Transaction envelope (version, chain_id, actor, method, path, body_sha256, created_at, expires_at, nonce) | internal/auth/transaction.go |
| TxID computation | SHA-256 of the canonical sign bytes | internal/auth/transaction.go |
| Body hash | Request body is canonicalized before SHA-256 hashing for the body_sha256 field | internal/auth/txverify.go |
| Evidence hashes | Evidence payloads are canonicalized before hashing for anchoring | internal/evidence/ |
| Event signing | DAG event payloads use canonical serialization | internal/event/ |
Canonicalization rules
JCS (RFC 8785) specifies:
- Sorted keys. Object keys are sorted by Unicode code point (lexicographic byte order). Sorting is recursive — nested objects have their keys sorted too.
- No whitespace. No spaces after colons or commas. No newlines or indentation.
- Compact separators.
,between elements,:between key and value. - Arrays preserve order. Array element order is never changed.
- Literals are lowercase.
true,false,null— never capitalized. - Strings use minimal escaping. Only characters that MUST be escaped per JSON spec are escaped:
",\, and control characters U+0000–U+001F. All other characters (including non-ASCII Unicode) are output as literal UTF-8.
Number formatting
RFC 8785 delegates number formatting to ECMAScript’s Number.toString():
- Integers are serialized without a decimal point or trailing zeros:
1not1.0,0not0.0. - Negative zero becomes
0(not-0). - No scientific notation for values in normal integer range (up to ~1e20).
- Fractional values use the shortest representation that round-trips:
1.5not1.50.
Edge cases AetherNet avoids
AetherNet payloads never contain:
- IEEE 754 special values (
NaN,Infinity,-Infinity) — these are rejected at the API layer. The canonicalizer serializes them asnullper RFC 8785, but they should never appear in protocol data. - Negative zero (
-0) — canonicalized to0if encountered, but not generated by the protocol. - Numbers requiring scientific notation — all protocol numeric values are integers well within safe range.
These edge cases are handled defensively in the canonicalizer but are not part of the protocol’s valid input space.
SDK utilities
Both the Go and Python SDKs expose canonicalization utilities for third-party verifiers.
Python
from aethernet import canonical_hash, canonical_bytes
from aethernet.signing import canonicalize_json
# Raw canonical bytes (for signature verification)
raw = canonical_bytes({"b": 2, "a": 1})
# b'{"a":1,"b":2}'
# SHA-256 hash of canonical bytes (for TxID / evidence hash verification)
h = canonical_hash({"b": 2, "a": 1})
# "43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777"
Go
import "github.com/Aethernet-network/aethernet/pkg/sdk"
raw, err := sdk.CanonicalBytes(map[string]any{"b": 2, "a": 1})
// []byte(`{"a":1,"b":2}`)
hash, err := sdk.CanonicalHash(map[string]any{"b": 2, "a": 1})
// "43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777"
Cross-language test vectors
These vectors are the ground truth. Both Go and Python implementations are tested against them. If you are implementing a third-party verifier, use these to validate your canonicalization.
| Name | Input | Canonical bytes | SHA-256 |
|---|---|---|---|
| simple_key_sorting | {"b":2,"a":1} | {"a":1,"b":2} | 43258cff...292777 |
| nested_recursive_sorting | {"outer":{"z":1,"a":2},"inner":[3,1,2]} | {"inner":[3,1,2],"outer":{"a":2,"z":1}} | ab9eb50b...ef71ad |
| integer_zero | {"val":0} | {"val":0} | 3d327872...1415070a |
| integer_negative | {"val":-1} | {"val":-1} | bd78e10a...70a373 |
| integer_large | {"val":1000000000000} | {"val":1000000000000} | e4064fa9...f7190b |
| empty_object | {} | {} | 44136fa3...caaff8a |
| empty_string_value | {"a":""} | {"a":""} | 258555fe...5d81b9 |
| empty_array_value | {"a":[]} | {"a":[]} | 50e86600...f2cc55 |
| unicode_trademark | {"name":"AetherNet™"} | {"name":"AetherNet™"} | 55c82259...bb90d |
| boolean_and_null | {"flag":true,"nothing":null} | {"flag":true,"nothing":null} | aecd9894...6eab3 |
| tx_v1_payload | (full TX-V1 envelope) | {"actor":"abc123def456","body_sha256":"e3b0c442...","chain_id":"aethernet-testnet-1","created_at":1700000000,"expires_at":1700000120,"method":"POST","nonce":"deadbeef01234567","path":"/v1/agents/register","version":"AETHERNET-TX-V1"} | 25c54f03...9100cf |
Full SHA-256 hashes and complete test data are in:
- Go:
internal/auth/canonical_test.go - Python:
sdk/python/tests/test_canonical_crosslang.py