Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 48 additions & 21 deletions .github/scripts/ensure-binaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ set -euo pipefail
# (branch, tag, or commit SHA).
#
# defra and hub.rs binaries are downloaded as GitHub Actions artifacts
# from their CI workflows. The exact commit for each ref must have a
# successful CI run with uploaded artifacts, or this script fails.
# from their CI workflows when the pinned commit has them. Those artifacts
# are only produced on a push to main, so a ref pinned to a branch still
# under review has none; that case falls back to a source build rather than
# failing, because a pinned branch is the normal state while a cross-repo
# change is in flight.
#
# orbis-rs has no CI artifact pipeline, so this script resolves the
# commit, clones/fetches into a persistent local checkout, and does
# an incremental cargo build --release.
# orbis-rs has no CI artifact pipeline at all, so it is always built from
# source: resolve the commit, clone or fetch into a persistent local
# checkout, and do an incremental cargo build --release.
#
# Required:
# backbone.toml — in the repo root (or any ancestor directory)
Expand Down Expand Up @@ -90,41 +93,45 @@ resolve_commit() {
}

# Download a binary artifact from a GitHub Actions workflow run.
# Try to place $binary_name in the cache from a CI artifact. Returns non-zero
# without exiting when the commit has no successful run or that run published
# no matching artifact, so the caller can build from source instead.
download_artifact() {
local repo=$1 ref=$2 artifact_name=$3 binary_name=$4
local commit
commit=$(resolve_commit "$repo" "$ref")
echo "$repo: $ref → ${commit:0:12}"
local repo=$1 commit=$2 artifact_name=$3 binary_name=$4

local run_id
run_id=$(gh run list -R "sourcenetwork/$repo" \
--commit "$commit" --status success --workflow ci.yml \
--limit 1 --json databaseId -q '.[0].databaseId')

if [[ -z "$run_id" ]]; then
echo " ERROR: no successful CI run found for $repo@${commit:0:12}" >&2
echo " The CI for $repo must complete successfully before backbone CI can run." >&2
exit 1
echo " no successful CI run for $repo@${commit:0:12}"
return 1
fi

echo " Downloading $artifact_name from run $run_id..."
local tmp_dir
tmp_dir=$(mktemp -d)
gh run download "$run_id" -R "sourcenetwork/$repo" \
--name "$artifact_name" --dir "$tmp_dir"
if ! gh run download "$run_id" -R "sourcenetwork/$repo" \
--name "$artifact_name" --dir "$tmp_dir" 2>/dev/null; then
echo " run $run_id published no $artifact_name"
rm -rf "$tmp_dir"
return 1
fi

local found
found=$(find "$tmp_dir" -type f | head -1)
if [[ -z "$found" ]]; then
echo " ERROR: artifact $artifact_name was empty" >&2
echo " artifact $artifact_name was empty"
rm -rf "$tmp_dir"
exit 1
return 1
fi

cp "$found" "$CACHE_DIR/$binary_name"
chmod +x "$CACHE_DIR/$binary_name"
rm -rf "$tmp_dir"
echo " $binary_name: ready"
echo " $binary_name: ready (artifact)"
return 0
}

# --- orbis-rs helper functions (source build, no CI artifacts) ---
Expand Down Expand Up @@ -246,11 +253,31 @@ for var in DEFRA_REPO DEFRA_REF HUBD_REPO HUBD_REF ORBIS_REPO ORBIS_REF; do
echo " $var=${!var}"
done

# defra and hub.rs: download release artifacts from their CI
# defra and hub.rs: prefer a CI artifact, build from source when the pinned
# commit has none. Their artifact jobs are gated on a push to main, so a ref
# pinned to a branch under review always takes the source path.
echo ""
echo "--- defra/hub.rs (GitHub Actions artifacts) ---"
download_artifact "$DEFRA_REPO" "$DEFRA_REF" "defra-iroh-aarch64-apple-darwin" "defra-iroh"
download_artifact "$HUBD_REPO" "$HUBD_REF" "hubd-aarch64-apple-darwin" "hubd"
echo "--- defra (artifact, else source) ---"
DEFRA_COMMIT=$(resolve_commit "$DEFRA_REPO" "$DEFRA_REF")
echo "$DEFRA_REPO: $DEFRA_REF → ${DEFRA_COMMIT:0:12}"
if ! download_artifact "$DEFRA_REPO" "$DEFRA_COMMIT" \
"defra-iroh-aarch64-apple-darwin" "defra-iroh"; then
echo " Falling back to a source build."
build_if_missing "$DEFRA_REPO" "$DEFRA_REF" "$DEFRA_COMMIT" \
"cli:defra:defra-iroh:sourcehub,orbis,iroh"
prune_old_versions "$DEFRA_REPO"
fi

