Skip to content

Commit 06adb7a

Browse files
committed
feat: plain k8s build of hashi-guardian
Adds a non-enclave Containerfile under docker/hashi-guardian-k8s/ that mirrors hashi-screener's build shape, registers grpc.health.v1.Health in the guardian binary so K8s gRPC probes work, and ships a dev-only bootstrap_operator_init example that drives OperatorInit from env vars against a real AWS S3 bucket. The existing docker/hashi-guardian/ Nitro enclave build is untouched.
1 parent 1b5c240 commit 06adb7a

6 files changed

Lines changed: 238 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/hashi-guardian/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ serde_json.workspace = true
1212
tracing.workspace = true
1313
tracing-subscriber.workspace = true
1414
tonic.workspace = true
15+
tonic-health.workspace = true
1516
hashi-types = { path = "../hashi-types" }
1617

1718
# Crypto dependencies
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Bootstrap a hashi-guardian instance by calling `OperatorInit` with AWS S3
5+
//! credentials and dummy share commitments. This is a *development-only* helper
6+
//! intended to make a running guardian fully initialized enough to emit
7+
//! heartbeats against a real AWS S3 bucket with Object Lock enabled.
8+
//!
9+
//! It does **not** run a real `SetupNewKey` + `ProvisionerInit` flow — the
10+
//! resulting guardian will accept `OperatorInit` and start heartbeating, but
11+
//! will not be able to sign withdrawals (because no real key shares are ever
12+
//! combined). For production bootstrap use the operator + KP flow, not this.
13+
//!
14+
//! Usage (from repo root):
15+
//!
16+
//! GUARDIAN_ENDPOINT=http://localhost:3000 \
17+
//! AWS_S3_BUCKET=mysten-hashi-guardian-dev \
18+
//! AWS_S3_REGION=us-west-2 \
19+
//! AWS_ACCESS_KEY_ID=... \
20+
//! AWS_SECRET_ACCESS_KEY=... \
21+
//! BITCOIN_NETWORK=signet \
22+
//! cargo run -p hashi-guardian --example bootstrap_operator_init
23+
24+
use anyhow::anyhow;
25+
use anyhow::Context;
26+
use anyhow::Result;
27+
use hashi_types::proto::guardian_service_client::GuardianServiceClient;
28+
use hashi_types::proto::GuardianShareCommitment;
29+
use hashi_types::proto::GuardianShareId;
30+
use hashi_types::proto::Network as ProtoNetwork;
31+
use hashi_types::proto::OperatorInitRequest;
32+
use hashi_types::proto::S3Config as ProtoS3Config;
33+
use std::env;
34+
35+
/// Must match `hashi_types::guardian::crypto::NUM_OF_SHARES`. We provide that
36+
/// many dummy commitments because `OperatorInitRequest` stores them verbatim;
37+
/// actual share verification happens later in `ProvisionerInit`, which this
38+
/// helper does not drive.
39+
const NUM_OF_SHARES: u32 = 5;
40+
41+
fn required_env(name: &str) -> Result<String> {
42+
env::var(name).map_err(|_| anyhow!("required env var `{name}` is not set"))
43+
}
44+
45+
fn parse_network(s: &str) -> Result<ProtoNetwork> {
46+
match s.to_ascii_lowercase().as_str() {
47+
"mainnet" => Ok(ProtoNetwork::Mainnet),
48+
"testnet" => Ok(ProtoNetwork::Testnet),
49+
"regtest" => Ok(ProtoNetwork::Regtest),
50+
"signet" => Ok(ProtoNetwork::Signet),
51+
other => Err(anyhow!(
52+
"unknown BITCOIN_NETWORK `{other}`; expected mainnet/testnet/regtest/signet"
53+
)),
54+
}
55+
}
56+
57+
#[tokio::main]
58+
async fn main() -> Result<()> {
59+
tracing_subscriber::fmt()
60+
.with_env_filter(
61+
tracing_subscriber::EnvFilter::builder()
62+
.with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
63+
.from_env_lossy(),
64+
)
65+
.init();
66+
67+
let endpoint =
68+
env::var("GUARDIAN_ENDPOINT").unwrap_or_else(|_| "http://localhost:3000".to_string());
69+
let bucket = required_env("AWS_S3_BUCKET")?;
70+
let region = required_env("AWS_S3_REGION")?;
71+
let access_key = required_env("AWS_ACCESS_KEY_ID")?;
72+
let secret_key = required_env("AWS_SECRET_ACCESS_KEY")?;
73+
let network_str = env::var("BITCOIN_NETWORK").unwrap_or_else(|_| "signet".to_string());
74+
let network = parse_network(&network_str)?;
75+
76+
tracing::info!(
77+
endpoint = %endpoint,
78+
bucket = %bucket,
79+
region = %region,
80+
network = ?network,
81+
"connecting to guardian"
82+
);
83+
84+
let mut client = GuardianServiceClient::connect(endpoint.clone())
85+
.await
86+
.with_context(|| format!("failed to connect to guardian at {endpoint}"))?;
87+
88+
let share_commitments: Vec<GuardianShareCommitment> = (1..=NUM_OF_SHARES)
89+
.map(|id| GuardianShareCommitment {
90+
id: Some(GuardianShareId { id: Some(id) }),
91+
digest_hex: Some(String::new()),
92+
})
93+
.collect();
94+
95+
let request = OperatorInitRequest {
96+
s3_config: Some(ProtoS3Config {
97+
access_key: Some(access_key),
98+
secret_key: Some(secret_key),
99+
bucket_name: Some(bucket),
100+
region: Some(region),
101+
}),
102+
share_commitments,
103+
network: Some(network as i32),
104+
};
105+
106+
tracing::info!("calling OperatorInit");
107+
let response = client
108+
.operator_init(request)
109+
.await
110+
.context("operator_init RPC failed")?;
111+
tracing::info!(?response, "OperatorInit returned successfully");
112+
println!("OperatorInit complete.");
113+
Ok(())
114+
}

