Signing Schemes#
ACE supports multiple signing schemes through an extensible registry. Each scheme defines key generation, address derivation, signing, and verification.
Ed25519#
| Property | Value |
|---|---|
| Scheme ID | ed25519 |
| Algorithm | Ed25519 (EdDSA over Curve25519) |
| Key size | 32 bytes (public), 64 bytes (private) |
| Signature size | 64 bytes |
| Address format | Base58 (Solana-style) |
| Typical chains | Solana, general-purpose |
Address Derivation#
publicKey[32 bytes] → Base58Encode(publicKey) → address
Example: 5Ht7RkVSupHeNbGWiHfwJ3RYn4RZfpAv5tk2UrQKbkWR
For Ed25519, the address field IS the Base58-encoded public key. If signingPublicKey is also present, implementations should verify they match.
Signing#
signData = SHA-256(length-prefixed envelope fields)
signature = Ed25519.sign(privateKey, signData)
encoded = Base64(signature[64 bytes])
Verification#
signData = SHA-256(reconstructed length-prefixed fields)
publicKey = Base58Decode(sender.address) → 32 bytes
valid = Ed25519.verify(publicKey, signData, Base64Decode(signature))
Address Normalization#
Base58 addresses are case-sensitive. No normalization is applied.
Libraries#
| Language | Library |
|---|---|
| Swift | CryptoKit (Curve25519.Signing) |
| TypeScript | @noble/ed25519 or tweetnacl |
| Python | PyNaCl or cryptography |
| Rust | ed25519-dalek |
| Go | crypto/ed25519 |
secp256k1#
| Property | Value |
|---|---|
| Scheme ID | secp256k1 |
| Algorithm | ECDSA over secp256k1 |
| Key size | 32 bytes (private), 33 bytes (compressed public) |
| Signature size | 65 bytes (r[32] + s[32] + v[1]) |
| Address format | 0x + hex(keccak256(uncompressedPubKey[1:])[12:]) |
| Typical chains | Ethereum, Base, Polygon, Arbitrum, BNB Chain |
Address Derivation#
privateKey[32 bytes]
→ secp256k1.publicKey(compressed=false)[65 bytes]
→ drop first byte (0x04 prefix)
→ Keccak-256(uncompressedPubKey[64 bytes])
→ take last 20 bytes
→ "0x" + hex(20 bytes)
→ address
Example: 0x7a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b
Signing#
signData = SHA-256(length-prefixed envelope fields)
(v, r, s) = secp256k1.sign_recoverable(privateKey, signData)
encoded = "0x" + hex(r[32] || s[32] || v[1])
The v value is the recovery ID (0 or 1), allowing public key recovery from the signature.
Low-S Normalization
Implementations should normalize signatures to low-S form per EIP-2:
if s > secp256k1_N / 2:
s = secp256k1_N - s
v = v ^ 1
Verification#
signData = SHA-256(reconstructed length-prefixed fields)
recoveredPubKey = secp256k1.recover(signData, r, s, v)
recoveredAddress = keccak256(recoveredPubKey[1:])[12:]
valid = constantTimeEqual(recoveredAddress, sender.address)
Verification uses public key recovery (ecrecover), consistent with Ethereum's signature model.
Address Normalization#
EVM addresses are normalized to lowercase hex with 0x prefix:
"0x7A3B...F91C" → "0x7a3b...f91c"
Comparison must be case-insensitive or performed on normalized addresses.
Libraries#
| Language | Library |
|---|---|
| Swift | GigaBitcoin/secp256k1.swift (module: P256K) |
| TypeScript | @noble/secp256k1 or ethers |
| Python | eth-keys or coincurve |
| Rust | secp256k1 (rust-bitcoin) |
| Go | ethereum/go-ethereum/crypto |
Signature Encoding#
| Scheme | Encoding | Size |
|---|---|---|
ed25519 | Base64 | 64 bytes |
secp256k1 | 0x + hex(r[32] || s[32] || v[1]) | 65 bytes |
Adding New Schemes#
New signing schemes are added via PR to the ACE Protocol specification repository. Each scheme must define key generation, address derivation, signing format, and verification algorithm.