Skip to content
This repository was archived by the owner on Jan 27, 2026. It is now read-only.

Commit c505468

Browse files
committed
move invite logic to prime-core crate
1 parent 5e327ac commit c505468

10 files changed

Lines changed: 385 additions & 55 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/orchestrator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ workspace = true
99
[dependencies]
1010
p2p = { workspace = true}
1111
shared = { workspace = true }
12+
prime-core = { workspace = true }
1213

1314
actix-web = { workspace = true }
1415
alloy = { workspace = true }

crates/orchestrator/src/node/invite.rs

Lines changed: 24 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ use crate::models::node::OrchestratorNode;
33
use crate::p2p::InviteRequest as InviteRequestWithMetadata;
44
use crate::store::core::StoreContext;
55
use crate::utils::loop_heartbeats::LoopHeartbeats;
6-
use alloy::primitives::utils::keccak256 as keccak;
7-
use alloy::primitives::U256;
8-
use alloy::signers::Signer;
96
use anyhow::{bail, Result};
107
use futures::stream;
118
use futures::StreamExt;
129
use log::{debug, error, info, warn};
13-
use p2p::InviteRequest;
1410
use p2p::InviteRequestUrl;
11+
use prime_core::invite::{
12+
admin::{generate_invite_expiration, generate_invite_nonce, generate_invite_signature},
13+
common::InviteBuilder,
14+
};
1515
use shared::web3::wallet::Wallet;
1616
use std::sync::Arc;
17-
use std::time::SystemTime;
18-
use std::time::UNIX_EPOCH;
1917
use tokio::sync::mpsc::Sender;
2018
use tokio::time::{interval, Duration};
2119

@@ -89,29 +87,15 @@ impl NodeInviter {
8987
nonce: [u8; 32],
9088
expiration: [u8; 32],
9189
) -> Result<[u8; 65]> {
92-
let domain_id: [u8; 32] = U256::from(self.domain_id).to_be_bytes();
93-
let pool_id: [u8; 32] = U256::from(self.pool_id).to_be_bytes();
94-
95-
let digest = keccak(
96-
[
97-
&domain_id,
98-
&pool_id,
99-
node.address.as_slice(),
100-
&nonce,
101-
&expiration,
102-
]
103-
.concat(),
104-
);
105-
106-
let signature = self
107-
.wallet
108-
.signer
109-
.sign_message(digest.as_slice())
110-
.await?
111-
.as_bytes()
112-
.to_owned();
113-
114-
Ok(signature)
90+
generate_invite_signature(
91+
&self.wallet,
92+
self.domain_id,
93+
self.pool_id,
94+
node.address,
95+
nonce,
96+
expiration,
97+
)
98+
.await
11599
}
116100

