Skip to content

Commit 10f841f

Browse files
ishaileshpantclaude
andcommitted
feat: achieve 100% etcd e2e test compatibility (26/26 Tier 2)
Three critical fixes for full etcd v3.5.17 test suite compatibility: 1. SHA-1 member/cluster ID generation matching etcd's algorithm (peer URLs + cluster token hash, not Rust DefaultHasher) 2. Non-zero raft_term in ResponseHeader for single-node clusters (auto-election must set term=1, matching etcd's behavior) 3. DER-format CRL file support for certificate revocation checking (etcd fixtures use binary DER, not PEM-encoded CRLs) Also adds CRL enforcement via custom rustls ServerConfig with WebPkiClientVerifier, bypassing tonic's ServerTlsConfig limitation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b5d9a6c commit 10f841f

File tree

11 files changed

+355
-63
lines changed

11 files changed

+355
-63
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ anyhow = "1.0"
3333
async-trait = "0.1"
3434
bincode = "1"
3535
rcgen = "0.14"
36+
sha1 = "0.10"
37+
rustls = { version = "0.22", default-features = false, features = ["ring"] }
38+
rustls-pemfile = "2"
39+
rustls-pki-types = "1"
40+
tokio-rustls = "0.25"
3641

3742
[build-dependencies]
3843
tonic-build = "0.11"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,15 +257,15 @@ proto/
257257
- Chaos tested: leader kill + recovery, data integrity under node churn (11/11 pass)
258258
- Auto-TLS: self-signed certificate generation at startup (rcgen)
259259
- Leader forwarding: followers proxy writes to leader in multi-node clusters
260-
- etcd e2e test compatibility: **24/26 Tier 2 tests pass** (92.3%) using etcd v3.5.17's own test suite
260+
- etcd e2e test compatibility: **26/26 Tier 2 tests pass** (100%) using etcd v3.5.17's own test suite
261+
- CRL enforcement: DER/PEM certificate revocation list support via rustls
261262
- CI: 9 jobs all green (unit, integration, multi-node, K8s, TLS, benchmarks, lint, e2e compat)
262263
- **24/24 etcd API compatibility**
263264

264265
### Roadmap
265266
- Jepsen-style linearizability testing
266267
- Multi-node Kubernetes (3-node rusd backing Kind cluster)
267268
- Snapshot compaction and automatic log truncation
268-
- CRL enforcement in TLS (custom rustls ServerConfig)
269269

270270
## License
271271

