Skip to content

Commit ea57966

Browse files
feat: implement changing node account ids
Signed-off-by: venilinvasilev <[email protected]>
1 parent f9e9189 commit ea57966

File tree

8 files changed

+552
-6
lines changed

8 files changed

+552
-6
lines changed

flow-rust-ci.yaml

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
name: "Flow: Rust CI"
2+
on:
3+
pull_request:
4+
push:
5+
branches: ['main']
6+
7+
defaults:
8+
run:
9+
shell: bash
10+
11+
permissions:
12+
contents: read
13+
14+
env:
15+
NODE_VERSION: "20.18.3"
16+
17+
jobs:
18+
format:
19+
runs-on: hiero-client-sdk-linux-medium
20+
steps:
21+
- name: Harden Runner
22+
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
23+
with:
24+
egress-policy: audit
25+
26+
- name: Checkout Code
27+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
28+
with:
29+
submodules: 'recursive'
30+
31+
- name: Add `rustfmt` to toolchain
32+
run: |
33+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
34+
. $HOME/.cargo/env
35+
rustup +nightly component add rustfmt
36+
37+
- name: Format
38+
39+
run: |
40+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
41+
. $HOME/.cargo/env
42+
cargo +nightly fmt --check
43+
44+
check:
45+
runs-on: hiero-client-sdk-linux-medium
46+
steps:
47+
- name: Harden Runner
48+
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
49+
with:
50+
egress-policy: audit
51+
52+
- name: Checkout Code
53+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
54+
with:
55+
submodules: 'recursive'
56+
57+
- name: Setup Rust
58+
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # v1
59+
with:
60+
toolchain: 1.88.0
61+
62+
- name: Setup GCC and OpenSSL
63+
run: |
64+
sudo apt-get update
65+
sudo apt-get install -y --no-install-recommends gcc libc6-dev libc-dev libssl-dev pkg-config openssl
66+
67+
- name: Install Protoc
68+
uses: step-security/setup-protoc@f6eb248a6510dbb851209febc1bd7981604a52e3 # v3.0.0
69+
with:
70+
repo-token: ${{ secrets.GITHUB_TOKEN }}
71+
72+
- name: Check
73+
run: |
74+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
75+
. $HOME/.cargo/env
76+
cargo check --examples --workspace
77+
78+
test:
79+
needs: ['check']
80+
runs-on: hiero-client-sdk-linux-medium
81+
steps:
82+
- name: Harden Runner
83+
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
84+
with:
85+
egress-policy: audit
86+
87+
- name: Checkout Code
88+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
89+
with:
90+
submodules: 'recursive'
91+
92+
- name: Setup Rust
93+
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # v1
94+
with:
95+
toolchain: 1.88.0
96+
97+
- name: Setup GCC and OpenSSL
98+
run: |
99+
sudo apt-get update
100+
sudo apt-get install -y --no-install-recommends gcc libc6-dev libc-dev libssl-dev pkg-config openssl
101+
102+
- name: Install Protoc
103+
uses: step-security/setup-protoc@f6eb248a6510dbb851209febc1bd7981604a52e3 # v3.0.0
104+
with:
105+
repo-token: ${{ secrets.GITHUB_TOKEN }}
106+
107+
- name: Setup NodeJS
108+
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
109+
with:
110+
node-version: ${{ env.NODE_VERSION }}
111+
112+
- name: Prepare Hiero Solo
113+
id: solo
114+
uses: hiero-ledger/hiero-solo-action@6a1a77601cf3e69661fb6880530a4edf656b40d5 # v0.14.0
115+
with:
116+
installMirrorNode: true
117+
hieroVersion: v0.65.0
118+
119+
- name: Create env file
120+
run: |
121+
touch .env
122+
echo TEST_OPERATOR_KEY="${{ steps.solo.outputs.privateKey }}" >> .env
123+
echo TEST_OPERATOR_ID="${{ steps.solo.outputs.accountId }}" >> .env
124+
echo TEST_NETWORK_NAME="localhost" >> .env
125+
echo TEST_RUN_NONFREE="1" >> .env
126+
cat .env
127+
128+
- name: Test
129+
run: |
130+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
131+
. $HOME/.cargo/env
132+
cargo test --workspace -- --skip node::update
133+
dab-tests:
134+
needs: ['check']
135+
runs-on: hiero-client-sdk-linux-medium
136+
steps:
137+
- name: Harden Runner
138+
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
139+
with:
140+
egress-policy: audit
141+
142+
- name: Checkout Code
143+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
144+
with:
145+
submodules: 'recursive'
146+
147+
- name: Setup Rust
148+
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # v1
149+
with:
150+
toolchain: 1.88.0
151+
152+
- name: Setup GCC and OpenSSL
153+
run: |
154+
sudo apt-get update
155+
sudo apt-get install -y --no-install-recommends gcc libc6-dev libc-dev libssl-dev pkg-config openssl
156+
157+
- name: Install Protoc
158+
uses: step-security/setup-protoc@f6eb248a6510dbb851209febc1bd7981604a52e3 # v3.0.0
159+
with:
160+
repo-token: ${{ secrets.GITHUB_TOKEN }}
161+
162+
- name: Setup NodeJS
163+
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
164+
with:
165+
node-version: ${{ env.NODE_VERSION }}
166+
167+
- name: Prepare Hiero Solo for DAB Tests
168+
id: solo-dab
169+
uses: hiero-ledger/hiero-solo-action@6a1a77601cf3e69661fb6880530a4edf656b40d5 # v0.14.0
170+
with:
171+
installMirrorNode: true
172+
hieroVersion: v0.65.0
173+
174+
- name: Create env file for DAB Tests
175+
run: |
176+
touch .env
177+
echo TEST_OPERATOR_KEY="${{ steps.solo-dab.outputs.privateKey }}" >> .env
178+
echo TEST_OPERATOR_ID="${{ steps.solo-dab.outputs.accountId }}" >> .env
179+
echo TEST_NETWORK_NAME="localhost" >> .env
180+
echo TEST_RUN_NONFREE="1" >> .env
181+
cat .env
182+
183+
- name: Run DAB Tests
184+
run: |
185+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
186+
. $HOME/.cargo/env
187+
cargo test --test e2e node::update -- --nocapture

