From 047dc62707ab2ead97ba5c000d7f0f204cc0e902 Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Fri, 24 Jul 2026 17:01:50 -0700 Subject: [PATCH 1/3] fix(client): fix rep selection --- Cargo.lock | 1 - keetanetwork-client/Cargo.toml | 1 - keetanetwork-client/src/client.rs | 23 ++---- keetanetwork-client/src/math.rs | 4 +- keetanetwork-client/src/rep.rs | 124 +++++++++++++----------------- keetanetwork-client/tests/e2e.rs | 60 +++++++++++++++ scripts/release.sh | 55 +++++++++++-- 7 files changed, 173 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e68f3e8..2889493 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1326,7 +1326,6 @@ dependencies = [ "prettyplease", "progenitor", "progenitor-client", - "rand_core 0.9.3", "reqwest", "serde", "serde_json", diff --git a/keetanetwork-client/Cargo.toml b/keetanetwork-client/Cargo.toml index 511dcb5..b3b8be5 100644 --- a/keetanetwork-client/Cargo.toml +++ b/keetanetwork-client/Cargo.toml @@ -46,7 +46,6 @@ keetanetwork-account = { workspace = true, features = ["alloc", "rasn"] } chrono = { workspace = true } progenitor-client = { workspace = true, optional = true } num-bigint = { workspace = true } -rand_core = { workspace = true } spin = { workspace = true, features = ["mutex", "spin_mutex", "rwlock", "once"] } async-trait = { workspace = true } futures = { workspace = true, features = ["alloc"] } diff --git a/keetanetwork-client/src/client.rs b/keetanetwork-client/src/client.rs index 1bbe1c4..7884349 100644 --- a/keetanetwork-client/src/client.rs +++ b/keetanetwork-client/src/client.rs @@ -8,7 +8,6 @@ use alloc::sync::Arc; use alloc::vec::Vec; use core::future::Future; use core::str::FromStr; -use core::sync::atomic::{AtomicU64, Ordering}; use core::time::Duration; use futures::future::{select, Either}; @@ -31,7 +30,7 @@ use crate::model::{ AccountState, Acl, Certificate, ChainPage, ChainQuery, HistoryEntry, HistoryPage, HistoryQuery, LedgerChecksum, Representative, TokenBalance, TransmitOptions, }; -use crate::rep::{RepBook, RepPart, RepRecord, RepRef, SmallRng}; +use crate::rep::{RepBook, RepPart, RepRecord, RepRef}; use crate::runtime::{Runtime, TaskHandle}; use crate::sync::{Mutex, RwLock}; use crate::transport::{LedgerSide, NodeTransport, TransportFactory}; @@ -115,9 +114,6 @@ struct Inner { /// discovery transport-agnostic. factory: Arc, runtime: Arc, - /// Per-pick counter mixed with the runtime clock to seed selection's - /// [`SmallRng`] so successive picks differ. - rng_counter: AtomicU64, network: RwLock>, subnet: RwLock>, /// `true` for the single anonymous-rep client built by [`KeetaClient::new`]; @@ -137,9 +133,8 @@ impl Drop for Inner { /// Async, durable client for a KeetaNet network. /// -/// Talks to a set of representatives: reads pick one rep (power-of-two -/// choices, weighted by reliability) with retry, backoff, and timeout; votes, -/// quotes, and publishes fan out to every rep and aggregate by quorum weight. +/// Talks to a set of representatives: reads go to the rep with the highest +/// effective score with retry, backoff, and timeout. /// /// See the [crate-level example](crate) for building and transmitting a block. #[derive(Clone, Debug)] @@ -206,8 +201,8 @@ impl KeetaClient { } /// Create a multi-representative client over `reps`, fanning votes and - /// publishes across them and selecting reps for reads by weighted - /// reliability. + /// publishes across them and routing reads to the rep with the highest + /// effective score (voting weight scaled by reliability). #[cfg(feature = "http")] pub fn with_representatives(reps: impl IntoIterator, config: ClientConfig) -> Self { let http = reqwest::Client::new(); @@ -256,7 +251,6 @@ impl KeetaClient { config, factory, runtime, - rng_counter: AtomicU64::new(0), network: RwLock::new(None), subnet: RwLock::new(None), single_rep, @@ -285,12 +279,9 @@ impl KeetaClient { self.bind_transports(self.inner.reps.snapshot()) } - /// Select one representative bound to its transport. + /// Select the current best-scored representative bound to its transport. fn pick_target(&self) -> Option { - let now = self.inner.runtime.now_millis(); - let counter = self.inner.rng_counter.fetch_add(1, Ordering::Relaxed); - let mut rng = SmallRng::seed_from_u64(now ^ counter); - let chosen = self.inner.reps.pick(&mut rng)?; + let chosen = self.inner.reps.pick()?; let transports = self.inner.transports.read(); transports.get(&chosen.key).map(|transport| RepPick { key: chosen.key, diff --git a/keetanetwork-client/src/math.rs b/keetanetwork-client/src/math.rs index dc67384..a320a5a 100644 --- a/keetanetwork-client/src/math.rs +++ b/keetanetwork-client/src/math.rs @@ -56,8 +56,8 @@ pub fn weight_fraction(weight: &BigInt, total: &BigInt, count: usize) -> f64 { bigint_ratio(weight, total) } -/// The Power-of-Two-Choices effective score: weight fraction scaled by -/// reliability. +/// The selection effective score: weight fraction scaled by reliability. +/// The highest-scored representative serves reads. #[must_use] pub fn selection_score(weight_fraction: f64, reliability: f64) -> f64 { weight_fraction * reliability diff --git a/keetanetwork-client/src/rep.rs b/keetanetwork-client/src/rep.rs index 3a820ed..b1e34a9 100644 --- a/keetanetwork-client/src/rep.rs +++ b/keetanetwork-client/src/rep.rs @@ -8,7 +8,6 @@ use alloc::string::String; use alloc::vec::Vec; use num_bigint::BigInt; -use rand_core::RngCore; use crate::math::{reliability_after_failure, reliability_after_success, selection_score, weight_fraction}; use crate::sync::RwLock; @@ -130,30 +129,21 @@ impl RepState { self.reps.iter().map(|rep| rep.weight.clone()).sum() } - /// Select one representative using Power of Two Choices: pick two random - /// indices and return the one with the higher effective score - /// (`weight_fraction * reliability`). Equal indices return that rep - /// directly, guaranteeing every rep a `1/n^2` baseline. - fn pick(&self, rng: &mut impl RngCore) -> Option { - let count = self.reps.len(); - if count == 0 { - return None; - } - - let chosen = if count == 1 { - &self.reps[0] - } else { - let total = self.total_weight(); - let index_a = (rng.next_u32() as usize) % count; - let index_b = (rng.next_u32() as usize) % count; - if self.effective_score(&self.reps[index_a], &total) >= self.effective_score(&self.reps[index_b], &total) { - &self.reps[index_a] - } else { - &self.reps[index_b] - } - }; + /// Select the representative with the highest effective score. + fn pick(&self) -> Option { + let total = self.total_weight(); + let leader = self + .reps + .iter() + .fold(None::<(&RepRecord, f64)>, |best, rep| { + let score = self.effective_score(rep, &total); + match best { + Some((_, top)) if top >= score => best, + _ => Some((rep, score)), + } + }); - Some(RepRef { key: chosen.key.clone(), weight: chosen.weight.clone() }) + leader.map(|(chosen, _)| RepRef { key: chosen.key.clone(), weight: chosen.weight.clone() }) } fn effective_score(&self, rep: &RepRecord, total: &BigInt) -> f64 { @@ -224,8 +214,8 @@ impl RepBook { (state.snapshot(), state.total_weight()) } - pub(crate) fn pick(&self, rng: &mut impl RngCore) -> Option { - self.state.read().pick(rng) + pub(crate) fn pick(&self) -> Option { + self.state.read().pick() } pub(crate) fn update_weights(&self, fetched: &[(String, BigInt)]) { @@ -233,47 +223,6 @@ impl RepBook { } } -/// A minimal `no_std` PRNG (`splitmix64`) for Power-of-Two-Choices selection. -/// -/// Selection needs only spread, not cryptographic randomness, so a tiny -/// seed-able generator replaces the std `rand` thread RNG and keeps rep -/// selection `no_std`. Seed it per pick from a monotonic clock mixed with a -/// counter so successive picks differ. -pub(crate) struct SmallRng(u64); - -impl SmallRng { - /// A generator seeded from `seed`. - pub(crate) fn seed_from_u64(seed: u64) -> Self { - Self(seed) - } -} - -impl RngCore for SmallRng { - fn next_u64(&mut self) -> u64 { - self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); - let mut z = self.0; - z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); - z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); - z ^ (z >> 31) - } - - fn next_u32(&mut self) -> u32 { - (self.next_u64() >> 32) as u32 - } - - fn fill_bytes(&mut self, dest: &mut [u8]) { - let mut chunks = dest.chunks_exact_mut(8); - for chunk in &mut chunks { - chunk.copy_from_slice(&self.next_u64().to_le_bytes()); - } - let remainder = chunks.into_remainder(); - if !remainder.is_empty() { - let bytes = self.next_u64().to_le_bytes(); - remainder.copy_from_slice(&bytes[..remainder.len()]); - } - } -} - /// A representative the client can talk to: its API endpoint, account, and /// voting weight. #[cfg(feature = "http")] @@ -346,14 +295,51 @@ mod tests { #[test] fn pick_returns_the_only_rep() { let state = RepState::new(vec![record("solo", 1)]); - let pick = state.pick(&mut SmallRng::seed_from_u64(1)); + let pick = state.pick(); assert!(matches!(pick, Some(chosen) if chosen.key == "solo")); } #[test] fn pick_on_empty_state_is_none() { let state = RepState::new(Vec::new()); - assert!(state.pick(&mut SmallRng::seed_from_u64(1)).is_none()); + assert!(state.pick().is_none()); + } + + #[test] + fn pick_always_returns_the_highest_weight_rep() { + let state = RepState::new(vec![record("light", 1), record("heavy", 99)]); + + let heavy_picks = (0..100) + .filter(|_| matches!(state.pick(), Some(chosen) if chosen.key == "heavy")) + .count(); + + assert_eq!(heavy_picks, 100); + } + + #[test] + fn pick_fails_over_when_the_leader_reliability_decays() { + let mut state = RepState::new(vec![record("heavy", 99), record("light", 1)]); + + // 0.99 weight * 0.01 reliability < 0.01 weight * 1.0 reliability. + state.decay("heavy", 0.01, 0.001); + + assert!(matches!(state.pick(), Some(chosen) if chosen.key == "light")); + } + + #[test] + fn pick_restores_the_leader_once_its_reliability_recovers() { + let mut state = RepState::new(vec![record("heavy", 99), record("light", 1)]); + state.decay("heavy", 0.01, 0.001); + + state.boost("heavy", 1.0); + + assert!(matches!(state.pick(), Some(chosen) if chosen.key == "heavy")); + } + + #[test] + fn pick_keeps_the_earlier_rep_on_a_score_tie() { + let state = RepState::new(vec![record("first", 5), record("second", 5)]); + assert!(matches!(state.pick(), Some(chosen) if chosen.key == "first")); } #[test] diff --git a/keetanetwork-client/tests/e2e.rs b/keetanetwork-client/tests/e2e.rs index 4c4c59f..bfe427f 100644 --- a/keetanetwork-client/tests/e2e.rs +++ b/keetanetwork-client/tests/e2e.rs @@ -523,6 +523,35 @@ async fn test_conflicting_vote_request_is_typed_node_error() -> Result<(), Box Result<(), Box> { + let fixture = fixture().await; + + // Built before the fixture's send publishes, so this block reads the + // same head and its `previous` is stale the moment that staple settles. + let stale = send_block(&fixture.client, &fixture.accounts, &fixture.accounts.recipient, SEND_AMOUNT + 1).await?; + + let accepted = fixture + .client + .transmit(&fixture.blocks, TransmitOptions::default()) + .await?; + assert!(accepted, "the node must accept the first staple"); + + let result = fixture + .client + .transmit(&[stale], TransmitOptions::default()) + .await; + assert!( + matches!(&result, Err(ClientError::Node { source }) if source.node_type() == Some(NodeErrorType::Ledger)), + "a stale-previous transmit must surface a typed LEDGER conflict, got {result:?}" + ); + + Ok(()) +} + /// Flat base-token fee a fee-enforcing node charges per transaction. const FEE_AMOUNT: u64 = 10; @@ -587,6 +616,37 @@ async fn test_transmit_without_signer_when_fee_required_errors() -> Result<(), B Ok(()) } +/// A round that fails after voting abandons its temporary vote on the +/// representative, and a retry that reworks the block at the same height +/// conflicts with that vote. +#[tokio::test(flavor = "multi_thread")] +async fn test_abandoned_temporary_vote_conflicts_with_a_reworked_retry() -> Result<(), Box> { + let (_node, client, accounts) = fee_fixture(); + + // The fee-less attempt fails only after the vote round, so the rep + // keeps a temporary vote for this block. + let original = send_block(&client, &accounts, &accounts.recipient, SEND_AMOUNT).await?; + let attempt = client + .transmit(&[original], TransmitOptions::default()) + .await; + assert!( + matches!(attempt, Err(ClientError::FeeRequired)), + "the fee-less attempt must fail with ClientError::FeeRequired, got {attempt:?}" + ); + + // Reworking the transfer yields a different block at the same height. + let reworked = send_block(&client, &accounts, &accounts.recipient, SEND_AMOUNT + 1).await?; + let retry = client + .transmit(&[reworked], trusted_fee_options(&accounts)) + .await; + assert!( + matches!(&retry, Err(ClientError::Node { source }) if source.node_type() == Some(NodeErrorType::Ledger)), + "the reworked retry must conflict with the abandoned temporary vote, got {retry:?}" + ); + + Ok(()) +} + /// Funding granted to a fee payer, covering several node fees. const PAYER_FUNDING: u64 = FEE_AMOUNT * 10; diff --git a/scripts/release.sh b/scripts/release.sh index 5396323..ff48bbe 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -241,6 +241,37 @@ resolve_package_version() { } # Crates.io API functions + +# The crates.io data access policy requires an identifying User-Agent; +# requests without one are rejected with a policy-violation error. +readonly CRATES_IO_USER_AGENT="node-rs-release-script (https://github.com/KeetaNetwork/node-rs)" + +# Fetch a crate's metadata from the crates.io API. Fails (non-zero, message +# on stderr) on any error other than "crate does not exist", which prints +# NOT_FOUND: a publish decision must never be made from an unreachable or +# rejected lookup. Callers run this in a command substitution, so they must +# die themselves on failure; an exit here only leaves the subshell. +fetch_crate_metadata() { + local package_name="$1" + + local response + if ! response=$(curl -s -A "$CRATES_IO_USER_AGENT" "https://crates.io/api/v1/crates/$package_name" 2>/dev/null); then + log_error "Could not reach crates.io to look up $package_name" + return 1 + fi + + if echo "$response" | jq -e '.errors' > /dev/null 2>&1; then + if echo "$response" | jq -e '.errors[] | select(.detail=="Not Found")' > /dev/null 2>&1; then + echo "NOT_FOUND" + return 0 + fi + log_error "crates.io rejected the $package_name lookup: $(echo "$response" | jq -r '.errors[0].detail')" + return 1 + fi + + echo "$response" +} + check_if_published() { local package_name="$1" local package_version="$2" @@ -248,9 +279,11 @@ check_if_published() { log_info "Checking if $package_name v$package_version is already published..." local response - response=$(curl -s "https://crates.io/api/v1/crates/$package_name" 2>/dev/null || echo "ERROR") + if ! response=$(fetch_crate_metadata "$package_name"); then + die "Aborting release: the $package_name published-version lookup failed" + fi - if [[ "$response" == "ERROR" ]] || echo "$response" | grep -q '"errors"'; then + if [[ "$response" == "NOT_FOUND" ]]; then return 1 # Not published fi @@ -268,9 +301,12 @@ get_package_checksum() { log_info "Getting checksum for $package_name v$package_version..." local response - response=$(curl -s "https://crates.io/api/v1/crates/$package_name" 2>/dev/null || echo "ERROR") + if ! response=$(fetch_crate_metadata "$package_name"); then + echo "ERROR" + return 1 + fi - if [[ "$response" == "ERROR" ]] || echo "$response" | grep -q '"errors"'; then + if [[ "$response" == "NOT_FOUND" ]]; then echo "ERROR" return 1 fi @@ -301,10 +337,17 @@ validate_package() { if cargo check --all-features; then log_success "Compilation validation passed for $package_name v$package_version" - # Then try cargo package, but don't fail if dependencies aren't on crates.io yet - if cargo package --allow-dirty --all-features 2>/dev/null; then + # Then try cargo package. A batched release cannot fully package a + # crate whose workspace dependencies are released in the same run + # (they are not on crates.io yet), so that failure is tolerated; + # any other packaging failure aborts. + local package_output + if package_output=$(cargo package --allow-dirty --all-features 2>&1); then log_success "Package validation passed for $package_name v$package_version" + elif echo "$package_output" | grep -q "failed to select a version for the requirement"; then + log_warning "Packaging $package_name v$package_version deferred: a workspace dependency is not published yet" elif [[ "$INITIAL_RELEASE" == "false" ]]; then + log_error "$package_output" die "Package validation failed for $package_name v$package_version" fi From 81ec809e0d6b41d73d6eecd02c02e5eb37b08851 Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Fri, 24 Jul 2026 17:07:25 -0700 Subject: [PATCH 2/3] chore: cleanup --- scripts/release.sh | 166 ++++++++++++++++++++++----------------------- 1 file changed, 81 insertions(+), 85 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index ff48bbe..02b2a00 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -152,7 +152,7 @@ die() { require_command() { local cmd="$1" local install_hint="${2:-}" - + if ! command -v "$cmd" &> /dev/null; then if [[ -n "$install_hint" ]]; then die "$cmd is required but not installed. $install_hint" @@ -165,18 +165,18 @@ require_command() { # Validation functions validate_environment() { log_info "Validating environment..." - + # Check required tools require_command "jq" "Please install jq: brew install jq" require_command "curl" "Please install curl" require_command "cargo" "Please install Rust and Cargo" require_command "git" "Please install git" - + # Verify we're in a git repository if ! git rev-parse --git-dir > /dev/null 2>&1; then die "Not in a git repository" fi - + # Check working directory cleanliness (skip in dry-run or with --allow-dirty) if [[ "$DRY_RUN" == "true" ]]; then log_dry_run "Skipping working directory clean check in dry-run mode" @@ -188,7 +188,7 @@ validate_environment() { git status --short exit 1 fi - + # Ensure we're in the project root if [[ ! -f "Cargo.toml" ]] || ! grep -q "^\[workspace\]" "Cargo.toml"; then die "Must be run from the workspace root directory" @@ -214,11 +214,11 @@ get_commits_since_last_release() { get_workspace_version() { local version version=$(grep '^version' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') - + if [[ -z "$version" ]]; then die "Could not extract version from Cargo.toml" fi - + echo "$version" } @@ -226,11 +226,11 @@ get_workspace_version() { resolve_package_version() { local package_dir="$1" local workspace_version="$2" - + # Extract version from package Cargo.toml local version_line version_line=$(grep '^version' "$package_dir/Cargo.toml" | head -1) - + if echo "$version_line" | grep -q "workspace = true"; then # Use workspace version echo "$workspace_version" @@ -246,20 +246,17 @@ resolve_package_version() { # requests without one are rejected with a policy-violation error. readonly CRATES_IO_USER_AGENT="node-rs-release-script (https://github.com/KeetaNetwork/node-rs)" -# Fetch a crate's metadata from the crates.io API. Fails (non-zero, message -# on stderr) on any error other than "crate does not exist", which prints -# NOT_FOUND: a publish decision must never be made from an unreachable or -# rejected lookup. Callers run this in a command substitution, so they must -# die themselves on failure; an exit here only leaves the subshell. +# Print a crate's metadata, NOT_FOUND for an unpublished crate, or fail. +# Runs in a command substitution: callers must die on failure themselves. fetch_crate_metadata() { local package_name="$1" - + local response if ! response=$(curl -s -A "$CRATES_IO_USER_AGENT" "https://crates.io/api/v1/crates/$package_name" 2>/dev/null); then log_error "Could not reach crates.io to look up $package_name" return 1 fi - + if echo "$response" | jq -e '.errors' > /dev/null 2>&1; then if echo "$response" | jq -e '.errors[] | select(.detail=="Not Found")' > /dev/null 2>&1; then echo "NOT_FOUND" @@ -268,25 +265,25 @@ fetch_crate_metadata() { log_error "crates.io rejected the $package_name lookup: $(echo "$response" | jq -r '.errors[0].detail')" return 1 fi - + echo "$response" } check_if_published() { local package_name="$1" local package_version="$2" - + log_info "Checking if $package_name v$package_version is already published..." - + local response if ! response=$(fetch_crate_metadata "$package_name"); then die "Aborting release: the $package_name published-version lookup failed" fi - + if [[ "$response" == "NOT_FOUND" ]]; then return 1 # Not published fi - + if echo "$response" | jq -e ".versions[] | select(.num==\"$package_version\")" > /dev/null 2>&1; then return 0 # Already published else @@ -297,28 +294,28 @@ check_if_published() { get_package_checksum() { local package_name="$1" local package_version="$2" - + log_info "Getting checksum for $package_name v$package_version..." - + local response if ! response=$(fetch_crate_metadata "$package_name"); then echo "ERROR" return 1 fi - + if [[ "$response" == "NOT_FOUND" ]]; then echo "ERROR" return 1 fi - + local checksum checksum=$(echo "$response" | jq -r ".versions[] | select(.num==\"$package_version\") | .checksum" 2>/dev/null) - + if [[ "$checksum" == "null" ]] || [[ -z "$checksum" ]]; then echo "ERROR" return 1 fi - + echo "$checksum" return 0 } @@ -328,19 +325,18 @@ validate_package() { local package_dir="$1" local package_name="$2" local package_version="$3" - + log_dry_run "Validating package $package_name v$package_version from $package_dir" - + cd "$package_dir" || die "Failed to change to package directory: $package_dir" - + # First try cargo check to validate compilation if cargo check --all-features; then log_success "Compilation validation passed for $package_name v$package_version" - + # Then try cargo package. A batched release cannot fully package a - # crate whose workspace dependencies are released in the same run - # (they are not on crates.io yet), so that failure is tolerated; - # any other packaging failure aborts. + # crate whose workspace dependencies are released in the same run, + # so that failure is tolerated. local package_output if package_output=$(cargo package --allow-dirty --all-features 2>&1); then log_success "Package validation passed for $package_name v$package_version" @@ -350,7 +346,7 @@ validate_package() { log_error "$package_output" die "Package validation failed for $package_name v$package_version" fi - + cd - > /dev/null || die "$ERR_RETURN_DIR" return 0 else @@ -364,14 +360,14 @@ publish_package() { local package_dir="$1" local package_name="$2" local package_version="$3" - + if [[ "$DRY_RUN" == "true" ]]; then validate_package "$package_dir" "$package_name" "$package_version" return $? fi - + log_info "Publishing $package_name v$package_version..." - + cd "$package_dir" || die "Failed to change to package directory: $package_dir" local publish_args=(--all-features) @@ -420,9 +416,9 @@ create_release_tag() { local commit_list="$2" local checksums="$3" local last_tag="$4" - + local tag_name="releases/v${version}" - + # Create tag message local tag_message="This is ${PROJECT_NAME} v${version} which has the following changes: " @@ -434,7 +430,7 @@ ${commit_list}" tag_message="${tag_message} - Initial release" fi - + tag_message="${tag_message} It includes the following release artifacts on crates.io: @@ -448,7 +444,7 @@ ${checksums} **Full Changelog**: https://github.com/KeetaNetwork/${PROJECT_NAME}/compare/${last_tag}...${tag_name}" fi - + if [[ "$DRY_RUN" == "true" ]]; then log_dry_run "Would create signed release tag: $tag_name" echo "" @@ -458,9 +454,9 @@ https://github.com/KeetaNetwork/${PROJECT_NAME}/compare/${last_tag}...${tag_name echo "----------------------------------------" return 0 fi - + log_info "Creating signed release tag: $tag_name" - + # Create signed tag if git tag -s "$tag_name" -m "$tag_message"; then log_success "Created signed tag: $tag_name" @@ -476,15 +472,15 @@ discover_workspace_packages() { # Use cargo metadata to get package information with dependencies local metadata metadata=$(cargo metadata --format-version 1 2>/dev/null) || die "Failed to get cargo metadata" - + # Get all workspace packages (filter out external dependencies) local workspace_packages workspace_packages=$(echo "$metadata" | jq -r '.workspace_members[]' | sed 's|.*/||' | sed 's|#.*||' | sort) - + if [[ -z "$workspace_packages" ]]; then die "No workspace packages found in metadata" fi - + # Convert to array local all_packages=() while IFS= read -r package; do @@ -492,11 +488,11 @@ discover_workspace_packages() { all_packages+=("$package") fi done <<< "$workspace_packages" - + if [[ ${#all_packages[@]} -eq 0 ]]; then die "No valid workspace packages found" fi - + # Topological sort based on dependencies from metadata topological_sort_packages "$metadata" "${all_packages[@]}" } @@ -505,22 +501,22 @@ topological_sort_packages() { local metadata="$1" shift local all_packages=("$@") - + local sorted_packages=() local remaining_packages=("${all_packages[@]}") local iteration=0 - + while [[ ${#remaining_packages[@]} -gt 0 && $iteration -lt $MAX_DEPENDENCY_ITERATIONS ]]; do local made_progress=false local new_remaining=() - + for package in "${remaining_packages[@]}"; do local has_unresolved_deps=false - + # Get dependencies for this package from metadata local package_deps package_deps=$(echo "$metadata" | jq -r ".packages[] | select(.name==\"$package\") | .dependencies[].name" 2>/dev/null) - + for dep in $package_deps; do # Check if this dependency is a workspace package still in remaining packages for remaining in "${remaining_packages[@]}"; do @@ -530,7 +526,7 @@ topological_sort_packages() { fi done done - + if [[ "$has_unresolved_deps" == false ]]; then # This package can be processed now sorted_packages+=("$package") @@ -540,19 +536,19 @@ topological_sort_packages() { new_remaining+=("$package") fi done - + remaining_packages=("${new_remaining[@]}") - + if [[ "$made_progress" == false ]]; then log_warning "Circular dependency detected or unable to resolve dependencies. Remaining packages: ${remaining_packages[*]}" # Add remaining packages in original order sorted_packages+=("${remaining_packages[@]}") break fi - + ((iteration++)) done - + # Output the sorted packages printf '%s\n' "${sorted_packages[@]}" } @@ -594,30 +590,30 @@ process_packages() { local workspace_version="$1" shift local packages=("$@") - + local published_packages=() local checksums_list="" - + # Publish each package in dependency order for package in "${packages[@]}"; do if [[ ! -d "$package" ]]; then log_warning "Package directory $package not found, skipping" continue fi - + log_info "Processing package: $package" - + # Resolve version (workspace or explicit) local version version=$(resolve_package_version "$package" "$workspace_version") - + if [[ -z "$version" ]]; then log_warning "Could not extract version for $package, skipping" continue fi - + log_info "Found $package version: $version" - + # Check if already published (skip check if --initial flag is used) if [[ "$INITIAL_RELEASE" == "true" ]]; then log_info "Initial release mode: forcing publication of $package v$version" @@ -625,10 +621,10 @@ process_packages() { else local skip_published_check=false fi - + if [[ "$skip_published_check" == "false" ]] && check_if_published "$package" "$version"; then log_warning "$package v$version is already published, skipping" - + # Still get checksum for release notes (unless dry-run) if [[ "$DRY_RUN" != "true" ]]; then local checksum @@ -645,12 +641,12 @@ process_packages() { # Publish the package if publish_package "$package" "$package" "$version"; then published_packages+=("$package@$version") - + if [[ "$DRY_RUN" != "true" ]]; then # Wait for crates.io to process log_info "Waiting for crates.io to process $package..." sleep $CRATES_IO_WAIT_TIME - + # Get checksum local checksum if checksum=$(get_package_checksum "$package" "$version"); then @@ -673,7 +669,7 @@ process_packages() { fi fi done - + # Output results for main function echo "PUBLISHED_PACKAGES:${published_packages[*]}" echo "CHECKSUMS_LIST_START" @@ -687,7 +683,7 @@ finalize_release() { local published_packages_str="$3" local checksums_list="$4" local last_tag="$5" - + # Convert string back to array IFS=' ' read -ra published_packages <<< "$published_packages_str" @@ -723,11 +719,11 @@ finalize_release() { log_info "- To run for real: make release [--initial]" else log_success "Release process completed successfully!" - + if [[ ${#published_packages[@]} -gt 0 ]]; then log_info "Published packages: ${published_packages[*]}" fi - + log_info "Created release tag: releases/v${workspace_version}" log_info "To push the tag to GitHub, run: git push origin releases/v${workspace_version}" fi @@ -754,17 +750,17 @@ main() { else log_info "Starting release process..." fi - + if [[ "$INITIAL_RELEASE" == "true" ]]; then log_info "Initial release mode: will force publication of all packages" fi - + # Validate environment and prerequisites validate_environment - + # Run tests and lints run_tests_and_lints - + # Get release information local last_tag last_tag=$(get_last_release_tag) @@ -773,15 +769,15 @@ main() { else log_info "No previous release tags found" fi - + log_info "Getting commits since last release..." local commit_list commit_list=$(get_commits_since_last_release "$last_tag") - + local workspace_version workspace_version=$(get_workspace_version) log_info "Release version: $workspace_version" - + # Discover packages and determine order log_info "Discovering workspace packages and dependency order..." local packages=() @@ -790,7 +786,7 @@ main() { packages+=("$package") fi done < <(discover_workspace_packages) - + if [[ ${#packages[@]} -eq 0 ]]; then die "No workspace packages found" fi @@ -808,16 +804,16 @@ main() { fi log_info "Package publishing order: ${packages[*]}" - + # Process packages and collect results local process_output process_output=$(process_packages "$workspace_version" "${packages[@]}") - + local published_packages_str local checksums_list published_packages_str=$(echo "$process_output" | grep "^PUBLISHED_PACKAGES:" | cut -d: -f2-) checksums_list=$(echo "$process_output" | sed -n '/^CHECKSUMS_LIST_START$/,/^CHECKSUMS_LIST_END$/p' | sed '1d;$d') - + # Finalize release finalize_release "$workspace_version" "$commit_list" "$published_packages_str" "$checksums_list" "$last_tag" } From b1e16a21ba101f6ced4bfb9533e48e78bf7a3208 Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Fri, 24 Jul 2026 17:13:43 -0700 Subject: [PATCH 3/3] chore(release): various packages --- Cargo.lock | 8 ++++---- Cargo.toml | 8 ++++---- keetanetwork-bindings/Cargo.toml | 2 +- keetanetwork-client-wasi/Cargo.toml | 2 +- keetanetwork-client-wasm/Cargo.toml | 2 +- keetanetwork-client/Cargo.toml | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2889493..310db63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1268,7 +1268,7 @@ dependencies = [ [[package]] name = "keetanetwork-bindings" -version = "0.4.3" +version = "0.4.4" dependencies = [ "chrono", "hex", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "keetanetwork-client" -version = "0.5.0" +version = "0.5.1" dependencies = [ "async-trait", "base64", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "keetanetwork-client-wasi" -version = "0.6.0" +version = "0.6.1" dependencies = [ "hex", "keetanetwork-account", @@ -1358,7 +1358,7 @@ dependencies = [ [[package]] name = "keetanetwork-client-wasm" -version = "0.5.0" +version = "0.5.1" dependencies = [ "chrono", "hex", diff --git a/Cargo.toml b/Cargo.toml index fc57f87..42cc39b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,22 +179,22 @@ path = "keetanetwork-ledger" default-features = false [workspace.dependencies.keetanetwork-client] -version = "0.5.0" +version = "0.5.1" path = "keetanetwork-client" default-features = false [workspace.dependencies.keetanetwork-bindings] -version = "0.4.3" +version = "0.4.4" path = "keetanetwork-bindings" default-features = false [workspace.dependencies.keetanetwork-client-wasi] -version = "0.6.0" +version = "0.6.1" path = "keetanetwork-client-wasi" default-features = false [workspace.dependencies.keetanetwork-client-wasm] -version = "0.5.0" +version = "0.5.1" path = "keetanetwork-client-wasm" default-features = false diff --git a/keetanetwork-bindings/Cargo.toml b/keetanetwork-bindings/Cargo.toml index 80ddedb..ac47282 100644 --- a/keetanetwork-bindings/Cargo.toml +++ b/keetanetwork-bindings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-bindings" -version = "0.4.3" +version = "0.4.4" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/keetanetwork-client-wasi/Cargo.toml b/keetanetwork-client-wasi/Cargo.toml index 9a9d4a2..6d0a089 100644 --- a/keetanetwork-client-wasi/Cargo.toml +++ b/keetanetwork-client-wasi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-client-wasi" -version = "0.6.0" +version = "0.6.1" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/keetanetwork-client-wasm/Cargo.toml b/keetanetwork-client-wasm/Cargo.toml index 80f6144..8febf29 100644 --- a/keetanetwork-client-wasm/Cargo.toml +++ b/keetanetwork-client-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-client-wasm" -version = "0.5.0" +version = "0.5.1" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/keetanetwork-client/Cargo.toml b/keetanetwork-client/Cargo.toml index b3b8be5..f3269e9 100644 --- a/keetanetwork-client/Cargo.toml +++ b/keetanetwork-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keetanetwork-client" -version = "0.5.0" +version = "0.5.1" edition.workspace = true authors.workspace = true license.workspace = true