117101
async fn send_invite(&self, node: &OrchestratorNode) -> Result<(), anyhow::Error> {
@@ -122,29 +106,21 @@ impl NodeInviter {
122106
let p2p_addresses = node.worker_p2p_addresses.as_ref().unwrap();
123107

124108
// Generate random nonce and expiration
125-
let nonce: [u8; 32] = rand::random();
126-
let expiration: [u8; 32] = U256::from(
127-
SystemTime::now()
128-
.duration_since(UNIX_EPOCH)
129-
.map_err(|e| anyhow::anyhow!("System time error: {}", e))?
130-
.as_secs()
131-
+ 1000,
132-
)
133-
.to_be_bytes();
109+
let nonce = generate_invite_nonce();
110+
let expiration = generate_invite_expiration(Some(1000))?;
134111

135112
let invite_signature = self.generate_invite(node, nonce, expiration).await?;
136-
let payload = InviteRequest {
137-
invite: hex::encode(invite_signature),
138-
pool_id: self.pool_id,
139-
url: self.url.clone(),
140-
timestamp: SystemTime::now()
141-
.duration_since(UNIX_EPOCH)
142-
.map_err(|e| anyhow::anyhow!("System time error: {}", e))?
143-
.as_secs(),
144-
expiration,
145-
nonce,
113+
114+
// Build the invite request using the builder
115+
let builder = match &self.url {
116+
InviteRequestUrl::MasterUrl(url) => InviteBuilder::with_url(self.pool_id, url.clone()),
117+
InviteRequestUrl::MasterIpPort(ip, port) => {
118+
InviteBuilder::with_ip_port(self.pool_id, ip.clone(), *port)
119+
}
146120
};
147121

122+
let payload = builder.build(invite_signature, nonce, expiration)?;
123+
148124
info!("Sending invite to node: {p2p_id}");
149125

150126
let (response_tx, response_rx) = tokio::sync::oneshot::channel();

crates/prime-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ path = "src/lib.rs"
1212

1313
[dependencies]
1414
shared = { workspace = true }
15+
p2p = { workspace = true }
1516
alloy = { workspace = true }
1617
alloy-provider = { workspace = true }
1718
serde = { workspace = true }
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use alloy::primitives::utils::keccak256 as keccak;
2+
use alloy::primitives::{Address, U256};
3+
use alloy::signers::Signer;
4+
use anyhow::Result;
5+
use rand_v8::prelude::*;
6+
use shared::web3::wallet::Wallet;
7+
use std::time::{SystemTime, UNIX_EPOCH};
8+
9+
/// Generates an invite signature for a node
10+
///
11+
/// This function is used by pool owners/admins to create signed invites
12+
/// that authorize nodes to join their pool.
13+
pub async fn generate_invite_signature(
14+
wallet: &Wallet,
15+
domain_id: u32,
16+
pool_id: u32,
17+
node_address: Address,
18+
nonce: [u8; 32],
19+
expiration: [u8; 32],
20+
) -> Result<[u8; 65]> {
21+
let domain_id_bytes: [u8; 32] = U256::from(domain_id).to_be_bytes();
22+
let pool_id_bytes: [u8; 32] = U256::from(pool_id).to_be_bytes();
23+
24+
let digest = keccak(
25+
[
26+
&domain_id_bytes,
27+
&pool_id_bytes,
28+
node_address.as_slice(),
29+
&nonce,
30+
&expiration,
31+
]
32+
.concat(),
33+
);
34+
35+
let signature = wallet
36+
.signer
37+
.sign_message(digest.as_slice())
38+
.await?
39+
.as_bytes()
40+
.to_owned();
41+
42+
Ok(signature)
43+
}
44+
45+
/// Generates an invite expiration timestamp
46+
///
47+
/// Creates an expiration timestamp for an invite, defaulting to 1000 seconds from now
48+
pub fn generate_invite_expiration(seconds_from_now: Option<u64>) -> Result<[u8; 32]> {
49+
let duration = seconds_from_now.unwrap_or(1000);
50+
let expiration = U256::from(
51+
SystemTime::now()
52+
.duration_since(UNIX_EPOCH)
53+
.map_err(|e| anyhow::anyhow!("System time error: {}", e))?
54+
.as_secs()
55+
+ duration,
56+
);
57+
Ok(expiration.to_be_bytes())
58+
}
59+
60+
/// Generates a random nonce for invite
61+
pub fn generate_invite_nonce() -> [u8; 32] {
62+
rand_v8::rngs::OsRng.gen()
63+
}
64+
65+
#[cfg(test)]
66+
mod tests {
67+
use super::*;
68+
69+
#[test]
70+
fn test_generate_invite_nonce() {
71+
let nonce1 = generate_invite_nonce();
72+
let nonce2 = generate_invite_nonce();
73+
assert_ne!(nonce1, nonce2, "Nonces should be unique");
74+
}
75+
76+
#[test]
77+
fn test_generate_invite_expiration() {
78+
let expiration = generate_invite_expiration(Some(3600)).unwrap();
79+
assert_eq!(expiration.len(), 32);
80+
}
81+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use alloy::primitives::Address;
2+
use anyhow::Result;
3+
use p2p::{InviteRequest, InviteRequestUrl};
4+
use std::time::{SystemTime, UNIX_EPOCH};
5+
6+
/// Builder for creating invite requests
7+
pub struct InviteBuilder {
8+
pool_id: u32,
9+
url: InviteRequestUrl,
10+
}
11+
12+
impl InviteBuilder {
13+
/// Creates a new InviteBuilder with a master URL
14+
pub fn with_url(pool_id: u32, url: String) -> Self {
15+
Self {
16+
pool_id,
17+
url: InviteRequestUrl::MasterUrl(url),
18+
}
19+
}
20+
21+
/// Creates a new InviteBuilder with IP and port
22+
pub fn with_ip_port(pool_id: u32, ip: String, port: u16) -> Self {
23+
Self {
24+
pool_id,
25+
url: InviteRequestUrl::MasterIpPort(ip, port),
26+
}
27+
}
28+
29+
/// Builds an InviteRequest with the given parameters
30+
pub fn build(
31+
self,
32+
invite_signature: [u8; 65],
33+
nonce: [u8; 32],
34+
expiration: [u8; 32],
35+
) -> Result<InviteRequest> {
36+
Ok(InviteRequest {
37+
invite: hex::encode(invite_signature),
38+
pool_id: self.pool_id,
39+
url: self.url,
40+
timestamp: SystemTime::now()
41+
.duration_since(UNIX_EPOCH)
42+
.map_err(|e| anyhow::anyhow!("System time error: {}", e))?
43+
.as_secs(),
44+
expiration,
45+
nonce,
46+
})
47+
}
48+
}
49+
50+
/// Metadata for an invite request that includes worker information
51+
#[derive(Debug, Clone)]
52+
pub struct InviteMetadata {
53+
pub worker_wallet_address: Address,
54+
pub worker_p2p_id: String,
55+
pub worker_addresses: Vec<String>,
56+
}
57+
58+
/// Full invite request with metadata
59+
#[derive(Debug)]
60+
pub struct InviteWithMetadata {
61+
pub metadata: InviteMetadata,
62+
pub request: InviteRequest,
63+
}
64+
65+
/// Helper to parse InviteRequestUrl into a usable endpoint
66+
pub fn get_endpoint_from_url(url: &InviteRequestUrl, path: &str) -> String {
67+
match url {
68+
InviteRequestUrl::MasterIpPort(ip, port) => {
69+
format!("http://{ip}:{port}/{path}")
70+
}
71+
InviteRequestUrl::MasterUrl(url) => {
72+
let url = url.trim_end_matches('/');
73+
format!("{url}/{path}")
74+
}
75+
}
76+
}
77+
78+
#[cfg(test)]
79+
mod tests {
80+
use super::*;
81+
82+
#[test]
83+
fn test_invite_builder_with_url() {
84+
let builder = InviteBuilder::with_url(1, "https://example.com".to_string());
85+
let signature = [0u8; 65];
86+
let nonce = [1u8; 32];
87+
let expiration = [2u8; 32];
88+
89+
let invite = builder.build(signature, nonce, expiration).unwrap();
90+
assert_eq!(invite.pool_id, 1);
91+
assert!(matches!(invite.url, InviteRequestUrl::MasterUrl(_)));
92+
}
93+
94+
#[test]
95+
fn test_get_endpoint_from_url() {
96+
let url = InviteRequestUrl::MasterUrl("https://example.com".to_string());
97+
assert_eq!(
98+
get_endpoint_from_url(&url, "heartbeat"),
99+
"https://example.com/heartbeat"
100+
);
101+
102+
let ip_port = InviteRequestUrl::MasterIpPort("192.168.1.1".to_string(), 8080);
103+
assert_eq!(
104+
get_endpoint_from_url(&ip_port, "heartbeat"),
105+
"http://192.168.1.1:8080/heartbeat"
106+
);
107+
}
108+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pub mod admin;
2+
pub mod common;
3+
pub mod worker;
4+
5+
pub use admin::*;
6+
pub use common::*;
7+
pub use worker::*;

0 commit comments

Comments
 (0)