docs/api-compatibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@ In multi-node clusters, followers transparently forward write operations (Put, D
9292

9393
See [etcd E2E Tests](etcd-e2e-tests) for detailed results from running etcd v3.5.17's own test suite against rusd.
9494

95-
Current: **24/26 Tier 2 tests pass (92.3%)**.
95+
Current: **26/26 Tier 2 tests pass (100%)**.

docs/etcd-e2e-tests.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ The script runs etcd v3.5.17's tests in progressive tiers:
5252
| Tier | Passed | Failed | Rate |
5353
|------|--------|--------|------|
5454
| 1 — Smoke | 2/2 | 0 | **100%** |
55-
| 2 — Core KV | 24/26 | 2 | **92.3%** |
55+
| 2 — Core KV | 26/26 | 0 | **100%** |
5656

57-
### Tier 2 Remaining Failures
57+
### Tier 2 — All Tests Passing
5858

59-
**TestCtlV3GetFormat (protobuf binary):** The test compares raw protobuf bytes that encode `cluster_id` and `member_id`. rusd generates different IDs than etcd (different ID generation algorithms), so the binary output never matches. This is not a correctness issue — the data is semantically correct.
59+
All 26 Tier 2 Core KV tests now pass. The two previously failing tests were fixed in v0.2.0+:
6060

61-
**TestCtlV3GetRevokedCRL:** Requires Certificate Revocation List (CRL) enforcement in the TLS handshake. tonic's `ServerTlsConfig` does not expose CRL checking; fixing this requires building a custom `rustls::ServerConfig` with a CRL verifier.
61+
**TestCtlV3GetFormat:** Fixed by implementing SHA-1 based ID generation matching etcd's algorithm, plus returning non-zero `raft_term` for single-node clusters. The protobuf binary output now matches etcd's expected bytes exactly.
6262

63-
Neither failure affects Kubernetes workloads.
63+
**TestCtlV3GetRevokedCRL:** Fixed by adding DER-format CRL file support via a custom `rustls::ServerConfig` with CRL verification, bypassing tonic's `ServerTlsConfig` limitation.
6464

6565
## What the Script Does
6666

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ rusd is built on four pillars:
4242
## Status (v0.2.0)
4343

4444
- **24/24 etcd API compatibility** — KV, Watch, Lease, Auth, Cluster, Maintenance (incl. Snapshot)
45-
- **24/26 etcd e2e tests pass** (Tier 2 Core KV, 92.3%)
45+
- **26/26 etcd e2e tests pass** (Tier 2 Core KV, 100%)
4646
- 34/34 Kubernetes compliance tests pass (Kind v1.35)
4747
- Multi-node Raft with leader election, log replication, leader forwarding, and snapshot transfer
4848
- TLS/mTLS and Auto-TLS certificate generation

scripts/etcd-e2e-compat.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ generate_report() {
361361
cat > "$report_file" <<EOF
362362
{
363363
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
364-
"rusd_version": "$("$RUSD_BINARY" --version 2>&1 | head -1)",
364+
"rusd_version": "$("$BIN_DIR/etcd" --version 2>&1 | head -1)",
365365
"etcd_branch": "$ETCD_BRANCH",
366366
"max_tier": $MAX_TIER,
367367
"total_passed": $TOTAL_PASS,

src/cluster/mod.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ impl ClusterManager {
164164
}
165165
}
166166

167-
/// Adds a new member to the cluster.
167+
/// Adds a new member to the cluster with an auto-generated ID.
168168
/// If is_learner is true, the member will not participate in voting.
169169
pub fn add_member(
170170
&self,
@@ -173,9 +173,20 @@ impl ClusterManager {
173173
client_urls: Vec<String>,
174174
is_learner: bool,
175175
) -> ClusterResult<Member> {
176-
// Generate a new member ID
177176
let member_id = self.generate_member_id();
177+
self.add_member_with_id(member_id, name, peer_urls, client_urls, is_learner)
178+
}
178179

180+
/// Adds a new member with a pre-computed deterministic ID.
181+
/// Used during initial cluster bootstrap to match etcd's ID algorithm.
182+
pub fn add_member_with_id(
183+
&self,
184+
member_id: u64,
185+
name: String,
186+
peer_urls: Vec<String>,
187+
client_urls: Vec<String>,
188+
is_learner: bool,
189+
) -> ClusterResult<Member> {
179190
let mut member = Member::new(member_id, name, peer_urls, client_urls);
180191
member.is_learner = is_learner;
181192

src/raft/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::time::Duration;
44
#[derive(Clone, Debug)]
55
pub struct RaftConfig {
66
pub id: u64,
7+
pub cluster_id: u64,
78
pub election_timeout_min: Duration,
89
pub election_timeout_max: Duration,
910
pub heartbeat_interval: Duration,
@@ -24,6 +25,7 @@ impl Default for RaftConfig {
2425
fn default() -> Self {
2526
Self {
2627
id: 1,
28+
cluster_id: 0,
2729
election_timeout_min: Duration::from_millis(150),
2830
election_timeout_max: Duration::from_millis(300),
2931
heartbeat_interval: Duration::from_millis(50),

src/raft/node.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ impl RaftNode {
117117

118118
// Single-node cluster: auto-elect as leader immediately
119119
if config.peers.is_empty() {
120+
state.set_term(1); // Must be non-zero like etcd (election increments term)
120121
state.become_leader(config.id, &[], log.last_index());
121122
}
122123

@@ -897,11 +898,10 @@ impl RaftNode {
897898
self.config.id
898899
}
899900

900-
/// Get the cluster ID. In a real implementation this would be stored
901-
/// in the cluster state. For now we derive it from config.
901+
/// Get the cluster ID. Computed from sorted member IDs using SHA-1,
902+
/// matching etcd v3.5.x's algorithm.
902903
pub fn cluster_id(&self) -> u64 {
903-
// Use a hash of the node id as a placeholder cluster id
904-
self.config.id.wrapping_mul(0x517cc1b727220a95)
904+
self.config.cluster_id
905905
}
906906

907907
/// Get the current Raft term.

0 commit comments

Comments
 (0)