6
6
from eth_keys .datatypes import Signature
7
7
from eth_utils import keccak , to_hex
8
8
from hyperliquid .exchange import Exchange
9
- from hyperliquid .utils .constants import TESTNET_API_URL , MAINNET_API_URL
10
9
from hyperliquid .utils .signing import get_timestamp_ms , action_hash , construct_phantom_agent , l1_payload
11
10
from loguru import logger
11
+ from pathlib import Path
12
12
13
13
from pusher .config import Config
14
+ from pusher .exception import PushError
14
15
15
16
SECP256K1_N_HALF = SECP256K1_N // 2
16
17
17
18
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
+
18
28
class 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 ()
24
40
41
+ def _load_public_key (self , key_path : str ):
25
42
# 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 ):
29
49
# 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 ()
32
51
x = numbers .x .to_bytes (32 , "big" )
33
52
y = numbers .y .to_bytes (32 , "big" )
34
53
uncompressed = b"\x04 " + x + y
35
- self .public_key_bytes = uncompressed
36
54
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 ,
53
61
)
62
+ # Strip leading 0x04 (uncompressed point indicator)
63
+ self .raw_pubkey_bytes = pubkey_bytes [1 :]
54
64
55
65
def set_oracle (self , dex , oracle_pxs , all_mark_pxs , external_perp_pxs ):
56
66
timestamp = get_timestamp_ms ()
@@ -67,15 +77,24 @@ def set_oracle(self, dex, oracle_pxs, all_mark_pxs, external_perp_pxs):
67
77
},
68
78
}
69
79
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 ,
78
83
)
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" )
79
98
80
99
def sign_l1_action (self , action , nonce , is_mainnet ):
81
100
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):
88
107
def sign_message (self , message_hash : bytes ) -> dict :
89
108
# Send message hash to KMS for signing
90
109
resp = self .client .sign (
91
- KeyId = self .key_id ,
110
+ KeyId = self .aws_kms_key_id ,
92
111
Message = message_hash ,
93
112
MessageType = "DIGEST" ,
94
113
SigningAlgorithm = "ECDSA_SHA_256" , # required for secp256k1
@@ -99,20 +118,12 @@ def sign_message(self, message_hash: bytes) -> dict:
99
118
# Ethereum requires low-s form
100
119
if s > SECP256K1_N_HALF :
101
120
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
+
111
122
# Try both recovery ids
112
123
for v in (0 , 1 ):
113
124
sig_obj = Signature (vrs = (v , r , s ))
114
125
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 :
116
127
return {
117
128
"r" : to_hex (r ),
118
129
"s" : to_hex (s ),
0 commit comments