crates/hashi-guardian/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use std::sync::OnceLock;
2020
use std::sync::RwLock;
2121
use std::time::Duration;
2222
use tonic::transport::Server;
23+
use tonic_health::server::health_reporter;
2324
use tracing::info;
2425

2526
mod getters;
@@ -128,7 +129,14 @@ async fn main() -> Result<()> {
128129
let addr = "0.0.0.0:3000".parse()?;
129130
info!("gRPC server listening on {}.", addr);
130131

132+
// gRPC health reporter — used by the K8s gRPC probe and GKE HealthCheckPolicy.
133+
let (health_reporter, health_service) = health_reporter();
134+
health_reporter
135+
.set_serving::<GuardianServiceServer<GuardianGrpc>>()
136+
.await;
137+
131138
let server_future = Server::builder()
139+
.add_service(health_service)
132140
.add_service(GuardianServiceServer::new(svc))
133141
.serve(addr);
134142

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# syntax=docker/dockerfile:1
2+
3+
# Plain Kubernetes (non-enclave) build of hashi-guardian.
4+
# Mirrors docker/hashi-screener/Containerfile. The sibling
5+
# docker/hashi-guardian/Containerfile is the Nitro enclave build and produces
6+
# an EIF, not a runnable OCI image — do not conflate the two.
7+
8+
FROM stagex/pallet-rust@sha256:84621c4c29330c8a969489671d46227a0a43f52cdae243f5b91e95781dbfe5ed AS pallet-rust
9+
FROM stagex/busybox@sha256:3d128909dbc8e7b6c4b8c3c31f4583f01a307907ea179934bb42c4ef056c7efd AS busybox
10+
FROM stagex/core-filesystem@sha256:da28831927652291b0fa573092fd41c8c96ca181ea224df7bff40e1833c3db13 AS core-filesystem
11+
12+
#
13+
# Deps stage: fetch and compile external dependencies (cached until Cargo.toml/Cargo.lock change)
14+
#
15+
FROM pallet-rust AS deps
16+
17+
# Shell needed by cc-based build scripts (blst, ring, etc.)
18+
COPY --from=busybox . /
19+
20+
# Copy only manifests
21+
COPY Cargo.toml Cargo.lock /src/
22+
COPY crates/hashi-guardian/Cargo.toml /src/crates/hashi-guardian/Cargo.toml
23+
COPY crates/hashi-types/Cargo.toml /src/crates/hashi-types/Cargo.toml
24+
25+
WORKDIR /src
26+
27+
# Create stub source files so cargo can resolve and compile all external deps
28+
RUN mkdir -p crates/hashi-guardian/src \
29+
&& echo 'fn main(){}' > crates/hashi-guardian/src/main.rs \
30+
&& touch crates/hashi-guardian/src/lib.rs \
31+
&& mkdir -p crates/hashi-types/src \
32+
&& touch crates/hashi-types/src/lib.rs
33+
34+
ENV TARGET=x86_64-unknown-linux-musl
35+
ENV OPENSSL_STATIC=true
36+
ENV CARGO_INCREMENTAL=0
37+
ENV RUSTFLAGS="-C target-feature=+crt-static -C relocation-model=static -C target-cpu=x86-64"
38+
39+
RUN cargo fetch
40+
RUN --network=none cargo build --release --frozen --target "$TARGET" --bin hashi-guardian
41+
42+
#
43+
# Build stage: compile with real source (only local crates recompile)
44+
#
45+
FROM deps AS build
46+
47+
ARG GIT_REVISION=unknown
48+
ENV GIT_REVISION=${GIT_REVISION}
49+
50+
COPY crates/hashi-guardian /src/crates/hashi-guardian
51+
COPY crates/hashi-types /src/crates/hashi-types
52+
53+
# Touch source files to invalidate cargo's fingerprint cache for local crates
54+
RUN find crates/ -name "*.rs" -exec touch {} +
55+
56+
RUN --network=none cargo build --release --frozen --target "$TARGET" --bin hashi-guardian
57+
58+
#
59+
# Package stage: minimal runtime image
60+
#
61+
FROM core-filesystem
62+
COPY --from=build /src/target/x86_64-unknown-linux-musl/release/hashi-guardian /usr/bin/hashi-guardian
63+
ENTRYPOINT ["/usr/bin/hashi-guardian"]

docker/hashi-guardian-k8s/build.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c), Mysten Labs, Inc.
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# Builds the hashi-guardian binary as a plain Kubernetes (non-enclave) image.
6+
#
7+
# Usage:
8+
# bash docker/hashi-guardian-k8s/build.sh # build with cache
9+
# GIT_REVISION=test bash docker/hashi-guardian-k8s/build.sh --no-cache && sha256sum out/hashi-guardian # build without cache, useful to check reproducibility
10+
11+
set -euo pipefail
12+
13+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14+
REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel)"
15+
IMAGE_NAME="${IMAGE_NAME:-hashi-guardian}"
16+
GIT_REVISION="${GIT_REVISION:-$(git -C "$REPO_ROOT" describe --always --exclude '*' --dirty --abbrev=8)}"
17+
IMAGE_TAG="${IMAGE_TAG:-${GIT_REVISION}}"
18+
OUT_DIR="${OUT_DIR:-${REPO_ROOT}/out}"
19+
20+
EXTRA_ARGS=()
21+
for arg in "$@"; do
22+
case "$arg" in
23+
--no-cache) EXTRA_ARGS+=("--no-cache") ;;
24+
*) echo "Unknown argument: $arg"; exit 1 ;;
25+
esac
26+
done
27+
28+
mkdir -p "${OUT_DIR}"
29+
30+
echo "Building ${IMAGE_NAME}:${IMAGE_TAG} (revision: ${GIT_REVISION})"
31+
32+
docker build \
33+
-f "${SCRIPT_DIR}/Containerfile" \
34+
--platform linux/amd64 \
35+
--build-arg "GIT_REVISION=${GIT_REVISION}" \
36+
--provenance=false \
37+
"${EXTRA_ARGS[@]}" \
38+
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
39+
-t "${IMAGE_NAME}:latest" \
40+
"${REPO_ROOT}"
41+
42+
echo "Successfully built ${IMAGE_NAME}:${IMAGE_TAG}"
43+
44+
# Extract the binary from the image
45+
CID=$(docker create "${IMAGE_NAME}:${IMAGE_TAG}")
46+
docker cp "${CID}:/usr/bin/hashi-guardian" "${OUT_DIR}/hashi-guardian"
47+
docker rm "${CID}" > /dev/null
48+
49+
echo ""
50+
echo "Binary: ${OUT_DIR}/hashi-guardian"
51+
echo "SHA-256: $(sha256sum "${OUT_DIR}/hashi-guardian" | awk '{print $1}')"

0 commit comments

Comments
 (0)