Skip to content

Commit 5ee10f2

Browse files
authored
Feat/signer trait (#11)
1 parent eeed565 commit 5ee10f2

File tree

3 files changed

+138
-80
lines changed

3 files changed

+138
-80
lines changed

src/api_client.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use {
2+
crate::signer::Signer,
23
reqwest::{Client, Url},
3-
secp256k1::{Message, Secp256k1, SecretKey},
44
serde::Serialize,
55
std::{sync::Arc, time::Duration},
66
wormhole_sdk::vaa::Body,
@@ -41,16 +41,11 @@ where
4141
}
4242

4343
impl<P: Serialize> Observation<P> {
44-
pub fn try_new(body: Body<P>, secret_key: SecretKey) -> Result<Self, anyhow::Error> {
44+
pub fn try_new(body: Body<P>, signer: impl Signer) -> Result<Self, anyhow::Error> {
4545
let digest = body.digest()?;
46-
let signature = Secp256k1::new()
47-
.sign_ecdsa_recoverable(&Message::from_digest(digest.secp256k_hash), &secret_key);
48-
let (recovery_id, signature_bytes) = signature.serialize_compact();
49-
let recovery_id: i32 = recovery_id.into();
50-
let mut signature = [0u8; 65];
51-
signature[..64].copy_from_slice(&signature_bytes);
52-
signature[64] = recovery_id as u8;
53-
46+
let signature = signer
47+
.sign(digest.secp256k_hash)
48+
.map_err(|e| anyhow::anyhow!("Failed to sign observation: {}", e))?;
5449
Ok(Self {
5550
version: 1,
5651
signature,
@@ -116,7 +111,7 @@ impl ApiClient {
116111
mod tests {
117112
use secp256k1::{
118113
ecdsa::{RecoverableSignature, RecoveryId},
119-
PublicKey,
114+
Message, PublicKey, Secp256k1, SecretKey,
120115
};
121116
use serde_json::Value;
122117
use serde_wormhole::RawMessage;
@@ -127,6 +122,7 @@ mod tests {
127122
#[test]
128123
fn test_new_signed_observation() {
129124
let secret_key = SecretKey::from_byte_array(&[1u8; 32]).expect("Invalid secret key length");
125+
let signer = crate::signer::FileSigner { secret_key };
130126
let body = Body {
131127
timestamp: 1234567890,
132128
nonce: 42,
@@ -137,7 +133,7 @@ mod tests {
137133
payload: vec![1, 2, 3, 4, 5],
138134
};
139135
let observation =
140-
Observation::try_new(body.clone(), secret_key).expect("Failed to create observation");
136+
Observation::try_new(body.clone(), signer).expect("Failed to create observation");
141137
assert_eq!(observation.version, 1);
142138
assert_eq!(observation.body, body);
143139

src/main.rs

Lines changed: 28 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use {
2-
crate::config::Command,
2+
crate::{
3+
config::Command,
4+
signer::{GuardianKey, Signer, GUARDIAN_KEY_ARMORED_BLOCK, STANDARD_ARMOR_LINE_HEADER},
5+
},
36
api_client::{ApiClient, Observation},
47
borsh::BorshDeserialize,
58
clap::Parser,
69
posted_message::PostedMessageUnreliableData,
710
prost::Message,
8-
secp256k1::{rand::rngs::OsRng, PublicKey, Secp256k1, SecretKey},
9-
sequoia_openpgp::armor::{Kind, Reader, ReaderMode, Writer},
11+
secp256k1::{rand::rngs::OsRng, Secp256k1},
12+
sequoia_openpgp::armor::{Kind, Writer},
1013
serde_wormhole::RawMessage,
11-
sha3::{Digest, Keccak256},
1214
solana_account_decoder::UiAccountEncoding,
1315
solana_client::{
1416
nonblocking::pubsub_client::PubsubClient,
@@ -20,7 +22,7 @@ use {
2022
solana_sdk::pubkey::Pubkey,
2123
std::{
2224
fs,
23-
io::{Cursor, IsTerminal, Read, Write},
25+
io::{IsTerminal, Write},
2426
str::FromStr,
2527
time::Duration,
2628
},
@@ -32,10 +34,11 @@ use {
3234
mod api_client;
3335
mod config;
3436
mod posted_message;
37+
mod signer;
3538

36-
struct RunListenerInput {
39+
struct RunListenerInput<T: Signer> {
3740
ws_url: String,
38-
secret_key: SecretKey,
41+
signer: T,
3942
wormhole_pid: Pubkey,
4043
accumulator_address: Pubkey,
4144
api_client: ApiClient,
@@ -112,7 +115,9 @@ fn message_data_to_body(unreliable_data: &PostedMessageUnreliableData) -> Body<&
112115
}
113116
}
114117

115-
async fn run_listener(input: RunListenerInput) -> Result<(), PubsubClientError> {
118+
async fn run_listener<T: Signer + 'static>(
119+
input: RunListenerInput<T>,
120+
) -> Result<(), PubsubClientError> {
116121
let client = PubsubClient::new(input.ws_url.as_str()).await?;
117122
let (mut stream, unsubscribe) = client
118123
.program_subscribe(
@@ -143,10 +148,10 @@ async fn run_listener(input: RunListenerInput) -> Result<(), PubsubClientError>
143148
};
144149

145150
tokio::spawn({
146-
let api_client = input.api_client.clone();
151+
let (api_client, signer) = (input.api_client.clone(), input.signer.clone());
147152
async move {
148153
let body = message_data_to_body(&unreliable_data);
149-
match Observation::try_new(body.clone(), input.secret_key) {
154+
match Observation::try_new(body.clone(), signer.clone()) {
150155
Ok(observation) => {
151156
if let Err(e) = api_client.post_observation(observation).await {
152157
tracing::error!(error = ?e, "Failed to post observation");
@@ -167,60 +172,8 @@ async fn run_listener(input: RunListenerInput) -> Result<(), PubsubClientError>
167172
))
168173
}
169174

170-
#[derive(Clone, PartialEq, Message)]
171-
pub struct GuardianKey {
172-
#[prost(bytes = "vec", tag = "1")]
173-
pub data: Vec<u8>,
174-
#[prost(bool, tag = "2")]
175-
pub unsafe_deterministic_key: bool,
176-
}
177-
178-
const GUARDIAN_KEY_ARMORED_BLOCK: &str = "WORMHOLE GUARDIAN PRIVATE KEY";
179-
const STANDARD_ARMOR_LINE_HEADER: &str = "PGP PRIVATE KEY BLOCK";
180-
181-
fn parse_and_verify_proto_guardian_key(content: String, mode: crate::config::Mode) -> GuardianKey {
182-
let content = content.replace(GUARDIAN_KEY_ARMORED_BLOCK, STANDARD_ARMOR_LINE_HEADER);
183-
let cursor = Cursor::new(content);
184-
let mut armor_reader = Reader::from_reader(cursor, ReaderMode::Tolerant(Some(Kind::SecretKey)));
185-
186-
let mut buf = Vec::new();
187-
armor_reader
188-
.read_to_end(&mut buf)
189-
.expect("Failed to read armored content");
190-
191-
let guardian_key =
192-
GuardianKey::decode(&mut buf.as_slice()).expect("Failed to decode GuardianKey");
193-
194-
if let crate::config::Mode::Production = mode {
195-
if guardian_key.unsafe_deterministic_key {
196-
panic!("Unsafe deterministic key is not allowed in production mode");
197-
}
198-
}
199-
200-
guardian_key
201-
}
202-
203-
fn load_secret_key(run_options: config::RunOptions) -> SecretKey {
204-
let content = fs::read_to_string(run_options.secret_key_path).expect("Failed to read file");
205-
let guardian_key = parse_and_verify_proto_guardian_key(content, run_options.mode);
206-
SecretKey::from_slice(&guardian_key.data).expect("Failed to create SecretKey from bytes")
207-
}
208-
209-
fn get_public_key(secret_key: &SecretKey) -> (PublicKey, [u8; 20]) {
210-
let secp = Secp256k1::new();
211-
let public_key = secret_key.public_key(&secp);
212-
let pubkey_uncompressed = public_key.serialize_uncompressed();
213-
let pubkey_hash: [u8; 32] = Keccak256::new_with_prefix(&pubkey_uncompressed[1..])
214-
.finalize()
215-
.into();
216-
let pubkey_evm: [u8; 20] = pubkey_hash[pubkey_hash.len() - 20..]
217-
.try_into()
218-
.expect("Invalid address length");
219-
(public_key, pubkey_evm)
220-
}
221-
222175
async fn run(run_options: config::RunOptions) {
223-
let secret_key = load_secret_key(run_options.clone());
176+
let signer = signer::FileSigner::try_new(run_options.clone()).expect("Failed to create signer");
224177
let client = PubsubClient::new(&run_options.pythnet_url)
225178
.await
226179
.expect("Invalid WebSocket URL");
@@ -232,7 +185,7 @@ async fn run(run_options: config::RunOptions) {
232185
let api_client =
233186
ApiClient::try_new(run_options.server_url, None).expect("Failed to create API client");
234187

235-
let (pubkey, pubkey_evm) = get_public_key(&secret_key);
188+
let (pubkey, pubkey_evm) = signer.get_public_key().expect("Failed to get public key");
236189
let evm_encded_public_key = format!("0x{}", hex::encode(pubkey_evm));
237190
tracing::info!(
238191
public_key = ?pubkey,
@@ -243,7 +196,7 @@ async fn run(run_options: config::RunOptions) {
243196
loop {
244197
if let Err(e) = run_listener(RunListenerInput {
245198
ws_url: run_options.pythnet_url.clone(),
246-
secret_key,
199+
signer: signer.clone(),
247200
wormhole_pid,
248201
accumulator_address,
249202
api_client: api_client.clone(),
@@ -285,7 +238,8 @@ async fn main() {
285238

286239
// Generate keypair (secret + public key)
287240
let (secret_key, _) = secp.generate_keypair(&mut rng);
288-
let (pubkey, pubkey_evm) = get_public_key(&secret_key);
241+
let signer = signer::FileSigner { secret_key };
242+
let (pubkey, pubkey_evm) = signer.get_public_key().expect("Failed to get public key");
289243

290244
let guardian_key = GuardianKey {
291245
data: secret_key.secret_bytes().to_vec(),
@@ -322,6 +276,7 @@ mod tests {
322276

323277
use base64::Engine;
324278
use borsh::BorshSerialize;
279+
use secp256k1::SecretKey;
325280
use solana_account_decoder::{UiAccount, UiAccountData};
326281

327282
use crate::posted_message::MessageData;
@@ -516,16 +471,21 @@ mod tests {
516471
-----END WORMHOLE GUARDIAN PRIVATE KEY-----
517472
"
518473
.to_string();
519-
let guardian_key = parse_and_verify_proto_guardian_key(content, config::Mode::Production);
474+
let guardian_key = crate::signer::FileSigner::parse_and_verify_proto_guardian_key(
475+
content,
476+
config::Mode::Production,
477+
)
478+
.expect("Failed to parse and verify guardian key");
520479
assert!(!guardian_key.unsafe_deterministic_key);
521480
let secret_key = SecretKey::from_slice(&guardian_key.data)
522481
.expect("Failed to create SecretKey from bytes");
482+
let signer = signer::FileSigner { secret_key };
523483
assert_eq!(
524484
hex::encode(secret_key.secret_bytes()),
525485
"f2f3127bff540c8441f99763f586858ef340c9962ad62b6181cd77203e81808f",
526486
);
527487
assert_eq!(
528-
hex::encode(get_public_key(&secret_key).1),
488+
hex::encode(signer.get_public_key().expect("Failed to get public key").1),
529489
"30e41be3f10d3ac813f91e49e189bbb948d030be",
530490
);
531491
}

src/signer.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use std::{
2+
fs,
3+
io::{Cursor, Read},
4+
};
5+
6+
use prost::Message as ProstMessage;
7+
use secp256k1::{Message, PublicKey, Secp256k1, SecretKey};
8+
use sequoia_openpgp::armor::{Kind, Reader, ReaderMode};
9+
use sha3::{Digest, Keccak256};
10+
11+
use crate::config::RunOptions;
12+
13+
pub trait Signer: Send + Sync + Sized + Clone {
14+
fn try_new(run_options: RunOptions) -> anyhow::Result<Self>;
15+
fn sign(&self, data: [u8; 32]) -> anyhow::Result<[u8; 65]>;
16+
fn get_public_key(&self) -> anyhow::Result<(PublicKey, [u8; 20])>;
17+
}
18+
19+
#[derive(Clone, Debug)]
20+
pub struct FileSigner {
21+
pub secret_key: SecretKey,
22+
}
23+
24+
#[derive(Clone, PartialEq, ProstMessage)]
25+
pub struct GuardianKey {
26+
#[prost(bytes = "vec", tag = "1")]
27+
pub data: Vec<u8>,
28+
#[prost(bool, tag = "2")]
29+
pub unsafe_deterministic_key: bool,
30+
}
31+
32+
pub const GUARDIAN_KEY_ARMORED_BLOCK: &str = "WORMHOLE GUARDIAN PRIVATE KEY";
33+
pub const STANDARD_ARMOR_LINE_HEADER: &str = "PGP PRIVATE KEY BLOCK";
34+
35+
impl FileSigner {
36+
pub fn parse_and_verify_proto_guardian_key(
37+
content: String,
38+
mode: crate::config::Mode,
39+
) -> anyhow::Result<GuardianKey> {
40+
let content = content.replace(GUARDIAN_KEY_ARMORED_BLOCK, STANDARD_ARMOR_LINE_HEADER);
41+
let cursor = Cursor::new(content);
42+
let mut armor_reader =
43+
Reader::from_reader(cursor, ReaderMode::Tolerant(Some(Kind::SecretKey)));
44+
45+
let mut buf = Vec::new();
46+
armor_reader
47+
.read_to_end(&mut buf)
48+
.map_err(|e| anyhow::anyhow!("Failed to read armored key: {}", e))?;
49+
50+
let guardian_key = GuardianKey::decode(&mut buf.as_slice())
51+
.map_err(|e| anyhow::anyhow!("Failed to decode GuardianKey: {}", e))?;
52+
53+
if let crate::config::Mode::Production = mode {
54+
if guardian_key.unsafe_deterministic_key {
55+
return Err(anyhow::anyhow!(
56+
"Unsafe deterministic key is not allowed in production mode"
57+
));
58+
}
59+
}
60+
61+
Ok(guardian_key)
62+
}
63+
}
64+
65+
impl Signer for FileSigner {
66+
fn try_new(run_options: RunOptions) -> anyhow::Result<Self> {
67+
let content = fs::read_to_string(run_options.secret_key_path)
68+
.map_err(|e| anyhow::anyhow!("Failed to read secret key file: {}", e))?;
69+
let guardian_key = Self::parse_and_verify_proto_guardian_key(content, run_options.mode)?;
70+
Ok(FileSigner {
71+
secret_key: SecretKey::from_slice(&guardian_key.data)
72+
.map_err(|e| anyhow::anyhow!("Failed to create SecretKey: {}", e))?,
73+
})
74+
}
75+
76+
fn sign(&self, data: [u8; 32]) -> anyhow::Result<[u8; 65]> {
77+
let signature =
78+
Secp256k1::new().sign_ecdsa_recoverable(&Message::from_digest(data), &self.secret_key);
79+
let (recovery_id, signature_bytes) = signature.serialize_compact();
80+
let recovery_id: i32 = recovery_id.into();
81+
let mut signature = [0u8; 65];
82+
signature[..64].copy_from_slice(&signature_bytes);
83+
signature[64] = recovery_id as u8;
84+
Ok(signature)
85+
}
86+
87+
fn get_public_key(&self) -> anyhow::Result<(PublicKey, [u8; 20])> {
88+
let secp = Secp256k1::new();
89+
let public_key = self.secret_key.public_key(&secp);
90+
let pubkey_uncompressed = public_key.serialize_uncompressed();
91+
let pubkey_hash: [u8; 32] = Keccak256::new_with_prefix(&pubkey_uncompressed[1..])
92+
.finalize()
93+
.into();
94+
let pubkey_evm: [u8; 20] =
95+
pubkey_hash[pubkey_hash.len() - 20..]
96+
.try_into()
97+
.map_err(|e| {
98+
anyhow::anyhow!("Failed to convert public key hash to EVM format: {}", e)
99+
})?;
100+
Ok((public_key, pubkey_evm))
101+
}
102+
}

0 commit comments

Comments
 (0)