Signing Schemes#

ACE supports multiple signing schemes through an extensible registry. Each scheme defines key generation, address derivation, signing, and verification.

Ed25519#

PropertyValue
Scheme IDed25519
AlgorithmEd25519 (EdDSA over Curve25519)
Key size32 bytes (public), 64 bytes (private)
Signature size64 bytes
Address formatBase58 (Solana-style)
Typical chainsSolana, 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#

LanguageLibrary
SwiftCryptoKit (Curve25519.Signing)
TypeScript@noble/ed25519 or tweetnacl
PythonPyNaCl or cryptography
Rusted25519-dalek
Gocrypto/ed25519

secp256k1#

PropertyValue
Scheme IDsecp256k1
AlgorithmECDSA over secp256k1
Key size32 bytes (private), 33 bytes (compressed public)
Signature size65 bytes (r[32] + s[32] + v[1])
Address format0x + hex(keccak256(uncompressedPubKey[1:])[12:])
Typical chainsEthereum, 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#

LanguageLibrary
SwiftGigaBitcoin/secp256k1.swift (module: P256K)
TypeScript@noble/secp256k1 or ethers
Pythoneth-keys or coincurve
Rustsecp256k1 (rust-bitcoin)
Goethereum/go-ethereum/crypto

Signature Encoding#

SchemeEncodingSize
ed25519Base6464 bytes
secp256k10x + 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.