At the Peking University Chongqing Big Data Research Institute, between 2023 and 2024, the assignment was a permissioned blockchain whose EVM had to run on China's national-standard cryptography before it could be deployed in government and carrier pilots. The chain's virtual machine had inherited Ethereum's precompiled contracts, and those are built on secp256k1, SHA-256 and the alt_bn128 pairing curve. Compliance meant SM2 for signatures, SM4 for symmetric encryption, SM9 for identity-based schemes, and SM3 as the hash underneath all of them. I rebuilt the elliptic-curve precompiles on the SM curves using GmSSL, and added a family of new precompiles for the SM algorithms themselves. These are the decisions that mattered.

Exhibit — what was rebuilt and what was added
rebuilt on SM curves    point addition, scalar multiplication   →  SM2's 256-bit prime curve
                        pairing check                          →  SM9's BN curve
added                   SM2 signature verification, SM4 encrypt / decrypt,
                        SM9 signature verification, SM9 pairing
underneath              GmSSL (C): one implementation for every primitive

1. Why precompiles, and not a Solidity library

Everything above could in principle be written in Solidity. Nobody does, for the same reason Ethereum did not: an elliptic-curve operation in EVM bytecode costs millions of gas, and a pairing check is not feasible at all. A precompiled contract is native code at a fixed address with its own gas formula; the EVM calls it like any other contract and the validator runs it at machine speed. A second reason mattered more to me. A library in Solidity is a new implementation of SM2, written by us and audited by nobody. A precompile lets every contract on the chain call one implementation, GmSSL's, which has been checked against the standard's published test vectors by more people than would ever read ours.

2. ecrecover has no SM2 twin

Ethereum's first precompile, ecrecover, takes a hash and a signature and returns the signer's address; half of Solidity's authentication patterns are built on it. I expected to swap secp256k1 for SM2 and keep the interface. The standard does not allow it.

Exhibit — what SM2 hashes before it signs (GB/T 32918)
Z_A  =  SM3( ENTL_A ‖ ID_A ‖ a ‖ b ‖ x_G ‖ y_G ‖ x_A ‖ y_A )
e    =  SM3( Z_A ‖ M )

The signer's own public key, (x_A, y_A), goes into the digest. To check a signature you must already know whose it is, so recovering the key from the signature is circular. The precompile therefore became verify, not recover: it takes the public key, the signer identity, the message and the signature, and returns true or false. Contracts that derived an identity from recovery had to be rewritten to pass the key in and derive the address from it afterwards. The identity string is a second trap. The standard's default, 1234567812345678, is what most implementations assume; the precompile takes it as an explicit input and documents the default, because a signature made under one ID is silently invalid under another, and that failure looks exactly like a forged signature.

3. The curve precompiles move to the SM curves

Point addition and scalar multiplication were re-implemented over SM2's curve, and the pairing check over SM9's, a 256-bit Barreto–Naehrig curve with an R-ate pairing. I kept Ethereum's wire format on purpose: coordinates as fixed-width 32-byte big-endian integers, the point at infinity as all zeros, and a malformed input as a failure rather than a guess, so that tooling which already knew how to encode a call to 0x06 could keep encoding it.

The check that matters is the one before the arithmetic. Every point that arrives in calldata is validated as on the curve, and for the pairing's second group as in the right subgroup, before it touches GmSSL. A pairing evaluated on a point off the curve is the classic invalid-curve attack, and a precompile is the worst possible place to leave that door open, because every contract on the chain is standing behind it.

4. Only deterministic operations go on-chain

SM2 and SM9 signing draw a random nonce. Put signing in a precompile and every validator produces a different signature for the same transaction, and consensus splits. So there is no signing precompile at all: verification, hashing, point arithmetic and pairings are on-chain, and signing stays where the private key is. SM4 was the borderline case. Encryption and decryption are deterministic once the caller supplies the key and IV, so they can be precompiles, but the documentation has to say the uncomfortable part plainly: anything passed in calldata is on the ledger forever. An on-chain SM4 call is a building block for a protocol that already manages its keys off-chain. It is not a privacy feature, and a precompile that lets people believe otherwise is a liability.

5. Gas follows the native cost

An underpriced precompile is a denial-of-service primitive: a transaction that costs little gas and burns a lot of validator time. Ethereum learned this the hard way and repriced its pairing precompile in EIP-1108 to a base charge plus a per-pair charge. I used the same shape. Each operation was benchmarked with GmSSL on the validators' hardware and priced relative to the existing schedule: a constant for a signature check, a term linear in input length for SM4 and hashing, a base plus per-pair term for the SM9 pairing. I will not quote our final numbers from memory. The method is the point: measure, price the worst case, and measure again when the library or the hardware changes.

6. The C boundary is a security boundary

GmSSL is C and the virtual machine is not, so every precompile call crosses a foreign-function boundary. The rules on that boundary were strict and boring. Inputs are length-checked, then copied into fixed-size buffers. No error from the library is allowed to become a process crash; every failure, a malformed point or a wrong-length key included, becomes a precompile failure that consumes the gas and returns nothing, which is what the EVM specifies. And the decoders were fuzzed, because a validator that can be crashed by calldata is a chain that can be halted by anyone with a wallet.

Exhibit — the tests each precompile had to pass
known-answer    the GB/T standards' own vectors for SM2, SM3, SM4 and SM9
differential    the same inputs through the GmSSL command line; byte-identical output
negative        off-curve points, wrong-subgroup points, truncated inputs, wrong IDs  →  failure, never a wrong "true"
fuzz            random calldata into every decoder; no crash, no hang

What it cost

What I took from it

A standard rarely maps one-to-one onto the API it replaces; the interface has to follow the mathematics, not the old function name, and ecrecover was the proof. Anything that draws randomness stays off the chain, however convenient it would be on it. And a foreign-function boundary inside a consensus engine is a security boundary: the checks that live there are what keep one bad transaction from stopping every node.