echo ""
echo "--- hub.rs (artifact, else source) ---"
HUBD_COMMIT=$(resolve_commit "$HUBD_REPO" "$HUBD_REF")
echo "$HUBD_REPO: $HUBD_REF → ${HUBD_COMMIT:0:12}"
if ! download_artifact "$HUBD_REPO" "$HUBD_COMMIT" \
"hubd-aarch64-apple-darwin" "hubd"; then
echo " Falling back to a source build."
build_if_missing "$HUBD_REPO" "$HUBD_REF" "$HUBD_COMMIT" "hubd:hubd"
prune_old_versions "$HUBD_REPO"
fi

# orbis-rs: no CI artifact pipeline, build from source
echo ""
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ See `docs/architecture.md` for the full security architecture.
backbone/
├── crates/
│ ├── test-infra/ # Shared primitives (ManagedProcess, ports, log tracking, run dirs)
│ ├── sourcehub-harness/ # Go sourcehubd manager (legacy, being replaced by hub-harness)
│ ├── sourcehub-harness/ # Vera (Go verad) devnet manager: the trust plane for gents-cloud tests
│ ├── defra-harness/ # DefraDB node manager + CLI client + test fixtures
│ ├── hub-harness/ # Hub.rs node manager + cluster builder + observability
│ └── orbis-harness/ # Orbis ring builder + DKG fixtures + event subscriptions
Expand Down Expand Up @@ -84,7 +84,7 @@ cargo fmt --all # Format
## Running the canonical test

```bash
# Requires sourcehubd, defra, and orbis-node binaries on PATH
# Requires hubd, defra-iroh, and orbis-node binaries on PATH (verad for tests/gents_cloud)
cargo test --test full_stack -- --ignored --nocapture
```

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ path = "tests/full_stack.rs"
name = "hub_light_client"
path = "tests/hub_light_client.rs"

[[test]]
name = "gents_cloud"
path = "tests/gents_cloud/main.rs"

[dev-dependencies]
orbis-harness = { path = "crates/orbis-harness" }
defra-harness = { path = "crates/defra-harness" }
Expand All @@ -68,6 +72,7 @@ hex.workspace = true
bs58 = "0.5"
eyre.workspace = true
sha2 = "0.10"
blst = "0.3"
tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
alloy-primitives.workspace = true
Expand Down
17 changes: 14 additions & 3 deletions backbone.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,38 @@
# Single source of truth for binary dependency versions. The CI script
# ensure-binaries.sh reads refs from this file. To use a different
# version, edit the ref here — branches, tags, and commit SHAs all work.
#
# defra and orbis-node are pinned to their Vera-compatibility branches until
# those land on main: Vera (github.com/sourcenetwork/vera, formerly SourceHub)
# renamed its protos to `vera.*` and its bech32 prefix to `vera`, and the
# clients on main still speak `sourcehub.*` / `source`.

[components.defra]
prefix = "DEFRA"
repo = "https://github.com/sourcenetwork/defradb.rs"
ref = "main"
ref = "vclq/vera-compat"
cargo_package = "cli"

[components.orbis-node]
prefix = "ORBIS"
repo = "https://github.com/sourcenetwork/orbis-rs"
ref = "jack/integration-testing"
ref = "vclq/vera-compat"
cargo_package = "orbis-node"

[components.cli-tool]
prefix = "ORBIS_CLI"
repo = "https://github.com/sourcenetwork/orbis-rs"
ref = "jack/integration-testing"
ref = "vclq/vera-compat"
cargo_package = "cli-tool"

[components.hubd]
prefix = "HUBD"
repo = "https://github.com/sourcenetwork/hub.rs"
ref = "main"
cargo_package = "hubd"

[components.verad]
prefix = "VERA"
repo = "https://github.com/sourcenetwork/vera"
ref = "main"
go_package = "./cmd/verad"
2 changes: 2 additions & 0 deletions crates/defra-harness/src/cluster/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ impl TestClusterBuilder {
acp_request_timeout: self.acp_request_timeout,
acp_receipt_timeout: self.acp_receipt_timeout,
extra_args: self.extra_rust_args.clone(),
extra_envs: Vec::new(),
};

let mut attempt = 1;
Expand Down Expand Up @@ -588,6 +589,7 @@ impl TestClusterBuilder {
acp_request_timeout: self.acp_request_timeout,
acp_receipt_timeout: self.acp_receipt_timeout,
extra_args: Vec::new(),
extra_envs: Vec::new(),
};