src/client/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,23 @@ impl Client {
637637
});
638638
}
639639

640+
/// Triggers an immediate network update from the address book.
641+
/// Note: This method is not part of the public API and may be changed or removed in future versions.
642+
pub(crate) async fn update_network_now(&self) {
643+
match NodeAddressBookQuery::new()
644+
.execute_mirrornet(self.mirrornet().load().channel(), None)
645+
.await
646+
{
647+
Ok(address_book) => {
648+
log::info!("Successfully updated network address book");
649+
self.set_network_from_address_book(address_book);
650+
}
651+
Err(e) => {
652+
log::warn!("Failed to update network address book: {e:?}");
653+
}
654+
}
655+
}
656+
640657
/// Returns the Account ID for the operator.
641658
#[must_use]
642659
pub fn get_operator_account_id(&self) -> Option<AccountId> {

src/client/network/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ impl NetworkData {
377377
node_ids = self.node_ids.to_vec();
378378
}
379379

380-
let node_sample_amount = (node_ids.len() + 2) / 3;
380+
let node_sample_amount = node_ids.len();
381381

382382
let node_id_indecies =
383383
rand::seq::index::sample(&mut thread_rng(), node_ids.len(), node_sample_amount);

src/execute.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,16 @@ pub(crate) trait Execute: ValidateChecksums {
127127
fn response_pre_check_status(response: &Self::GrpcResponse) -> crate::Result<i32>;
128128
}
129129

130-
struct ExecuteContext {
130+
struct ExecuteContext<'a> {
131131
// When `Some` the `transaction_id` will be regenerated when expired.
132132
operator_account_id: Option<AccountId>,
133133
network: Arc<NetworkData>,
134134
backoff_config: ExponentialBackoff,
135135
max_attempts: usize,
136136
// timeout for a single grpc request.
137137
grpc_timeout: Option<Duration>,
138+
// Reference to the client for triggering network updates
139+
client: &'a Client,
138140
}
139141

140142
pub(crate) async fn execute<E>(
@@ -187,24 +189,29 @@ where
187189
operator_account_id,
188190
network: client.net().0.load_full(),
189191
grpc_timeout: backoff.grpc_timeout,
192+
client,
190193
},
191194
executable,
192195
)
193196
.await
194197
}
195198

