@@ -25,4 +25,127 @@ TEST(LTC_share_test, Init)
2525 std::cout << rshare.type << std::endl;
2626
2727 auto share = ltc::load_share (rshare, NetService{" 0.0.0.0" , 0 });
28- }
28+ }
29+ // ---------------------------------------------------------------------------
30+ // Three-tier v35-phase wire-compat KATs (KAT-form of soak gates ST-2/ST-3,
31+ // "Standing CI reduction", v35-threetier-wirecompat-design.md §5.2).
32+ //
33+ // FENCED, NON-CIRCULAR: expected bytes are hand-transcribed from the jtoomim /
34+ // frstrtr/p2pool-merged-v36 oracle wire semantics (VarIntType == Bitcoin
35+ // CompactSize, VarStrType == CompactSize-prefixed bytes, FixedStrType(N) == N
36+ // raw bytes no prefix) -- NOT a second read of the c2pool SUT serializer. A
37+ // field-order or encoding drift in ltc/share_types.hpp that silently diverges
38+ // from what a T-A (jtoomim) node round-trips fails HERE.
39+ // ST-2: the v36 vote rides desired_version = VarInt(36) == single byte 0x24
40+ // (design §1.1); pin the CompactSize codec that carries it + the
41+ // share_type outer type tag (v35 mint class 35 -> "23", v36 -> "24").
42+ // ST-3: zero v36-byte leakage into the v35 codec -- the v35 HashLinkType has
43+ // NO extra_data slot (FixedStr(0), zero bytes), whereas the v36
44+ // V36HashLinkType inserts a VarStr(extra_data) the v35 wire never carries.
45+ // ---------------------------------------------------------------------------
46+ #include < span>
47+ #include < cstdint>
48+ #include < impl/ltc/share_types.hpp>
49+
50+ namespace {
51+
52+ // FixedStrType(32) operand: raw bytes 0x00..0x1f (oracle transcription).
53+ const std::string ST_STATE_HEX =
54+ " 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" ;
55+
56+ std::string st_state_raw ()
57+ {
58+ std::string s;
59+ for (int i = 0 ; i < 32 ; ++i) s.push_back (static_cast <char >(i));
60+ return s;
61+ }
62+
63+ std::string st_to_hex (std::span<std::byte> sp)
64+ {
65+ static const char * d = " 0123456789abcdef" ;
66+ std::string s;
67+ s.reserve (sp.size () * 2 );
68+ for (std::byte b : sp) {
69+ auto v = static_cast <unsigned >(b);
70+ s.push_back (d[(v >> 4 ) & 0xf ]);
71+ s.push_back (d[v & 0xf ]);
72+ }
73+ return s;
74+ }
75+
76+ } // namespace
77+
78+ // ST-3: v35 hash_link carries NO extra_data; v36 codec adds a VarStr slot the
79+ // v35 wire never has -> any v36-byte leak into a v35 share is structural, not policy.
80+ TEST (LTC_threetier_wirecompat, ST3_no_v36_hash_link_leak)
81+ {
82+ // v35 lineage (PaddingBugfixShare): state(32) || VarInt(len); FixedStr(0)
83+ // extra_data contributes ZERO bytes. length = 300 -> CompactSize "fd2c01".
84+ {
85+ ltc::HashLinkType hl;
86+ hl.m_state = FixedStrType<32 >(st_state_raw ());
87+ hl.m_length = 300 ;
88+ PackStream ss;
89+ ss << hl;
90+ EXPECT_EQ (st_to_hex (ss.get_span ()), ST_STATE_HEX + " fd2c01" );
91+ EXPECT_EQ (ss.get_span ().size (), 35u ); // 32 + 3, nothing between state and len
92+ }
93+ // v36 struct with EMPTY extra_data still emits the VarStr length prefix "00"
94+ // -> exactly one byte more than the v35 wire; the leak is detectable.
95+ {
96+ ltc::V36HashLinkType hl;
97+ hl.m_state = FixedStrType<32 >(st_state_raw ());
98+ hl.m_extra_data = BaseScript (std::vector<unsigned char >{});
99+ hl.m_length = 300 ;
100+ PackStream ss;
101+ ss << hl;
102+ EXPECT_EQ (st_to_hex (ss.get_span ()), ST_STATE_HEX + " 00" + " fd2c01" );
103+ EXPECT_EQ (ss.get_span ().size (), 36u );
104+ }
105+ // v36 struct with non-empty extra_data: state || VarStr(aabbcc) || VarInt(len).
106+ {
107+ ltc::V36HashLinkType hl;
108+ hl.m_state = FixedStrType<32 >(st_state_raw ());
109+ hl.m_extra_data = BaseScript (std::vector<unsigned char >{0xaa , 0xbb , 0xcc });
110+ hl.m_length = 300 ;
111+ PackStream ss;
112+ ss << hl;
113+ EXPECT_EQ (st_to_hex (ss.get_span ()), ST_STATE_HEX + " 03aabbcc" + " fd2c01" );
114+ }
115+ }
116+
117+ // ST-2: the v36 vote value (36) is the single byte 0x24 under the CompactSize
118+ // codec that carries desired_version; the share_type outer type tag agrees.
119+ TEST (LTC_threetier_wirecompat, ST2_desired_version_vote_byte)
120+ {
121+ // VarInt == Bitcoin CompactSize: pin the vote value 36 -> "24" plus the
122+ // boundary vectors, via the same VarInt codec desired_version uses.
123+ struct { uint64_t v; const char * hex; } vec[] = {
124+ {35 , " 23" }, {36 , " 24" }, {252 , " fc" },
125+ {253 , " fdfd00" }, {300 , " fd2c01" }, {65536 , " fe00000100" },
126+ };
127+ for (auto & t : vec) {
128+ ltc::HashLinkType hl;
129+ hl.m_state = FixedStrType<32 >(st_state_raw ());
130+ hl.m_length = t.v ;
131+ PackStream ss;
132+ ss << hl;
133+ EXPECT_EQ (st_to_hex (ss.get_span ()), ST_STATE_HEX + t.hex )
134+ << " VarInt(" << t.v << " ) != oracle CompactSize" ;
135+ }
136+ // share_type outer wrapper: VarInt(type) || VarStr(contents).
137+ // v35 mint class 35 -> "23"; v36 class -> "24" (same single-byte CompactSize
138+ // as the vote value 36) -- the tier discriminator on the wire.
139+ {
140+ chain::RawShare rs (35 , BaseScript (std::vector<unsigned char >{0xde , 0xad , 0xbe , 0xef }));
141+ PackStream ss;
142+ ss << rs;
143+ EXPECT_EQ (st_to_hex (ss.get_span ()), std::string (" 23" ) + " 04deadbeef" );
144+ }
145+ {
146+ chain::RawShare rs (36 , BaseScript (std::vector<unsigned char >{0xde , 0xad , 0xbe , 0xef }));
147+ PackStream ss;
148+ ss << rs;
149+ EXPECT_EQ (st_to_hex (ss.get_span ()), std::string (" 24" ) + " 04deadbeef" );
150+ }
151+ }
0 commit comments