using NATS.Server.Raft; namespace NATS.Server.Raft.Tests.Raft; /// /// Binary wire format encoding/decoding tests for all RAFT RPC types. /// These validate exact byte-for-byte fidelity with Go's raft.go encoding. /// Go reference: golang/nats-server/server/raft.go lines 2662-2796 (AppendEntry), /// 4560-4768 (vote types). /// public class RaftBinaryWireFormatTests { // --------------------------------------------------------------------------- // VoteRequest // --------------------------------------------------------------------------- // Go: server/raft.go:4560-4568 — voteRequest.encode() // Go: server/raft.go:4571-4583 — decodeVoteRequest() [Fact] public void VoteRequest_round_trip_encode_decode() { var original = new RaftVoteRequestWire( Term: 7, LastTerm: 3, LastIndex: 42, CandidateId: "peer0001"); var encoded = original.Encode(); encoded.Length.ShouldBe(RaftWireConstants.VoteRequestLen); // 32 bytes var decoded = RaftVoteRequestWire.Decode(encoded); decoded.Term.ShouldBe(7UL); decoded.LastTerm.ShouldBe(3UL); decoded.LastIndex.ShouldBe(42UL); decoded.CandidateId.ShouldBe("peer0001"); } [Fact] public void VoteRequest_bytes_are_little_endian() { var req = new RaftVoteRequestWire(Term: 1, LastTerm: 0, LastIndex: 0, CandidateId: ""); var bytes = req.Encode(); // term = 1 in little-endian: [1, 0, 0, 0, 0, 0, 0, 0] // Go: server/raft.go:4563 — le.PutUint64(buf[0:], vr.term) bytes[0].ShouldBe((byte)1); bytes[1].ShouldBe((byte)0); } [Fact] public void VoteRequest_zero_values_encode_to_zeroed_buffer() { var req = new RaftVoteRequestWire(Term: 0, LastTerm: 0, LastIndex: 0, CandidateId: ""); var bytes = req.Encode(); bytes.Length.ShouldBe(32); bytes.ShouldAllBe(b => b == 0); } [Fact] public void VoteRequest_large_term_round_trips() { var req = new RaftVoteRequestWire( Term: ulong.MaxValue, LastTerm: ulong.MaxValue - 1, LastIndex: ulong.MaxValue - 2, CandidateId: "node1234"); var decoded = RaftVoteRequestWire.Decode(req.Encode()); decoded.Term.ShouldBe(ulong.MaxValue); decoded.LastTerm.ShouldBe(ulong.MaxValue - 1); decoded.LastIndex.ShouldBe(ulong.MaxValue - 2); decoded.CandidateId.ShouldBe("node1234"); } [Fact] public void VoteRequest_short_buffer_throws_ArgumentException() { var shortBuffer = new byte[RaftWireConstants.VoteRequestLen - 1]; Should.Throw(() => RaftVoteRequestWire.Decode(shortBuffer)); } [Fact] public void VoteRequest_long_buffer_throws_ArgumentException() { var longBuffer = new byte[RaftWireConstants.VoteRequestLen + 1]; Should.Throw(() => RaftVoteRequestWire.Decode(longBuffer)); } [Fact] public void VoteRequest_candidate_id_truncated_to_8_bytes() { // IDs longer than 8 chars are silently truncated (Go copy semantics). // Go: server/raft.go:4566 — copy(buf[24:24+idLen], vr.candidate) var req = new RaftVoteRequestWire( Term: 1, LastTerm: 0, LastIndex: 0, CandidateId: "abcdefghXXXXXXXX"); // 16 chars; only first 8 kept var bytes = req.Encode(); // Check that the ID field contains only the first 8 chars. var idBytes = bytes[24..32]; System.Text.Encoding.ASCII.GetString(idBytes).ShouldBe("abcdefgh"); } [Fact] public void VoteRequest_short_candidate_id_zero_padded() { var req = new RaftVoteRequestWire( Term: 1, LastTerm: 0, LastIndex: 0, CandidateId: "abc"); var bytes = req.Encode(); bytes[27].ShouldBe((byte)0); // byte 3..7 should be zero bytes[28].ShouldBe((byte)0); // Decode should recover the original 3-char ID. var decoded = RaftVoteRequestWire.Decode(bytes); decoded.CandidateId.ShouldBe("abc"); } // --------------------------------------------------------------------------- // VoteResponse // --------------------------------------------------------------------------- // Go: server/raft.go:4739-4751 — voteResponse.encode() // Go: server/raft.go:4753-4762 — decodeVoteResponse() [Fact] public void VoteResponse_granted_true_round_trip() { var resp = new RaftVoteResponseWire(Term: 5, PeerId: "peer0002", Granted: true); var decoded = RaftVoteResponseWire.Decode(resp.Encode()); decoded.Term.ShouldBe(5UL); decoded.PeerId.ShouldBe("peer0002"); decoded.Granted.ShouldBeTrue(); decoded.Empty.ShouldBeFalse(); } [Fact] public void VoteResponse_granted_false_round_trip() { var resp = new RaftVoteResponseWire(Term: 3, PeerId: "peer0003", Granted: false); var decoded = RaftVoteResponseWire.Decode(resp.Encode()); decoded.Granted.ShouldBeFalse(); decoded.PeerId.ShouldBe("peer0003"); } [Fact] public void VoteResponse_empty_flag_round_trip() { // Go: server/raft.go:4746-4748 — buf[16] |= 2 when empty var resp = new RaftVoteResponseWire(Term: 1, PeerId: "p1", Granted: false, Empty: true); var decoded = RaftVoteResponseWire.Decode(resp.Encode()); decoded.Empty.ShouldBeTrue(); decoded.Granted.ShouldBeFalse(); } [Fact] public void VoteResponse_both_flags_set() { var resp = new RaftVoteResponseWire(Term: 1, PeerId: "p1", Granted: true, Empty: true); var bytes = resp.Encode(); // Go: server/raft.go:4744-4748 — bit 0 = granted, bit 1 = empty (bytes[16] & 1).ShouldBe(1); // granted (bytes[16] & 2).ShouldBe(2); // empty var decoded = RaftVoteResponseWire.Decode(bytes); decoded.Granted.ShouldBeTrue(); decoded.Empty.ShouldBeTrue(); } [Fact] public void VoteResponse_fixed_17_bytes() { var resp = new RaftVoteResponseWire(Term: 10, PeerId: "peer0001", Granted: true); resp.Encode().Length.ShouldBe(RaftWireConstants.VoteResponseLen); // 17 } [Fact] public void VoteResponse_short_buffer_throws_ArgumentException() { var shortBuffer = new byte[RaftWireConstants.VoteResponseLen - 1]; Should.Throw(() => RaftVoteResponseWire.Decode(shortBuffer)); } [Fact] public void VoteResponse_peer_id_truncated_to_8_bytes() { // Go: server/raft.go:4743 — copy(buf[8:], vr.peer) var resp = new RaftVoteResponseWire( Term: 1, PeerId: "longpeernamethatexceeds8chars", Granted: true); var bytes = resp.Encode(); // Bytes [8..15] hold the peer ID — only first 8 chars fit. var idBytes = bytes[8..16]; System.Text.Encoding.ASCII.GetString(idBytes).ShouldBe("longpeer"); } // --------------------------------------------------------------------------- // AppendEntry — zero entries // --------------------------------------------------------------------------- // Go: server/raft.go:2662-2711 — appendEntry.encode() // Go: server/raft.go:2714-2746 — decodeAppendEntry() [Fact] public void AppendEntry_zero_entries_round_trip() { var ae = new RaftAppendEntryWire( LeaderId: "lead0001", Term: 10, Commit: 8, PrevTerm: 9, PrevIndex: 7, Entries: [], LeaderTerm: 0); var encoded = ae.Encode(); // Base length + 1-byte uvarint(0) for lterm. // Go: server/raft.go:2681-2683 — lterm uvarint always appended encoded.Length.ShouldBe(RaftWireConstants.AppendEntryBaseLen + 1); var decoded = RaftAppendEntryWire.Decode(encoded); decoded.LeaderId.ShouldBe("lead0001"); decoded.Term.ShouldBe(10UL); decoded.Commit.ShouldBe(8UL); decoded.PrevTerm.ShouldBe(9UL); decoded.PrevIndex.ShouldBe(7UL); decoded.Entries.Count.ShouldBe(0); decoded.LeaderTerm.ShouldBe(0UL); } [Fact] public void AppendEntry_base_layout_at_correct_offsets() { // Go: server/raft.go:2693-2698 — exact layout: // [0..7]=leader [8..15]=term [16..23]=commit [24..31]=pterm [32..39]=pindex [40..41]=entryCount var ae = new RaftAppendEntryWire( LeaderId: "AAAAAAAA", // 0x41 x 8 Term: 1, Commit: 2, PrevTerm: 3, PrevIndex: 4, Entries: []); var bytes = ae.Encode(); // leader bytes bytes[0].ShouldBe((byte)'A'); bytes[7].ShouldBe((byte)'A'); // term = 1 LE bytes[8].ShouldBe((byte)1); bytes[9].ShouldBe((byte)0); // commit = 2 LE bytes[16].ShouldBe((byte)2); // entryCount = 0 bytes[40].ShouldBe((byte)0); bytes[41].ShouldBe((byte)0); } // --------------------------------------------------------------------------- // AppendEntry — single entry // --------------------------------------------------------------------------- [Fact] public void AppendEntry_single_entry_round_trip() { var data = "hello world"u8.ToArray(); var entry = new RaftEntryWire(RaftEntryType.Normal, data); var ae = new RaftAppendEntryWire( LeaderId: "leader01", Term: 5, Commit: 3, PrevTerm: 4, PrevIndex: 2, Entries: [entry]); var decoded = RaftAppendEntryWire.Decode(ae.Encode()); decoded.Entries.Count.ShouldBe(1); decoded.Entries[0].Type.ShouldBe(RaftEntryType.Normal); decoded.Entries[0].Data.ShouldBe(data); } [Fact] public void AppendEntry_entry_size_field_equals_1_plus_data_length() { // Go: server/raft.go:2702 — le.AppendUint32(buf, uint32(1+len(e.Data))) var data = new byte[10]; var entry = new RaftEntryWire(RaftEntryType.PeerState, data); var ae = new RaftAppendEntryWire( LeaderId: "ld", Term: 1, Commit: 0, PrevTerm: 0, PrevIndex: 0, Entries: [entry]); var bytes = ae.Encode(); // Entry starts at offset 42 (appendEntryBaseLen). // First 4 bytes are the uint32 size = 1 + 10 = 11. var sizeField = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(42)); sizeField.ShouldBe(11u); // Byte at offset 46 is the entry type. bytes[46].ShouldBe((byte)RaftEntryType.PeerState); } // --------------------------------------------------------------------------- // AppendEntry — multiple entries // --------------------------------------------------------------------------- [Fact] public void AppendEntry_multiple_entries_round_trip() { var entries = new RaftEntryWire[] { new(RaftEntryType.Normal, "first"u8.ToArray()), new(RaftEntryType.AddPeer, "second"u8.ToArray()), new(RaftEntryType.RemovePeer, "third"u8.ToArray()), }; var ae = new RaftAppendEntryWire( LeaderId: "lead0001", Term: 20, Commit: 15, PrevTerm: 19, PrevIndex: 14, Entries: entries); var decoded = RaftAppendEntryWire.Decode(ae.Encode()); decoded.Entries.Count.ShouldBe(3); decoded.Entries[0].Type.ShouldBe(RaftEntryType.Normal); decoded.Entries[0].Data.ShouldBe("first"u8.ToArray()); decoded.Entries[1].Type.ShouldBe(RaftEntryType.AddPeer); decoded.Entries[1].Data.ShouldBe("second"u8.ToArray()); decoded.Entries[2].Type.ShouldBe(RaftEntryType.RemovePeer); decoded.Entries[2].Data.ShouldBe("third"u8.ToArray()); } [Fact] public void AppendEntry_50_entries_preserve_order() { var entries = Enumerable.Range(0, 50) .Select(i => new RaftEntryWire(RaftEntryType.Normal, [(byte)i])) .ToArray(); var ae = new RaftAppendEntryWire( LeaderId: "lead0001", Term: 1, Commit: 0, PrevTerm: 0, PrevIndex: 0, Entries: entries); var decoded = RaftAppendEntryWire.Decode(ae.Encode()); decoded.Entries.Count.ShouldBe(50); for (var i = 0; i < 50; i++) decoded.Entries[i].Data[0].ShouldBe((byte)i); } [Fact] public void AppendEntry_entry_with_empty_data_round_trips() { var entry = new RaftEntryWire(RaftEntryType.LeaderTransfer, []); var ae = new RaftAppendEntryWire( LeaderId: "ld", Term: 1, Commit: 0, PrevTerm: 0, PrevIndex: 0, Entries: [entry]); var decoded = RaftAppendEntryWire.Decode(ae.Encode()); decoded.Entries.Count.ShouldBe(1); decoded.Entries[0].Data.Length.ShouldBe(0); decoded.Entries[0].Type.ShouldBe(RaftEntryType.LeaderTransfer); } // --------------------------------------------------------------------------- // AppendEntry — leaderTerm (uvarint tail) // --------------------------------------------------------------------------- // Go: server/raft.go:2709 — buf = append(buf, lterm...) // Go: server/raft.go:2740-2743 — if lterm, n := binary.Uvarint(msg[ri:]); n > 0 ... [Theory] [InlineData(0UL)] [InlineData(1UL)] [InlineData(127UL)] [InlineData(128UL)] [InlineData(ulong.MaxValue)] public void AppendEntry_leader_term_uvarint_round_trips(ulong lterm) { var ae = new RaftAppendEntryWire( LeaderId: "lead0001", Term: 5, Commit: 3, PrevTerm: 4, PrevIndex: 2, Entries: [], LeaderTerm: lterm); var decoded = RaftAppendEntryWire.Decode(ae.Encode()); decoded.LeaderTerm.ShouldBe(lterm); } // --------------------------------------------------------------------------- // AppendEntry — error cases // --------------------------------------------------------------------------- [Fact] public void AppendEntry_short_buffer_throws_ArgumentException() { // Buffer smaller than appendEntryBaseLen (42 bytes). var shortBuffer = new byte[RaftWireConstants.AppendEntryBaseLen - 1]; Should.Throw(() => RaftAppendEntryWire.Decode(shortBuffer)); } // --------------------------------------------------------------------------- // AppendEntryResponse // --------------------------------------------------------------------------- // Go: server/raft.go:2777-2794 — appendEntryResponse.encode() // Go: server/raft.go:2799-2817 — decodeAppendEntryResponse() [Fact] public void AppendEntryResponse_success_true_round_trip() { var resp = new RaftAppendEntryResponseWire( Term: 12, Index: 99, PeerId: "follwr01", Success: true); var encoded = resp.Encode(); encoded.Length.ShouldBe(RaftWireConstants.AppendEntryResponseLen); // 25 var decoded = RaftAppendEntryResponseWire.Decode(encoded); decoded.Term.ShouldBe(12UL); decoded.Index.ShouldBe(99UL); decoded.PeerId.ShouldBe("follwr01"); decoded.Success.ShouldBeTrue(); } [Fact] public void AppendEntryResponse_success_false_round_trip() { var resp = new RaftAppendEntryResponseWire( Term: 3, Index: 1, PeerId: "follwr02", Success: false); var decoded = RaftAppendEntryResponseWire.Decode(resp.Encode()); decoded.Success.ShouldBeFalse(); decoded.PeerId.ShouldBe("follwr02"); } [Fact] public void AppendEntryResponse_success_byte_is_0_or_1() { // Go: server/raft.go:2815 — ar.success = msg[24] == 1 var yes = new RaftAppendEntryResponseWire(Term: 1, Index: 0, PeerId: "p", Success: true); var no = new RaftAppendEntryResponseWire(Term: 1, Index: 0, PeerId: "p", Success: false); yes.Encode()[24].ShouldBe((byte)1); no.Encode()[24].ShouldBe((byte)0); } [Fact] public void AppendEntryResponse_layout_at_correct_offsets() { // Go: server/raft.go:2784-2792 — exact layout: // [0..7]=term [8..15]=index [16..23]=peer [24]=success var resp = new RaftAppendEntryResponseWire( Term: 1, Index: 2, PeerId: "BBBBBBBB", Success: true); var bytes = resp.Encode(); bytes[0].ShouldBe((byte)1); // term LE bytes[8].ShouldBe((byte)2); // index LE bytes[16].ShouldBe((byte)'B'); // peer[0] bytes[24].ShouldBe((byte)1); // success = 1 } [Fact] public void AppendEntryResponse_short_buffer_throws_ArgumentException() { var shortBuffer = new byte[RaftWireConstants.AppendEntryResponseLen - 1]; Should.Throw(() => RaftAppendEntryResponseWire.Decode(shortBuffer)); } [Fact] public void AppendEntryResponse_long_buffer_throws_ArgumentException() { var longBuffer = new byte[RaftWireConstants.AppendEntryResponseLen + 1]; Should.Throw(() => RaftAppendEntryResponseWire.Decode(longBuffer)); } [Fact] public void AppendEntryResponse_peer_id_truncated_to_8_bytes() { // Go: server/raft.go:2787 — copy(buf[16:16+idLen], ar.peer) var resp = new RaftAppendEntryResponseWire( Term: 1, Index: 0, PeerId: "verylongpeeridthatexceeds8", Success: false); var bytes = resp.Encode(); var idBytes = bytes[16..24]; System.Text.Encoding.ASCII.GetString(idBytes).ShouldBe("verylong"); } // --------------------------------------------------------------------------- // Wire constant values // --------------------------------------------------------------------------- [Fact] public void Wire_constants_match_go_definitions() { // Go: server/raft.go:4558 — voteRequestLen = 24 + idLen = 32 RaftWireConstants.VoteRequestLen.ShouldBe(32); // Go: server/raft.go:4737 — voteResponseLen = 8 + 8 + 1 = 17 RaftWireConstants.VoteResponseLen.ShouldBe(17); // Go: server/raft.go:2660 — appendEntryBaseLen = idLen + 4*8 + 2 = 42 RaftWireConstants.AppendEntryBaseLen.ShouldBe(42); // Go: server/raft.go:2757 — appendEntryResponseLen = 24 + 1 = 25 RaftWireConstants.AppendEntryResponseLen.ShouldBe(25); // Go: server/raft.go:2756 — idLen = 8 RaftWireConstants.IdLen.ShouldBe(8); } }