Merge pull request #47 from floor-licker/fix/hmac-body-benchmark-and-credentials

fix: prepare hmac credentials for auth paths
This commit is contained in:
floor-licker
2026-06-22 17:10:47 -04:00
committed by GitHub
3 changed files with 117 additions and 42 deletions
+10 -3
View File
@@ -1,7 +1,7 @@
use alloy_signer_local::PrivateKeySigner; use alloy_signer_local::PrivateKeySigner;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
use polyfill_rs::{ use polyfill_rs::{
auth::create_l2_headers, auth::{create_l2_headers_with_body_bytes, PreparedApiCredentials},
orders::{OrderBuilder, BYTES32_ZERO}, orders::{OrderBuilder, BYTES32_ZERO},
types::{ types::{
ApiCredentials, CreateOrderOptions, FastOrderDelta, OrderDelta, OrderType, PostOrder, ApiCredentials, CreateOrderOptions, FastOrderDelta, OrderDelta, OrderType, PostOrder,
@@ -108,6 +108,7 @@ fn benchmark_order_submit_payload_auth(c: &mut Criterion) {
secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(), secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
passphrase: "benchmark-passphrase".to_string(), passphrase: "benchmark-passphrase".to_string(),
}; };
let prepared_api_creds = PreparedApiCredentials::new(api_creds.clone());
c.bench_function("order_submit_body_and_l2_headers", |b| { c.bench_function("order_submit_body_and_l2_headers", |b| {
b.iter(|| { b.iter(|| {
@@ -117,8 +118,14 @@ fn benchmark_order_submit_payload_auth(c: &mut Criterion) {
black_box(post_options), black_box(post_options),
); );
let body_bytes = serde_json::to_vec(black_box(&body)).unwrap(); let body_bytes = serde_json::to_vec(black_box(&body)).unwrap();
let headers = let headers = create_l2_headers_with_body_bytes(
create_l2_headers(&signer, &api_creds, "POST", "/order", Some(&body)).unwrap(); &signer,
&prepared_api_creds,
"POST",
"/order",
Some(&body_bytes),
)
.unwrap();
black_box((body_bytes, headers)) black_box((body_bytes, headers))
}) })
}); });
+99 -35
View File
@@ -13,8 +13,10 @@ use base64::engine::Engine;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use serde::Serialize; use serde::Serialize;
use sha2::Sha256; use sha2::Sha256;
use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, LazyLock, RwLock}; use std::ops::Deref;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
// Header constants // Header constants
@@ -26,8 +28,75 @@ const POLY_API_KEY_HEADER: &str = "poly_api_key";
const POLY_PASS_HEADER: &str = "poly_passphrase"; const POLY_PASS_HEADER: &str = "poly_passphrase";
type Headers = HashMap<&'static str, String>; type Headers = HashMap<&'static str, String>;
static DECODED_SECRET_CACHE: LazyLock<RwLock<HashMap<String, Arc<[u8]>>>> =
LazyLock::new(|| RwLock::new(HashMap::new())); pub trait HmacApiCredentials {
fn api_key(&self) -> &str;
fn passphrase(&self) -> &str;
fn decoded_secret_bytes(&self) -> Result<Cow<'_, [u8]>>;
}
#[derive(Debug, Clone)]
pub struct PreparedApiCredentials {
credentials: ApiCredentials,
decoded_secret: std::result::Result<Arc<[u8]>, String>,
}
impl PreparedApiCredentials {
pub fn new(credentials: ApiCredentials) -> Self {
let decoded_secret = base64::engine::general_purpose::URL_SAFE
.decode(&credentials.secret)
.map(Into::into)
.map_err(|e| format!("Failed to decode base64 secret: {}", e));
Self {
credentials,
decoded_secret,
}
}
pub fn credentials(&self) -> &ApiCredentials {
&self.credentials
}
}
impl Deref for PreparedApiCredentials {
type Target = ApiCredentials;
fn deref(&self) -> &Self::Target {
&self.credentials
}
}
impl HmacApiCredentials for ApiCredentials {
fn api_key(&self) -> &str {
&self.api_key
}
fn passphrase(&self) -> &str {
&self.passphrase
}
fn decoded_secret_bytes(&self) -> Result<Cow<'_, [u8]>> {
Ok(Cow::Owned(decode_secret_bytes(&self.secret)?))
}
}
impl HmacApiCredentials for PreparedApiCredentials {
fn api_key(&self) -> &str {
&self.credentials.api_key
}
fn passphrase(&self) -> &str {
&self.credentials.passphrase
}
fn decoded_secret_bytes(&self) -> Result<Cow<'_, [u8]>> {
match &self.decoded_secret {
Ok(decoded_secret) => Ok(Cow::Borrowed(decoded_secret.as_ref())),
Err(err) => Err(PolyfillError::crypto(err.clone())),
}
}
}
// EIP-712 struct for CLOB authentication // EIP-712 struct for CLOB authentication
sol! { sol! {
@@ -159,7 +228,7 @@ pub fn build_hmac_signature<T>(
where where
T: ?Sized + Serialize, T: ?Sized + Serialize,
{ {
let decoded_secret = decoded_secret_bytes(secret)?; let decoded_secret = decode_secret_bytes(secret)?;
let body_bytes = let body_bytes =
match body { match body {
Some(b) => Some(serde_json::to_vec(b).map_err(|e| { Some(b) => Some(serde_json::to_vec(b).map_err(|e| {
@@ -207,28 +276,10 @@ pub fn build_hmac_signature_bytes(
Ok(base64::engine::general_purpose::URL_SAFE.encode(result.into_bytes())) Ok(base64::engine::general_purpose::URL_SAFE.encode(result.into_bytes()))
} }
fn decoded_secret_bytes(secret: &str) -> Result<Arc<[u8]>> { fn decode_secret_bytes(secret: &str) -> Result<Vec<u8>> {
if let Some(decoded) = DECODED_SECRET_CACHE base64::engine::general_purpose::URL_SAFE
.read()
.map_err(|_| PolyfillError::internal_simple("Decoded secret cache lock poisoned"))?
.get(secret)
.cloned()
{
return Ok(decoded);
}
let decoded: Arc<[u8]> = base64::engine::general_purpose::URL_SAFE
.decode(secret) .decode(secret)
.map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {}", e)))? .map_err(|e| PolyfillError::crypto(format!("Failed to decode base64 secret: {}", e)))
.into();
let mut cache = DECODED_SECRET_CACHE
.write()
.map_err(|_| PolyfillError::internal_simple("Decoded secret cache lock poisoned"))?;
Ok(cache
.entry(secret.to_string())
.or_insert_with(|| decoded.clone())
.clone())
} }
/// Create L1 headers for authentication (using private key signature) /// Create L1 headers for authentication (using private key signature)
@@ -259,7 +310,7 @@ pub fn create_l1_headers(signer: &PrivateKeySigner, nonce: Option<U256>) -> Resu
/// to satisfy bilateral verification requirements at the protocol layer. /// to satisfy bilateral verification requirements at the protocol layer.
pub fn create_l2_headers<T>( pub fn create_l2_headers<T>(
signer: &PrivateKeySigner, signer: &PrivateKeySigner,
api_creds: &ApiCredentials, api_creds: &(impl HmacApiCredentials + ?Sized),
method: &str, method: &str,
req_path: &str, req_path: &str,
body: Option<&T>, body: Option<&T>,
@@ -272,29 +323,42 @@ where
let timestamp = get_current_unix_time_secs(); let timestamp = get_current_unix_time_secs();
// Generate cryptographic authenticator using temporal and message context // Generate cryptographic authenticator using temporal and message context
let hmac_signature = let decoded_secret = api_creds.decoded_secret_bytes()?;
build_hmac_signature(&api_creds.secret, timestamp, method, req_path, body)?; let body_bytes =
match body {
Some(b) => Some(serde_json::to_vec(b).map_err(|e| {
PolyfillError::parse(format!("Failed to serialize body: {}", e), None)
})?),
None => None,
};
let hmac_signature = build_hmac_signature_bytes(
&decoded_secret,
timestamp,
method,
req_path,
body_bytes.as_deref(),
)?;
// Construct header map with authentication primitives in canonical order // Construct header map with authentication primitives in canonical order
Ok(HashMap::from([ Ok(HashMap::from([
(POLY_ADDR_HEADER, address), (POLY_ADDR_HEADER, address),
(POLY_SIG_HEADER, hmac_signature), (POLY_SIG_HEADER, hmac_signature),
(POLY_TS_HEADER, timestamp.to_string()), (POLY_TS_HEADER, timestamp.to_string()),
(POLY_API_KEY_HEADER, api_creds.api_key.clone()), (POLY_API_KEY_HEADER, api_creds.api_key().to_string()),
(POLY_PASS_HEADER, api_creds.passphrase.clone()), (POLY_PASS_HEADER, api_creds.passphrase().to_string()),
])) ]))
} }
pub fn create_l2_headers_with_body_bytes( pub fn create_l2_headers_with_body_bytes(
signer: &PrivateKeySigner, signer: &PrivateKeySigner,
api_creds: &ApiCredentials, api_creds: &(impl HmacApiCredentials + ?Sized),
method: &str, method: &str,
req_path: &str, req_path: &str,
body_bytes: Option<&[u8]>, body_bytes: Option<&[u8]>,
) -> Result<Headers> { ) -> Result<Headers> {
let address = encode_prefixed(signer.address().as_slice()); let address = encode_prefixed(signer.address().as_slice());
let timestamp = get_current_unix_time_secs(); let timestamp = get_current_unix_time_secs();
let decoded_secret = decoded_secret_bytes(&api_creds.secret)?; let decoded_secret = api_creds.decoded_secret_bytes()?;
let hmac_signature = let hmac_signature =
build_hmac_signature_bytes(&decoded_secret, timestamp, method, req_path, body_bytes)?; build_hmac_signature_bytes(&decoded_secret, timestamp, method, req_path, body_bytes)?;
@@ -302,8 +366,8 @@ pub fn create_l2_headers_with_body_bytes(
(POLY_ADDR_HEADER, address), (POLY_ADDR_HEADER, address),
(POLY_SIG_HEADER, hmac_signature), (POLY_SIG_HEADER, hmac_signature),
(POLY_TS_HEADER, timestamp.to_string()), (POLY_TS_HEADER, timestamp.to_string()),
(POLY_API_KEY_HEADER, api_creds.api_key.clone()), (POLY_API_KEY_HEADER, api_creds.api_key().to_string()),
(POLY_PASS_HEADER, api_creds.passphrase.clone()), (POLY_PASS_HEADER, api_creds.passphrase().to_string()),
])) ]))
} }
@@ -364,7 +428,7 @@ mod tests {
let timestamp = 1234567890; let timestamp = 1234567890;
let body = serde_json::json!({"orderID": "abc123"}); let body = serde_json::json!({"orderID": "abc123"});
let body_bytes = serde_json::to_vec(&body).unwrap(); let body_bytes = serde_json::to_vec(&body).unwrap();
let decoded_secret = decoded_secret_bytes(secret).unwrap(); let decoded_secret = decode_secret_bytes(secret).unwrap();
let object_signature = let object_signature =
build_hmac_signature(secret, timestamp, "delete", "/order", Some(&body)).unwrap(); build_hmac_signature(secret, timestamp, "delete", "/order", Some(&body)).unwrap();
+8 -4
View File
@@ -3,7 +3,9 @@
//! This module provides a production-ready client for interacting with //! This module provides a production-ready client for interacting with
//! Polymarket, optimized for high-frequency trading environments. //! Polymarket, optimized for high-frequency trading environments.
use crate::auth::{create_l1_headers, create_l2_headers, create_l2_headers_with_body_bytes}; use crate::auth::{
create_l1_headers, create_l2_headers, create_l2_headers_with_body_bytes, PreparedApiCredentials,
};
use crate::errors::{PolyfillError, Result}; use crate::errors::{PolyfillError, Result};
use crate::http_config::{create_colocated_client, create_internet_client, prewarm_connections}; use crate::http_config::{create_colocated_client, create_internet_client, prewarm_connections};
use crate::types::{ use crate::types::{
@@ -94,7 +96,7 @@ pub struct ClobClient {
pub base_url: String, pub base_url: String,
chain_id: u64, chain_id: u64,
signer: Option<PrivateKeySigner>, signer: Option<PrivateKeySigner>,
api_creds: Option<ApiCreds>, api_creds: Option<PreparedApiCredentials>,
builder_code: Option<String>, builder_code: Option<String>,
order_builder: Option<crate::orders::OrderBuilder>, order_builder: Option<crate::orders::OrderBuilder>,
#[allow(dead_code)] #[allow(dead_code)]
@@ -134,12 +136,14 @@ impl ClobClient {
.clone() .clone()
.map(|signer| crate::orders::OrderBuilder::new(signer, auth.sig_type, auth.funder)); .map(|signer| crate::orders::OrderBuilder::new(signer, auth.sig_type, auth.funder));
let api_creds = auth.api_creds.map(PreparedApiCredentials::new);
Self { Self {
http_client, http_client,
base_url: host.to_string(), base_url: host.to_string(),
chain_id, chain_id,
signer: auth.signer, signer: auth.signer,
api_creds: auth.api_creds, api_creds,
builder_code: auth.builder_code, builder_code: auth.builder_code,
order_builder, order_builder,
dns_cache: None, dns_cache: None,
@@ -257,7 +261,7 @@ impl ClobClient {
/// Set API credentials /// Set API credentials
pub fn set_api_creds(&mut self, api_creds: ApiCreds) { pub fn set_api_creds(&mut self, api_creds: ApiCreds) {
self.api_creds = Some(api_creds); self.api_creds = Some(PreparedApiCredentials::new(api_creds));
} }
/// Start background keep-alive to maintain warm connection /// Start background keep-alive to maintain warm connection