66//! the account.
77
88use anyhow:: Result ;
9- use bulk_keychain:: { Action , Keypair , Pubkey , PythOraclePrice , SignedTransaction , Signer } ;
9+ use bulk_keychain:: {
10+ Action , Keypair , Pubkey , PythOraclePrice , SignatureDomain , SignedTransaction , Signer ,
11+ } ;
1012
1113/// Wrapper for signing keys using bulk-keychain.
1214///
@@ -21,15 +23,23 @@ pub struct BulkSigner {
2123
2224impl BulkSigner {
2325 /// Create a new signer from a base58-encoded private key and oracle account.
24- pub fn new ( private_key_base58 : & str , oracle_account_base58 : & str ) -> Result < Self > {
26+ ///
27+ /// `signature_domain` selects the BULK network the signature commits to; it must
28+ /// match the network the configured validator endpoints belong to, otherwise the
29+ /// signature is rejected (see `BulkConfig::signature_domain`).
30+ pub fn new (
31+ private_key_base58 : & str ,
32+ oracle_account_base58 : & str ,
33+ signature_domain : SignatureDomain ,
34+ ) -> Result < Self > {
2535 let keypair = Keypair :: from_base58 ( private_key_base58)
2636 . map_err ( |e| anyhow:: anyhow!( "failed to parse keypair: {}" , e) ) ?;
2737
2838 let oracle_account = Pubkey :: from_base58 ( oracle_account_base58)
2939 . map_err ( |e| anyhow:: anyhow!( "failed to parse oracle account pubkey: {}" , e) ) ?;
3040
3141 let pubkey_base58 = keypair. pubkey ( ) . to_string ( ) ;
32- let signer = Signer :: new ( keypair) ;
42+ let signer = Signer :: new ( keypair, signature_domain ) ;
3343
3444 Ok ( Self {
3545 signer,
@@ -66,33 +76,39 @@ mod tests {
6676 // Test keypair (generated offline, safe for tests only)
6777 const TEST_PRIVATE_KEY_BASE58 : & str = "4wBqpZM9k1k4reVTJezJTqcPYLkuJSYwZYfwJC3xjYw9" ;
6878
79+ const TEST_DOMAIN : SignatureDomain = SignatureDomain :: Devnet ;
80+
6981 fn test_oracle_account ( ) -> String {
7082 // Use the signer's own pubkey as oracle account for tests
7183 let keypair = Keypair :: from_base58 ( TEST_PRIVATE_KEY_BASE58 ) . unwrap ( ) ;
7284 keypair. pubkey ( ) . to_string ( )
7385 }
7486
87+ fn test_signer ( ) -> BulkSigner {
88+ BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & test_oracle_account ( ) , TEST_DOMAIN ) . unwrap ( )
89+ }
90+
7591 #[ test]
7692 fn test_signer_creation ( ) {
77- let signer = BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & test_oracle_account ( ) ) . unwrap ( ) ;
93+ let signer = test_signer ( ) ;
7894 assert ! ( !signer. pubkey_base58( ) . is_empty( ) ) ;
7995 }
8096
8197 #[ test]
8298 fn test_signer_invalid_key ( ) {
83- let result = BulkSigner :: new ( "invalid-key" , "invalid-account" ) ;
99+ let result = BulkSigner :: new ( "invalid-key" , "invalid-account" , TEST_DOMAIN ) ;
84100 assert ! ( result. is_err( ) ) ;
85101 }
86102
87103 #[ test]
88104 fn test_signer_empty_key ( ) {
89- let result = BulkSigner :: new ( "" , "" ) ;
105+ let result = BulkSigner :: new ( "" , "" , TEST_DOMAIN ) ;
90106 assert ! ( result. is_err( ) ) ;
91107 }
92108
93109 #[ test]
94110 fn test_sign_transaction ( ) {
95- let mut signer = BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & test_oracle_account ( ) ) . unwrap ( ) ;
111+ let mut signer = test_signer ( ) ;
96112
97113 let oracles = vec ! [ PythOraclePrice {
98114 timestamp: 1704067200000 ,
@@ -112,7 +128,8 @@ mod tests {
112128 #[ test]
113129 fn test_sign_transaction_multiple_oracles ( ) {
114130 let oracle_account = test_oracle_account ( ) ;
115- let mut signer = BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & oracle_account) . unwrap ( ) ;
131+ let mut signer =
132+ BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & oracle_account, TEST_DOMAIN ) . unwrap ( ) ;
116133
117134 let oracles = vec ! [
118135 PythOraclePrice {
@@ -149,7 +166,8 @@ mod tests {
149166 fn test_sign_transaction_uses_oracle_account ( ) {
150167 // Verify the transaction uses the oracle account, not the signer's own pubkey
151168 let oracle_account = test_oracle_account ( ) ;
152- let mut signer = BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & oracle_account) . unwrap ( ) ;
169+ let mut signer =
170+ BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & oracle_account, TEST_DOMAIN ) . unwrap ( ) ;
153171
154172 let oracles = vec ! [ PythOraclePrice {
155173 timestamp: 1000 ,
@@ -164,9 +182,42 @@ mod tests {
164182 assert_eq ! ( tx. signer, signer. pubkey_base58( ) ) ;
165183 }
166184
185+ #[ test]
186+ fn test_signature_domain_changes_signature ( ) {
187+ // The domain is committed into the signature preimage but is not part of the
188+ // transaction payload, so a mismatch is only observable as a rejected signature.
189+ // Signing identical input under different domains must diverge.
190+ let oracle_account = test_oracle_account ( ) ;
191+ let oracles = vec ! [ PythOraclePrice {
192+ timestamp: 1000 ,
193+ feed_index: 1 ,
194+ price: 100 ,
195+ exponent: -2 ,
196+ } ] ;
197+
198+ let sign_with = |domain| {
199+ BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & oracle_account, domain)
200+ . unwrap ( )
201+ . sign_transaction ( oracles. clone ( ) , 42 )
202+ . unwrap ( )
203+ } ;
204+
205+ let mainnet = sign_with ( SignatureDomain :: Mainnet ) ;
206+ let testnet = sign_with ( SignatureDomain :: Testnet ) ;
207+ let devnet = sign_with ( SignatureDomain :: Devnet ) ;
208+
209+ assert_ne ! ( mainnet. signature, testnet. signature) ;
210+ assert_ne ! ( mainnet. signature, devnet. signature) ;
211+ assert_ne ! ( testnet. signature, devnet. signature) ;
212+
213+ // The domain must not leak into the serialized payload.
214+ let json = serde_json:: to_string ( & mainnet) . unwrap ( ) ;
215+ assert ! ( !json. contains( "mainnet" ) ) ;
216+ }
217+
167218 #[ test]
168219 fn test_deterministic_signature ( ) {
169- let mut signer = BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & test_oracle_account ( ) ) . unwrap ( ) ;
220+ let mut signer = test_signer ( ) ;
170221
171222 let oracles = vec ! [ PythOraclePrice {
172223 timestamp: 1000 ,
@@ -184,7 +235,7 @@ mod tests {
184235
185236 #[ test]
186237 fn test_different_nonce_different_signature ( ) {
187- let mut signer = BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & test_oracle_account ( ) ) . unwrap ( ) ;
238+ let mut signer = test_signer ( ) ;
188239
189240 let oracles1 = vec ! [ PythOraclePrice {
190241 timestamp: 1000 ,
@@ -202,7 +253,7 @@ mod tests {
202253
203254 #[ test]
204255 fn test_transaction_json_format ( ) {
205- let mut signer = BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & test_oracle_account ( ) ) . unwrap ( ) ;
256+ let mut signer = test_signer ( ) ;
206257
207258 let oracles = vec ! [
208259 PythOraclePrice {
@@ -249,7 +300,7 @@ mod tests {
249300 use bulk_keychain:: ed25519_dalek:: { Signature , Verifier , VerifyingKey } ;
250301
251302 let keypair = Keypair :: from_base58 ( TEST_PRIVATE_KEY_BASE58 ) . unwrap ( ) ;
252- let mut signer = BulkSigner :: new ( TEST_PRIVATE_KEY_BASE58 , & test_oracle_account ( ) ) . unwrap ( ) ;
303+ let mut signer = test_signer ( ) ;
253304
254305 let oracles = vec ! [ PythOraclePrice {
255306 timestamp: 1704067200000 ,
0 commit comments