196-
async fn execute_inner<E>(ctx: &ExecuteContext, executable: &E) -> crate::Result<E::Response>
199+
async fn execute_inner<'a, E>(
200+
ctx: &ExecuteContext<'a>,
201+
executable: &E,
202+
) -> crate::Result<E::Response>
197203
where
198204
E: Execute + Sync,
199205
{
200-
fn recurse_ping(ctx: &ExecuteContext, index: usize) -> BoxFuture<'_, bool> {
206+
fn recurse_ping<'a, 'b: 'a>(ctx: &'b ExecuteContext<'a>, index: usize) -> BoxFuture<'b, bool> {
201207
Box::pin(async move {
202208
let ctx = ExecuteContext {
203209
operator_account_id: None,
204210
network: Arc::clone(&ctx.network),
205211
backoff_config: ctx.backoff_config.clone(),
206212
max_attempts: ctx.max_attempts,
207213
grpc_timeout: ctx.grpc_timeout,
214+
client: ctx.client,
208215
};
209216
let ping_query = PingQuery::new(ctx.network.node_ids()[index]);
210217

@@ -350,8 +357,8 @@ fn map_tonic_error(
350357
}
351358
}
352359

353-
async fn execute_single<E: Execute + Sync>(
354-
ctx: &ExecuteContext,
360+
async fn execute_single<'a, E: Execute + Sync>(
361+
ctx: &ExecuteContext<'a>,
355362
executable: &E,
356363
node_index: usize,
357364
transaction_id: &mut Option<TransactionId>,
@@ -449,6 +456,26 @@ async fn execute_single<E: Execute + Sync>(
449456
)))
450457
}
451458

459+
Status::InvalidNodeAccountId => {
460+
// The node account is invalid or doesn't match the submitted node
461+
// Mark the node as unhealthy and retry with backoff
462+
// This typically indicates the address book is out of date
463+
ctx.network.mark_node_unhealthy(node_index);
464+
465+
log::warn!(
466+
"Node at index {node_index} / node id {node_account_id} returned {status:?}, marking unhealthy. Updating address book before retry."
467+
);
468+
469+
// Update the network address book before retrying
470+
ctx.client.update_network_now().await;
471+
472+
Err(retry::Error::Transient(executable.make_error_pre_check(
473+
status,
474+
transaction_id.as_ref(),
475+
response,
476+
)))
477+
}
478+
452479
_ if executable.should_retry_pre_check(status) => {
453480
// conditional retry on pre-check should back-off and try again
454481
Err(retry::Error::Transient(executable.make_error_pre_check(

tests/e2e/common/mod.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,39 @@ pub(crate) fn setup_nonfree() -> Option<TestEnvironment> {
193193
}
194194
}
195195
}
196+
197+
/// Setup test environment for non-free tests with local 2-node network.
198+
/// Specifically configured for node update tests with:
199+
/// - Node 0.0.3 at 127.0.0.1:50211
200+
/// - Node 0.0.4 at 127.0.0.1:51211
201+
/// - Mirror network at 127.0.0.1:5600
202+
/// Returns None if TEST_RUN_NONFREE is not set or not on local network.
203+
pub(crate) fn setup_nonfree_local_two_nodes() -> Option<TestEnvironment> {
204+
let _ = dotenvy::dotenv();
205+
let _ = env_logger::builder().parse_default_env().is_test(true).try_init();
206+
207+
let config = &*CONFIG;
208+
209+
if !config.run_nonfree_tests {
210+
log::debug!("skipping non-free test");
211+
return None;
212+
}
213+
214+
if !config.is_local {
215+
log::debug!("skipping test, only runs on local network");
216+
return None;
217+
}
218+
219+
let mut network = HashMap::new();
220+
network.insert("127.0.0.1:50211".to_string(), AccountId::new(0, 0, 3));
221+
network.insert("127.0.0.1:51211".to_string(), AccountId::new(0, 0, 4));
222+
223+
let client = Client::for_network(network).unwrap();
224+
client.set_mirror_network(vec!["127.0.0.1:5600".to_string()]);
225+
226+
if let Some(op) = &config.operator {
227+
client.set_operator(op.account_id, op.private_key.clone());
228+
}
229+
230+
Some(TestEnvironment { config, client })
231+
}

tests/e2e/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod ethereum_transaction;
88
mod fee_schedules;
99
mod file;
1010
mod network_version_info;
11+
mod node;
1112
mod node_address_book;
1213
mod prng;
1314
/// Resources for various tests.

tests/e2e/node/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod update;

0 commit comments

Comments
 (0)