Skip to content

Commit 1fe3aad

Browse files
committed
Support external Sui networks in hashi-localnet CLI
Add --sui-network flag to connect to external Sui networks (devnet, testnet) instead of spawning a local one. Mirrors the existing external Bitcoin node pattern with SuiNetworkInfo trait + ExternalSuiNetwork. For external networks, the Move source is patched at publish time to allow non-validator registration and committee formation with equal weight, since test keys are not actual Sui validators.
1 parent 6a287a8 commit 1fe3aad

File tree

8 files changed

+715
-93
lines changed

8 files changed

+715
-93
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! A lightweight Sui network handle that connects to an already-running node
5+
//! (e.g. devnet, testnet). Unlike [`SuiNetworkHandle`], this does NOT
6+
//! spawn or manage the Sui process.
7+
8+
use anyhow::Context;
9+
use anyhow::Result;
10+
use std::collections::BTreeMap;
11+
use std::path::Path;
12+
use sui_crypto::SuiSigner;
13+
use sui_crypto::ed25519::Ed25519PrivateKey;
14+
use sui_rpc::Client;
15+
use sui_rpc::field::FieldMask;
16+
use sui_rpc::field::FieldMaskUtil;
17+
use sui_rpc::proto::sui::rpc::v2::ExecuteTransactionRequest;
18+
use sui_sdk_types::Address;
19+
use sui_sdk_types::Argument;
20+
use sui_sdk_types::GasPayment;
21+
use sui_sdk_types::Input;
22+
use sui_sdk_types::ProgrammableTransaction;
23+
use sui_sdk_types::StructTag;
24+
use sui_sdk_types::Transaction;
25+
use sui_sdk_types::TransactionExpiration;
26+
use sui_sdk_types::TransactionKind;
27+
use sui_sdk_types::TransferObjects;
28+
use sui_sdk_types::bcs::ToBcs;
29+
use tracing::info;
30+
31+
use crate::hashi_network::SuiNetworkInfo;
32+
use crate::sui_network::keypair_from_base64;
33+
34+
pub struct ExternalSuiNetwork {
35+
rpc_url: String,
36+
client: Client,
37+
operator_key: Ed25519PrivateKey,
38+
validator_keys: BTreeMap<Address, Ed25519PrivateKey>,
39+
}
40+
41+
impl ExternalSuiNetwork {
42+
/// Connect to an already-running Sui network, generate validator keys, and fund them.
43+
///
44+
/// # Arguments
45+
/// - `rpc_url`: Sui RPC URL (e.g. `https://fullnode.devnet.sui.io:443`)
46+
/// - `operator_key`: Ed25519 key with sufficient SUI balance for funding validators
47+
/// - `num_validators`: Number of validator keypairs to generate and fund
48+
pub async fn new(
49+
rpc_url: &str,
50+
operator_key: Ed25519PrivateKey,
51+
num_validators: usize,
52+
) -> Result<Self> {
53+
let mut client = Client::new(rpc_url)?;
54+
55+
crate::sui_network::wait_for_ready(&mut client)
56+
.await
57+
.with_context(|| {
58+
format!(
59+
"Failed to connect to Sui network at {}. Ensure the node is reachable.",
60+
rpc_url
61+
)
62+
})?;
63+
64+
let operator_addr = operator_key.public_key().derive_address();
65+
info!(
66+
"Connected to external Sui network at {}, operator: {}",
67+
rpc_url, operator_addr
68+
);
69+
70+
// Generate fresh Ed25519 keypairs for hashi validator identities
71+
let mut validator_keys = BTreeMap::new();
72+
for _ in 0..num_validators {
73+
let seed: [u8; 32] = rand::random();
74+
let key = Ed25519PrivateKey::new(seed);
75+
let addr = key.public_key().derive_address();
76+
validator_keys.insert(addr, key);
77+
}
78+
79+
let mut network = Self {
80+
rpc_url: rpc_url.to_string(),
81+
client,
82+
operator_key,
83+
validator_keys,
84+
};
85+
86+
// Fund validator accounts from operator.
87+
// External networks have limited funds, so use 5 SUI per validator
88+
// (enough for registration + gas during operations).
89+
let fund_requests: Vec<(Address, u64)> = network
90+
.validator_keys
91+
.keys()
92+
.map(|addr| (*addr, 5 * 1_000_000_000))
93+
.collect();
94+
network.fund(&fund_requests).await?;
95+
96+
Ok(network)
97+
}
98+
99+
/// Load an Ed25519 private key from a Sui keystore file by address.
100+
///
101+
/// The keystore file is a JSON array of base64-encoded keys, each prefixed
102+
/// with a scheme byte (as written by `sui keytool import`).
103+
pub fn load_key_from_keystore(
104+
keystore_path: &Path,
105+
target_address: &Address,
106+
) -> Result<Ed25519PrivateKey> {
107+
let contents = std::fs::read_to_string(keystore_path)
108+
.with_context(|| format!("Failed to read keystore at {}", keystore_path.display()))?;
109+
let keys: Vec<String> = serde_json::from_str(&contents)
110+
.with_context(|| format!("Failed to parse keystore at {}", keystore_path.display()))?;
111+
112+
for b64_key in &keys {
113+
if let Ok(key) = keypair_from_base64(b64_key) {
114+
let addr = key.public_key().derive_address();
115+
if addr == *target_address {
116+
return Ok(key);
117+
}
118+
}
119+
}
120+
121+
anyhow::bail!(
122+
"No Ed25519 key found for address {} in keystore at {}",
123+
target_address,
124+
keystore_path.display()
125+
)
126+
}
127+
128+
/// Fund Sui addresses from the operator account.
129+
pub async fn fund(&mut self, requests: &[(Address, u64)]) -> Result<()> {
130+
let sender = self.operator_key.public_key().derive_address();
131+
let price = self.client.get_reference_gas_price().await?;
132+
133+
let gas_objects = self
134+
.client
135+
.select_coins(
136+
&sender,
137+
&StructTag::sui().into(),
138+
requests.iter().map(|r| r.1).sum(),
139+
&[],
140+
)
141+
.await?;
142+
143+
let (inputs, transfers): (Vec<Input>, Vec<sui_sdk_types::Command>) = requests
144+
.iter()
145+
.enumerate()
146+
.map(|(i, request)| {
147+
(
148+
Input::Pure(request.0.to_bcs().unwrap()),
149+
sui_sdk_types::Command::TransferObjects(TransferObjects {
150+
objects: vec![Argument::NestedResult(0, i as u16)],
151+
address: Argument::Input(i as u16),
152+
}),
153+
)
154+
})
155+
.unzip();
156+
157+
let (input_amounts, argument_amounts) = requests
158+
.iter()
159+
.enumerate()
160+
.map(|(i, request)| {
161+
(
162+
Input::Pure(request.1.to_bcs().unwrap()),
163+
Argument::Input((i + inputs.len()) as u16),
164+
)
165+
})
166+
.unzip();
167+
168+
let pt = ProgrammableTransaction {
169+
inputs: [inputs, input_amounts].concat(),
170+
commands: [
171+
vec![sui_sdk_types::Command::SplitCoins(
172+
sui_sdk_types::SplitCoins {
173+
coin: Argument::Gas,
174+
amounts: argument_amounts,
175+
},
176+
)],
177+
transfers,
178+
]
179+
.concat(),
180+
};
181+
182+
let gas_payment_objects = gas_objects
183+
.iter()
184+
.map(|o| -> anyhow::Result<_> { Ok((&o.object_reference()).try_into()?) })
185+
.collect::<Result<Vec<_>>>()?;
186+
187+
let transaction = Transaction {
188+
kind: TransactionKind::ProgrammableTransaction(pt),
189+
sender,
190+
gas_payment: GasPayment {
191+
objects: gas_payment_objects,
192+
owner: sender,
193+
price,
194+
budget: 1_000_000_000,
195+
},
196+
expiration: TransactionExpiration::None,
197+
};
198+
199+
let signature = self.operator_key.sign_transaction(&transaction)?;
200+
201+
let response = self
202+
.client
203+
.execute_transaction_and_wait_for_checkpoint(
204+
ExecuteTransactionRequest::new(transaction.into())
205+
.with_signatures(vec![signature.into()])
206+
.with_read_mask(FieldMask::from_str("*")),
207+
std::time::Duration::from_secs(10),
208+
)
209+
.await?
210+
.into_inner();
211+
212+
anyhow::ensure!(
213+
response.transaction().effects().status().success(),
214+
"fund failed"
215+
);
216+
217+
info!("Funded {} validator accounts from operator", requests.len());
218+
Ok(())
219+
}
220+
221+
/// Write a minimal `client.yaml` + keystore so `sui move build` can resolve
222+
/// framework dependencies. Call this before publishing.
223+
pub fn write_sui_config(&self, sui_dir: &Path) -> Result<()> {
224+
std::fs::create_dir_all(sui_dir)?;
225+
226+
// Write a minimal keystore (empty array is valid)
227+
let keystore_path = sui_dir.join("sui.keystore");
228+
std::fs::write(&keystore_path, "[]")?;
229+
230+
// Write client.yaml pointing to our RPC
231+
let client_yaml = format!(
232+
"---\nkeystore:\n File: {keystore}\nenvs:\n - alias: external\n rpc: \"{rpc}\"\n ws: ~\nactive_env: external\nactive_address: \"{addr}\"\n",
233+
keystore = keystore_path.display(),
234+
rpc = self.rpc_url,
235+
addr = self.operator_key.public_key().derive_address(),
236+
);
237+
std::fs::write(sui_dir.join("client.yaml"), client_yaml)?;
238+
239+
Ok(())
240+
}
241+
}
242+
243+
impl SuiNetworkInfo for ExternalSuiNetwork {
244+
fn rpc_url(&self) -> &str {
245+
&self.rpc_url
246+
}
247+
248+
fn client(&self) -> Client {
249+
self.client.clone()
250+
}
251+
252+
fn validator_keys(&self) -> &BTreeMap<Address, Ed25519PrivateKey> {
253+
&self.validator_keys
254+
}
255+
256+
fn funding_key(&self) -> &Ed25519PrivateKey {
257+
&self.operator_key
258+
}
259+
}

0 commit comments

Comments
 (0)