let mut attempt = 1;
Expand Down
1 change: 1 addition & 0 deletions crates/defra-harness/src/node/go_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ impl DefraNode for GoNode {
}

args.extend(config.extra_args.iter().cloned());
envs.extend(config.extra_envs.iter().cloned());

(self.binary_path.clone(), args, envs)
}
Expand Down
14 changes: 14 additions & 0 deletions crates/defra-harness/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ pub struct OrbisSignerConfig {
pub ring_id: String,
/// Derivation label (e.g. `"x-archive"`) for derived key signing.
pub derivation: String,
/// Hex ed25519 private key (64 bytes) the node authenticates to the ring
/// with, when it differs from `--identity`.
///
/// The ring accepts EdDSA bearer tokens only, while a node whose document
/// ACP is SourceHub/Vera must hold a secp256k1 identity to sign chain
/// transactions. Set this to keep both.
pub service_identity: Option<String>,
}

/// Configuration for a single DefraDB node.
Expand Down Expand Up @@ -217,6 +224,12 @@ pub struct NodeConfig {
pub acp_circuit_breaker_reset_timeout: Option<u64>,
pub acp_request_timeout: Option<u64>,
pub acp_receipt_timeout: Option<u64>,
/// Extra environment variables for the node process, applied after the
/// managed ones. The escape hatch for process-global switches a node reads
/// from the environment rather than a flag -- notably
/// `DEFRA_ALLOW_NON_GO_VERIFIABLE_SIGNING`, which gates emitting BLS
/// (ring-signed) blocks that Go peers cannot verify.
pub extra_envs: Vec<(String, String)>,
/// Raw CLI flags appended after every managed flag, so they win under
/// clap's last-one-wins parsing. The escape hatch for options the builder
/// has no typed method for -- notably enforcement tests, which must set an
Expand Down Expand Up @@ -258,6 +271,7 @@ impl NodeConfig {
acp_circuit_breaker_reset_timeout: None,
acp_request_timeout: None,
acp_receipt_timeout: None,
extra_envs: Vec::new(),
extra_args: Vec::new(),
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/defra-harness/src/node/rust_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ impl DefraNode for RustNode {
"--signer-orbis-derivation".into(),
signer.derivation.clone(),
]);
if let Some(ref service_identity) = signer.service_identity {
args.extend(["--signer-orbis-identity".into(), service_identity.clone()]);
}
} else if !config.signing_enabled {
args.push("--no-signing".to_string());
}
Expand Down Expand Up @@ -220,6 +223,7 @@ impl DefraNode for RustNode {
}

args.extend(config.extra_args.iter().cloned());
envs.extend(config.extra_envs.iter().cloned());

(self.binary_path.clone(), args, envs)
}
Expand Down
31 changes: 31 additions & 0 deletions crates/orbis-harness/src/cli/did.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@
///
/// We use the `ed25519-dalek` approach via raw bytes: generate the public key
/// from the seed, then encode as did:key with multicodec + base58btc.
/// Build the 64-byte ed25519 private key DefraDB expects for `--identity` or
/// `--signer-orbis-identity`, from a 32-byte seed.
///
/// DefraDB stores an ed25519 private key as seed followed by public key (Go
/// parity) and picks the key type by length, so a 128-character hex string is
/// an ed25519 identity and a 64-character one is secp256k1. The resulting DID
/// is the same one [`signer_did_for_pk`] derives from the seed.
pub fn ed25519_identity_hex(seed_hex: &str) -> String {
let seed_bytes = hex::decode(seed_hex).expect("seed must be valid hex");
let signing_key = ed25519_dalek::SigningKey::from_bytes(
&seed_bytes[..32]
.try_into()
.expect("seed must be at least 32 bytes"),
);
let mut key = signing_key.to_bytes().to_vec();
key.extend_from_slice(&signing_key.verifying_key().to_bytes());
hex::encode(key)
}

pub fn signer_did_for_pk(private_key_hex: &str) -> String {
let seed_bytes = hex::decode(private_key_hex).expect("signer_did_pk must be valid hex");

Expand Down Expand Up @@ -36,6 +55,18 @@ mod tests {
assert!(did.starts_with("did:key:z"), "got: {}", did);
}

#[test]
fn ed25519_identity_hex_is_seed_then_public_key() {
let seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let identity = ed25519_identity_hex(seed);
assert_eq!(identity.len(), 128, "64 bytes as hex");
assert!(
identity.starts_with(seed),
"the seed is the first half: {}",
identity
);
}

#[test]
fn signer_did_deterministic() {
let key_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
Expand Down
Loading
Loading