Skip to main content

tsoracle_driver_openraft/
log_codec.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//  https://www.tsoracle.rs
8//
9//  Copyright (c) 2026 Prisma Risk
10//
11//  Licensed under the Apache License, Version 2.0 (the "License");
12//  you may not use this file except in compliance with the License.
13//  You may obtain a copy of the License at
14//
15//      https://www.apache.org/licenses/LICENSE-2.0
16//
17//  Unless required by applicable law or agreed to in writing, software
18//  distributed under the License is distributed on an "AS IS" BASIS,
19//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20//  See the License for the specific language governing permissions and
21//  limitations under the License.
22//
23
24//! Concrete [`LogStoreCodec`] provider for this driver's [`TypeConfig`].
25//!
26//! The orphan rule forbids implementing `tsoracle_codec::VersionedCodec` for
27//! openraft's `Entry`/`Vote`/`LogId` directly (foreign trait, foreign types),
28//! so the toolkit exposes the local [`LogStoreCodec`] provider trait and this
29//! driver supplies the marker that implements it. The v4 arms postcard each
30//! openraft value whole via `encode_postcard`/`decode_postcard_exact`,
31//! byte-identical to the toolkit's [`tsoracle_openraft_toolkit::DefaultLogStoreCodec`] —
32//! `RocksdbLogStore` frames the version byte, this provider only handles the
33//! body.
34
35use tsoracle_codec::{CodecError, decode_postcard_exact, encode_postcard};
36use tsoracle_openraft_toolkit::LogStoreCodec;
37
38use crate::type_config::{OpenraftLogId, OpenraftVote, TypeConfig};
39
40/// The [`LogStoreCodec`] provider this driver wires into `RocksdbLogStore`.
41///
42/// Zero-sized type-level marker. Behavior-preserving: every body is the same
43/// whole-value postcard the store wrote before the seam existed.
44#[derive(Debug, Clone, Copy, Default)]
45pub struct OpenraftLogCodec;
46
47impl LogStoreCodec<TypeConfig> for OpenraftLogCodec {
48    fn encode_entry(
49        version: u8,
50        entry: &<TypeConfig as openraft::RaftTypeConfig>::Entry,
51    ) -> Result<Vec<u8>, CodecError> {
52        // Defense-in-depth: never serialize an AdvanceDenseBatch under a write
53        // version that predates it. The driver's proposer gate already refuses
54        // to propose one before activation; this is the second line so a gate
55        // regression fails the write here at the leader rather than committing a
56        // poison entry a pre-v6 follower cannot decode.
57        if version < tsoracle_openraft_toolkit::BATCH_WRITE_VERSION {
58            if let openraft::EntryPayload::Normal(
59                crate::log_entry::HighWaterCommand::AdvanceDenseBatch { .. },
60            ) = &entry.payload
61            {
62                return Err(CodecError::NotRepresentable { version });
63            }
64        }
65        encode_postcard(entry)
66    }
67
68    fn decode_entry(
69        _version: u8,
70        body: &[u8],
71    ) -> Result<<TypeConfig as openraft::RaftTypeConfig>::Entry, CodecError> {
72        decode_postcard_exact(body)
73    }
74
75    fn encode_vote(_version: u8, vote: &OpenraftVote) -> Result<Vec<u8>, CodecError> {
76        encode_postcard(vote)
77    }
78
79    fn decode_vote(_version: u8, body: &[u8]) -> Result<OpenraftVote, CodecError> {
80        decode_postcard_exact(body)
81    }
82
83    fn encode_log_id(_version: u8, log_id: &OpenraftLogId) -> Result<Vec<u8>, CodecError> {
84        encode_postcard(log_id)
85    }
86
87    fn decode_log_id(_version: u8, body: &[u8]) -> Result<OpenraftLogId, CodecError> {
88        decode_postcard_exact(body)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::log_entry::HighWaterCommand;
96    use tsoracle_consensus::AdvancePayload;
97
98    #[test]
99    fn entry_body_matches_raw_postcard() {
100        // The driver provider must agree byte-for-byte with raw postcard (and
101        // therefore with the toolkit DefaultLogStoreCodec), proving the seam is
102        // behavior-preserving.
103        let lid = openraft::testing::log_id::<TypeConfig>(1, 1, 1);
104        let entry: <TypeConfig as openraft::RaftTypeConfig>::Entry =
105            openraft::entry::RaftEntry::new_normal(
106                lid,
107                HighWaterCommand::Advance(AdvancePayload { at_least: 5 }),
108            );
109        let via_provider =
110            <OpenraftLogCodec as LogStoreCodec<TypeConfig>>::encode_entry(4, &entry).unwrap();
111        let raw = postcard::to_stdvec(&entry).unwrap();
112        assert_eq!(via_provider, raw);
113    }
114
115    #[test]
116    fn vote_body_matches_raw_postcard() {
117        let vote: OpenraftVote = openraft::Vote::new_committed(7, 3);
118        let via_provider =
119            <OpenraftLogCodec as LogStoreCodec<TypeConfig>>::encode_vote(4, &vote).unwrap();
120        assert_eq!(via_provider, postcard::to_stdvec(&vote).unwrap());
121        let back: OpenraftVote =
122            <OpenraftLogCodec as LogStoreCodec<TypeConfig>>::decode_vote(4, &via_provider).unwrap();
123        assert_eq!(back, vote);
124    }
125
126    #[test]
127    fn log_id_body_matches_raw_postcard() {
128        let log_id: OpenraftLogId = openraft::testing::log_id::<TypeConfig>(2, 1, 9);
129        let via_provider =
130            <OpenraftLogCodec as LogStoreCodec<TypeConfig>>::encode_log_id(4, &log_id).unwrap();
131        assert_eq!(via_provider, postcard::to_stdvec(&log_id).unwrap());
132    }
133
134    #[test]
135    fn encode_entry_rejects_batch_below_batch_version() {
136        use crate::log_entry::{DenseAdvance, HighWaterCommand};
137        use tsoracle_openraft_toolkit::{BATCH_WRITE_VERSION, DENSE_WRITE_VERSION};
138        let lid = openraft::testing::log_id::<TypeConfig>(1, 1, 1);
139        let entry: <TypeConfig as openraft::RaftTypeConfig>::Entry =
140            openraft::entry::RaftEntry::new_normal(
141                lid,
142                HighWaterCommand::AdvanceDenseBatch {
143                    entries: vec![DenseAdvance {
144                        key: tsoracle_core::SeqKey::try_new("k").unwrap(),
145                        count: 1,
146                    }],
147                },
148            );
149        // Below BATCH_WRITE_VERSION: refused, no bytes produced. Tested at both
150        // the baseline and the just-below-threshold version to pin the whole
151        // pre-v6 range, not a single point.
152        use tsoracle_openraft_toolkit::BASELINE_WRITE_VERSION;
153        for below in [BASELINE_WRITE_VERSION, DENSE_WRITE_VERSION] {
154            assert!(
155                matches!(
156                    <OpenraftLogCodec as LogStoreCodec<TypeConfig>>::encode_entry(below, &entry),
157                    Err(CodecError::NotRepresentable { .. })
158                ),
159                "AdvanceDenseBatch must be refused at write version {below}",
160            );
161        }
162        // At BATCH_WRITE_VERSION: allowed.
163        assert!(
164            <OpenraftLogCodec as LogStoreCodec<TypeConfig>>::encode_entry(
165                BATCH_WRITE_VERSION,
166                &entry
167            )
168            .is_ok()
169        );
170    }
171}