66from eth_keys .datatypes import Signature
77from eth_utils import keccak , to_hex
88from hyperliquid .exchange import Exchange
9- from hyperliquid .utils .constants import TESTNET_API_URL , MAINNET_API_URL
109from hyperliquid .utils .signing import get_timestamp_ms , action_hash , construct_phantom_agent , l1_payload
1110from loguru import logger
11+ from pathlib import Path
1212
1313from pusher .config import Config
14+ from pusher .exception import PushError
1415
1516SECP256K1_N_HALF = SECP256K1_N // 2
1617
1718
19+ def _init_client ():
20+ # AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY should be set as environment variables
21+ return boto3 .client (
22+ "kms" ,
23+ # can specify an endpoint for e.g. LocalStack
24+ # endpoint_url="http://localhost:4566"
25+ )
26+
27+
1828class KMSSigner :
19- def __init__ (self , config : Config ):
20- use_testnet = config .hyperliquid .use_testnet
21- url = TESTNET_API_URL if use_testnet else MAINNET_API_URL
22- self .oracle_publisher_exchange : Exchange = Exchange (wallet = None , base_url = url )
23- self .client = self ._init_client (config )
29+ def __init__ (self , config : Config , publisher_exchanges : list [Exchange ]):
30+ self .use_testnet = config .hyperliquid .use_testnet
31+ self .publisher_exchanges = publisher_exchanges
32+
33+ # AWS client and public key load
34+ self .client = _init_client ()
35+ try :
36+ self ._load_public_key (config .kms .aws_kms_key_id_path )
37+ except Exception as e :
38+ logger .exception ("Failed to load public key from KMS; it might be incorrectly configured; error: {}" , repr (e ))
39+ exit ()
2440
41+ def _load_public_key (self , key_path : str ):
2542 # Fetch public key once so we can derive address and check recovery id
26- key_path = config .kms .key_path
27- self .key_id = open (key_path , "r" ).read ().strip ()
28- self .pubkey_der = self .client .get_public_key (KeyId = self .key_id )["PublicKey" ]
43+ self .aws_kms_key_id = Path (key_path ).read_text ().strip ()
44+ pubkey_der = self .client .get_public_key (KeyId = self .aws_kms_key_id )["PublicKey" ]
45+ self .pubkey = serialization .load_der_public_key (pubkey_der )
46+ self ._construct_pubkey_address_and_bytes ()
47+
48+ def _construct_pubkey_address_and_bytes (self ):
2949 # Construct eth address to log
30- pub = serialization .load_der_public_key (self .pubkey_der )
31- numbers = pub .public_numbers ()
50+ numbers = self .pubkey .public_numbers ()
3251 x = numbers .x .to_bytes (32 , "big" )
3352 y = numbers .y .to_bytes (32 , "big" )
3453 uncompressed = b"\x04 " + x + y
35- self .public_key_bytes = uncompressed
3654 self .address = "0x" + keccak (uncompressed [1 :])[- 20 :].hex ()
37- logger .info ("KMSSigner address: {}" , self .address )
38-
39- def _init_client (self , config ):
40- aws_region_name = config .kms .aws_region_name
41- access_key_id_path = config .kms .access_key_id_path
42- access_key_id = open (access_key_id_path , "r" ).read ().strip ()
43- secret_access_key_path = config .kms .secret_access_key_path
44- secret_access_key = open (secret_access_key_path , "r" ).read ().strip ()
45-
46- return boto3 .client (
47- "kms" ,
48- region_name = aws_region_name ,
49- aws_access_key_id = access_key_id ,
50- aws_secret_access_key = secret_access_key ,
51- # can specify an endpoint for e.g. LocalStack
52- # endpoint_url="http://localhost:4566"
55+ logger .info ("public key loaded from KMS: {}" , self .address )
56+
57+ # Parse KMS public key into uncompressed secp256k1 bytes
58+ pubkey_bytes = self .pubkey .public_bytes (
59+ serialization .Encoding .X962 ,
60+ serialization .PublicFormat .UncompressedPoint ,
5361 )
62+ # Strip leading 0x04 (uncompressed point indicator)
63+ self .raw_pubkey_bytes = pubkey_bytes [1 :]
5464
5565 def set_oracle (self , dex , oracle_pxs , all_mark_pxs , external_perp_pxs ):
5666 timestamp = get_timestamp_ms ()
@@ -67,15 +77,24 @@ def set_oracle(self, dex, oracle_pxs, all_mark_pxs, external_perp_pxs):
6777 },
6878 }
6979 signature = self .sign_l1_action (
70- action ,
71- timestamp ,
72- self .oracle_publisher_exchange .base_url == MAINNET_API_URL ,
73- )
74- return self .oracle_publisher_exchange ._post_action (
75- action ,
76- signature ,
77- timestamp ,
80+ action = action ,
81+ nonce = timestamp ,
82+ is_mainnet = not self .use_testnet ,
7883 )
84+ return self ._send_update (action , signature , timestamp )
85+
86+ def _send_update (self , action , signature , timestamp ):
87+ for exchange in self .publisher_exchanges :
88+ try :
89+ return exchange ._post_action (
90+ action = action ,
91+ signature = signature ,
92+ nonce = timestamp ,
93+ )
94+ except Exception as e :
95+ logger .exception ("perp_deploy_set_oracle exception for endpoint: {} error: {}" , exchange .base_url , repr (e ))
96+
97+ raise PushError ("all push endpoints failed" )
7998
8099 def sign_l1_action (self , action , nonce , is_mainnet ):
81100 hash = action_hash (action , vault_address = None , nonce = nonce , expires_after = None )
@@ -88,7 +107,7 @@ def sign_l1_action(self, action, nonce, is_mainnet):
88107 def sign_message (self , message_hash : bytes ) -> dict :
89108 # Send message hash to KMS for signing
90109 resp = self .client .sign (
91- KeyId = self .key_id ,
110+ KeyId = self .aws_kms_key_id ,
92111 Message = message_hash ,
93112 MessageType = "DIGEST" ,
94113 SigningAlgorithm = "ECDSA_SHA_256" , # required for secp256k1
@@ -99,20 +118,12 @@ def sign_message(self, message_hash: bytes) -> dict:
99118 # Ethereum requires low-s form
100119 if s > SECP256K1_N_HALF :
101120 s = SECP256K1_N - s
102- # Parse KMS public key into uncompressed secp256k1 bytes
103- # TODO: Pull this into init
104- pubkey = serialization .load_der_public_key (self .pubkey_der )
105- pubkey_bytes = pubkey .public_bytes (
106- serialization .Encoding .X962 ,
107- serialization .PublicFormat .UncompressedPoint ,
108- )
109- # Strip leading 0x04 (uncompressed point indicator)
110- raw_pubkey_bytes = pubkey_bytes [1 :]
121+
111122 # Try both recovery ids
112123 for v in (0 , 1 ):
113124 sig_obj = Signature (vrs = (v , r , s ))
114125 recovered_pub = sig_obj .recover_public_key_from_msg_hash (message_hash )
115- if recovered_pub .to_bytes () == raw_pubkey_bytes :
126+ if recovered_pub .to_bytes () == self . raw_pubkey_bytes :
116127 return {
117128 "r" : to_hex (r ),
118129 "s" : to_hex (s ),
0 commit comments