usigverifier

package
v0.0.48 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 11 Imported by: 0

README

usigverifier — Universal Signature Verifier Precompile

The only EVM precompile Push Chain ships on top of the cosmos-evm baseline. Verifies Ed25519 signatures inside the EVM so Solidity contracts can authenticate Solana-style signatures (or any other Ed25519 input) without re-implementing the curve in EVM bytecode.

Addresses

Address Why it exists
0x00000000000000000000000000000000000000ca Original "legacy" address. Hardcoded into contracts deployed before the address-range cleanup.
0xEC00000000000000000000000000000000000001 New address in the reserved Push precompile range (0xEC...).

Both addresses are registered simultaneously and point at the same implementation. Backward compatibility for previously-deployed contracts is the only reason the legacy address still exists. New code should target 0xEC00000000000000000000000000000000000001.

Wired into app/app.go:781-795:

usigverifierPrecompile, _   := usigverifierprecompile.NewPrecompile()
usigverifierPrecompileV2, _ := usigverifierprecompile.NewPrecompileV2()
corePrecompiles[usigverifierPrecompile.Address()]   = usigverifierPrecompile
corePrecompiles[usigverifierPrecompileV2.Address()] = usigverifierPrecompileV2

Solidity Interface

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.18;

address constant USigVerifier_PRECOMPILE_ADDRESS    = 0x00000000000000000000000000000000000000ca;
address constant USigVerifier_PRECOMPILE_ADDRESS_V2 = 0xEC00000000000000000000000000000000000001;

interface IUSigVerifier {
    /// Verifies signature over `"0x" + hex(msgDigest)` (66 ASCII bytes).
    /// Used by UEA_SVM. Solana wallets render the hex string in their sign-message UI.
    function verifyEd25519(bytes calldata pubKey, bytes32 msgDigest, bytes calldata signature)
        external view returns (bool);

    /// Verifies signature over the raw message bytes (standard Ed25519 semantics).
    /// Use this if your signer uses the conventional `ed25519.Sign(privKey, rawBytes)` API.
    function verifyEd25519RawMessage(bytes calldata pubKey, bytes calldata message, bytes calldata signature)
        external view returns (bool);
}
Method Signed bytes Gas Use when
verifyEd25519(bytes,bytes32,bytes) "0x" + hex(msgDigest) (66 ASCII bytes) 4000 (flat) UEA_SVM / Solana-wallet flows where the user signs a hex string in Phantom/Solflare
verifyEd25519RawMessage(bytes,bytes,bytes) Raw message bytes 4000 + 12 per 32-byte word of message New integrations / relayers using standard ed25519.Sign(privKey, rawBytes)

Both methods are view and touch no chain state.

Why only the raw method scales with size

ed25519.Verify hashes the whole message, so its CPU cost grows with the message (~58 µs at 32 B, ~146 µs at 128 KiB, ~922 µs at 1 MB). verifyEd25519 always verifies the same 66-byte ASCII string no matter what the caller sends, so its cost is constant and its price stays flat. verifyEd25519RawMessage verifies caller-supplied bytes, so it is priced per 32-byte word — the same per-word rate the EVM SHA-256 precompile charges for comparable hashing work.

Because both methods are view, a contract can park one large message in memory and loop STATICCALLs over it, paying the calldata only once. Pricing alone is therefore not the whole defence: message is also hard-capped at 128 KiB (MaxEd25519MessageBytes), and anything larger reverts with message too large instead of being verified.

len(message) Gas
0 4,000
32 B 4,012
1 KiB 4,384
8 KiB 7,072
64 KiB 28,576
128 KiB (cap) 53,152
> 128 KiB reverts

Verification Semantics

Two methods, two distinct signing conventions. A signature produced for one method will not verify under the other — the test vectors in query_test.go lock this in.

verifyEd25519 — hex-ASCII convention (legacy / wallet-friendly)

Internally (query.go:VerifyEd25519), the bytes32 msgDigest is rendered as a 0x-prefixed hex string before being passed to ed25519.Verify:

msgStr := "0x" + hex.EncodeToString(msg)  // 66 ASCII bytes
msgBytes := []byte(msgStr)
ok = ed25519.Verify(pubKeyBytes, msgBytes, signature)

The off-chain signer must sign the 66-byte ASCII string "0x"+hex(digest), not the raw 32 bytes. This is what UEA_SVM uses so that a Solana wallet (Phantom, Solflare) shows the user a copy-pasteable hex string in its sign-message prompt rather than opaque bytes.

verifyEd25519RawMessage — raw-bytes convention (standard)

Standard Ed25519 verification — signature is checked against the raw message bytes:

ok = ed25519.Verify(pubKeyBytes, message, signature)

Use this when your signer uses ed25519.Sign(privKey, rawBytes) (default in every Solana SDK / nacl library). message may be any length up to MaxEd25519MessageBytes (128 KiB), not just 32 bytes.

Common rules
  • pubKey must be exactly 32 bytes; signature must be exactly 64 bytes — otherwise the precompile reverts with invalid params.
  • verifyEd25519RawMessage reverts with message too large past MaxEd25519MessageBytes (128 KiB).
  • Unknown method IDs revert with the standard unknown method error.
  • verifyEd25519 costs a flat 4000 gas; verifyEd25519RawMessage costs 4000 plus 12 per 32-byte word of message.

Generating the ABI

If USigVerifier.sol is changed, regenerate abi.json with:

