Optimising the hyperliquid-python-sdk
Getting 2.5ms order signing to <0.1ms.
The following are all implemented optimizations in the quantpylib’s hyperliquid wrapper.
For a pedestrian signing of a hyperliquid l1-payload such as order actions, the latency cost is a couple of ms. Over a simple experiment of 1000 iterations, a rough latency percentile benchmark gives 2.39-2.57ms in p50-p99 latency.
That is practically the network latency of getting a market data payload from Binance through a cdn, which you might be doing if you are a mm anyway.
We can do much better.
In instances where orders are not preemptively signed, order signing lives directly on the hot path. Hyperliquid’s signing path (from signing.py) is a pipeline of canonicalization and hashing
EIP-712 encoding
and finally, secp256k1 signing.
Optimising this pipeline is a simple matter of appreciating the schema of an EIP712 payload, Ethereum’s structured-data signing format. The signer is given a typed message with 4 pieces:
domain: identifies the application / signing domaintypes: declares the schema of the messageprimaryType: the top-level struct being signedmessage: the actual field values
The eth-account takes encode_typed_data of these components and produces a SignableMessage. On each order sign, it hashes the type definition of the EIP712 domain, performs ABI validation, obtains the domain separator, computes the Agent type hash - all of which are static.
The dynamic fields go into a tiny component, the connection-id and agent hash, which is part of the final digest:
keccak(”\x19\x01” || domain_separator || struct_hash)a 32-byte digest signed by secp256k1. Between parsing the schema, walking the dictionary fields, ABI encoding and object creation/allocation - most of which are static per order - our hot path is not so hot anymore. If we can surgically stick to hashing only the dynamic components, our working set is a lot smaller.
Last but not least, significant performance degradations arise when we rely on native eth-account backend to perform signing. Elliptic curve signing relies on mathematically nontrivial operations, requiring large-integer modular arithmetic and scalar multiplication.
Following the ‘everything is object’ design, simple arithmetic such as ‘+’ requires namespace lookup, operator dispatch, pointer chasing, heap allocation and refcount churn in Python - cryptographic operations implemented at the Python layer is terrible news for low latency code.
Wow, Everything is Computer - President Donald Trump
Wow, Everything is Object - Python Engineer
Again, appreciating this truth is 90% of the job done - we can find solutions to push the cryptographic work from Python runtime into a secp256k1 backend optimised in C, such as the bitcoin-core library. Writing a tiny Python binding around it is the remaining 10%…oh wait, it already exists.
Bringing it all together, we can now replace our sign_l1_action with sign_l1_action_fast.
from coincurve import PrivateKey
_EIP712DOMAIN_TYPEHASH = keccak(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
_DOMAIN_SEPARATOR = keccak(
_EIP712DOMAIN_TYPEHASH
+ keccak(b"Exchange")
+ keccak(b"1")
+ (1337).to_bytes(32, "big")
+ b'\x00' * 32
)
_AGENT_TYPEHASH = keccak(b"Agent(string source,bytes32 connectionId)")
_SOURCE_A = keccak(b"a")
_SOURCE_B = keccak(b"b")
def sign_l1_action_fast(ccwallet, action, vault_prefix, nonce, source_prefix, expires_after):
data = msgpack.packb(action)
data += nonce.to_bytes(8, "big")
data += vault_prefix
if expires_after is not None:
data += b"\x00" + expires_after.to_bytes(8, "big")
connection_id = keccak(data)
agent_hash = keccak(_AGENT_TYPEHASH + source_prefix + connection_id)
digest = keccak(b"\x19\x01" + _DOMAIN_SEPARATOR + agent_hash)
sig = ccwallet.sign_recoverable(digest, hasher=None)
return {"r": to_hex(sig[:32]), "s": to_hex(sig[32:64]), "v": sig[64] + 27}
class Hyperliquid:
def __init__(self...):
...
self.ccwallet = PrivateKey(self.wallet.key)
self._vault_prefix = b"\x00" if self.vault_address is None else (b"\x01" + address_to_bytes(self.vault_address))
self._src_prefix = _SOURCE_A if self.is_mainnet else _SOURCE_Band there we go! Using a faster implementation of cryptographic signatures is most of the speedup, giving us 0.29-0.32ms in the same experiment. Using the pre-computed cache values gives a further 4-fold speedup, @ 0.08-0.09ms.
And that wasn’t even that hard…a simple trick in the Python Houdini






