Skip to content

Commit f406a60

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

File tree

8 files changed

+420
-8
lines changed

8 files changed

+420
-8
lines changed

.github/workflows/flow-rust-ci.yaml

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ jobs:
114114
uses: hiero-ledger/hiero-solo-action@6a1a77601cf3e69661fb6880530a4edf656b40d5 # v0.14.0
115115
with:
116116
installMirrorNode: true
117-
hieroVersion: v0.65.0
118117

119118
- name: Create env file
120119
run: |
@@ -129,4 +128,58 @@ jobs:
129128
run: |
130129
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
131130
. $HOME/.cargo/env
132-
cargo test --workspace
131+
cargo test --workspace -- --skip node::update
132+
dab-tests:
133+
needs: ['check']
134+
runs-on: hiero-client-sdk-linux-medium
135+
steps:
136+
- name: Harden Runner
137+
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
138+
with:
139+
egress-policy: audit
140+
141+
- name: Checkout Code
142+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
143+
with:
144+
submodules: 'recursive'
145+
146+
- name: Setup Rust
147+
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # v1
148+
with:
149+
toolchain: 1.88.0
150+
151+
- name: Setup GCC and OpenSSL
152+
run: |
153+
sudo apt-get update
154+
sudo apt-get install -y --no-install-recommends gcc libc6-dev libc-dev libssl-dev pkg-config openssl
155+
156+
- name: Install Protoc
157+
uses: step-security/setup-protoc@f6eb248a6510dbb851209febc1bd7981604a52e3 # v3.0.0
158+
with:
159+
repo-token: ${{ secrets.GITHUB_TOKEN }}
160+
161+
- name: Setup NodeJS
162+
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
163+
with:
164+
node-version: ${{ env.NODE_VERSION }}
165+
166+
- name: Prepare Hiero Solo for DAB Tests
167+
id: solo-dab
168+
uses: hiero-ledger/hiero-solo-action@6a1a77601cf3e69661fb6880530a4edf656b40d5 # v0.14.0
169+
with:
170+
installMirrorNode: true
171+
172+
- name: Create env file for DAB Tests
173+
run: |
174+
touch .env
175+
echo TEST_OPERATOR_KEY="${{ steps.solo-dab.outputs.privateKey }}" >> .env
176+
echo TEST_OPERATOR_ID="${{ steps.solo-dab.outputs.accountId }}" >> .env
177+
echo TEST_NETWORK_NAME="localhost" >> .env
178+
echo TEST_RUN_NONFREE="1" >> .env
179+
cat .env
180+
181+
- name: Run DAB Tests
182+
run: |
183+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
184+
. $HOME/.cargo/env
185+
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)