cd precompiles/usigverifier
solcjs USigVerifier.sol --abi
mv *.abi abi.json
jq --argjson abi "$(cat abi.json)" \
   '{"_format": "hh-sol-artifact-1", "contractName": "USigVerifier",
     "sourceName": "precompiles/USigVerifier.sol",
     "bytecode": "0x", "deployedBytecode": "0x",
     "linkReferences": {}, "deployedLinkReferences": {},
     "abi": $abi}' <<< '{}' > abi.json

The Go binary embeds abi.json via //go:embed, so a fresh make build will pick up the change.

Testing from the Command Line

# Make sure the precompile is enabled in the EVM params:
# app_state["evm"]["params"]["active_static_precompiles"] must include
# 0x00000000000000000000000000000000000000ca and/or 0xEC00000000000000000000000000000000000001
# (test_node.sh installs the legacy address by default).

cast call 0xEC00000000000000000000000000000000000001 \
    "verifyEd25519(bytes,bytes32,bytes)" \
    "<32-byte pubKey hex>" \
    "<bytes32 digest>" \
    "<64-byte signature hex>"

# Decode the boolean response
cast abi-decode "verifyEd25519(bytes,bytes32,bytes)(bool)" <returndata>

If the call returns 0x (empty), the precompile is not in active_static_precompiles for the current chain — that's a configuration issue, not a verification failure.

Layout

precompiles/usigverifier/
|-- USigVerifier.sol     Solidity interface (the source of truth for the ABI)
|-- abi.json             Embedded into the binary via go:embed
|-- usigverifier.go      Precompile struct, NewPrecompile / NewPrecompileV2, RequiredGas (gas schedule), Run
|-- query.go             VerifyEd25519 / VerifyEd25519RawMessage method handlers
|-- gas_test.go          Gas-schedule + size-cap regression tests and benchmarks
+-- README.md            (this file)

Documentation

Index

Constants

View Source
const (
	VerifyEd25519Method           = "verifyEd25519"
	VerifyEd25519RawMessageMethod = "verifyEd25519RawMessage"
)
View Source
const (
	// Both addresses stay registered: 0x…CA is live on donut and dropping it
	// would unregister the precompile for contracts already calling it.
	USigVerifierPrecompileAddress   = "0x00000000000000000000000000000000000000ca"
	USigVerifierPrecompileAddressV2 = "0xEC00000000000000000000000000000000000001"
	// VerifyEd25519Gas is the gas cost for verifying an Ed25519 signature over a
	// bytes32 digest. The verified message is always the 66-byte ASCII hex form
	// of that digest, so the work does not vary with the calldata — flat is the
	// honest price here.
	VerifyEd25519Gas uint64 = 4000
	// VerifyEd25519RawMessageBaseGas is the fixed part of a raw-message
	// verification: the Ed25519 curve arithmetic, which dominates below ~8 KiB.
	VerifyEd25519RawMessageBaseGas uint64 = 4000
	// VerifyEd25519RawMessagePerWordGas is charged for every 32-byte word of the
	// message on top of the base, so that the SHA-512 pass Ed25519 makes over the
	// whole message is paid for. Priced off the EVM SHA-256 precompile, which
	// charges 12 gas per 32-byte word for comparable hashing work.
	VerifyEd25519RawMessagePerWordGas uint64 = 12
	// MaxEd25519MessageBytes hard-caps the message a raw-message verification
	// accepts (128 KiB, the same limit used for gateway payloads). This is a view
	// method, so a contract can hold one large message in memory and loop
	// STATICCALLs over it, paying the calldata only once; on that path a price
	// curve alone is not a defence, the size has to be bounded outright.
	MaxEd25519MessageBytes = 128 * 1024
)

Variables

View Source
var ABI abi.ABI

Functions

func GetAddress

func GetAddress() common.Address

return address of the precompile

func GetAddressV2 added in v0.0.23

func GetAddressV2() common.Address

GetAddressV2 returns the new (0xEC..01) address of the precompile

Types

type Precompile

type Precompile struct {
	cmn.Precompile
	abi.ABI
}

Precompile defines the precompile

func NewPrecompile

func NewPrecompile() (*Precompile, error)

func NewPrecompileV2 added in v0.0.23

func NewPrecompileV2() (*Precompile, error)

NewPrecompileV2 creates a new USigVerifier precompile at the new address (0xEC..01). It provides the same functionality as NewPrecompile but at the reserved address range.

func (Precompile) IsTransaction

func (Precompile) IsTransaction(method *abi.Method) bool

IsTransaction checks if the given method name corresponds to a transaction or query.

func (Precompile) RequiredGas

func (p Precompile) RequiredGas(input []byte) uint64

RequiredGas is charged before Run executes, so it runs on unvalidated, attacker-controlled calldata and must never panic.

func (Precompile) Run

func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error)

func (Precompile) VerifyEd25519

func (p Precompile) VerifyEd25519(
	method *abi.Method,
	args []interface{},
) ([]byte, error)

VerifyEd25519 verifies a signature over the ASCII bytes of "0x"+hex(msgDigest). This is the legacy / Solana-wallet-friendly form used by UEA_SVM.

func (Precompile) VerifyEd25519RawMessage added in v0.0.40

func (p Precompile) VerifyEd25519RawMessage(
	method *abi.Method,
	args []interface{},
) ([]byte, error)

VerifyEd25519RawMessage verifies a signature over raw message bytes — standard Ed25519 semantics. Use this when the signer used the conventional ed25519.Sign(privKey, rawBytes) API. Messages larger than MaxEd25519MessageBytes are rejected.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL