Bidirectional, byte-exact validation of the IEEE 2664-2024 § 5.5.10 BUFFER BLOCK wire format across gsfapi (C#) and pyapi (Python). Each test set is three deterministic buffer blocks; the receiver hexdumps any mismatch.
| Direction | Sender | Receiver | What it validates |
|---|---|---|---|
| gsfapi → pyapi | csharp-publisher/ | python-subscriber/ | pyapi _handle_bufferblock parses the IEEE flags byte and GZip-decompresses |
| pyapi → gsfapi | python-publisher/ | csharp-subscriber/ | pyapi SubscriberConnection.send_buffer_block emits an IEEE-aligned frame |
| gsfapi → gsfapi | csharp-publisher/ | csharp-subscriber/ | gsfapi round-trip (--publisher=csharp) after the over-allocation + TSSC fixes |
Per IEEE Std 2664-2024 § 5.5.10 Figure 34, with the gsfapi SIGNAL INDEX extension at +5
(the IEEE figure has no in-band signal identifier; including the runtime ID lets one
subscription carry buffer blocks for multiple signals):
+0 uint32 SEQUENCE VALUE (big-endian, ack tracker)
+4 byte BUFFER BLOCK FLAGS (Table 8: 0x01 RequireConfirmation, 0x08 Compressed, 0x10 CacheIndex)
+5 int32 SIGNAL INDEX (big-endian, runtime ID)
+9 byte[] PAYLOAD (GZip-compressed when COMPRESSED flag set)
GZip is the IEEE-mandated default for buffer-block payload compression (Annex /
BufferBlockPayloadCompressionAlgorithms, default Gzip, Gzip).
Per IEEE 2664-2024 Table 8 bit 0x04, BufferBlock payloads can be AES-encrypted over UDP using
the cipher key sets negotiated by ROTATE CIPHER KEYS / UPDATE CIPHER KEYS. The wire layout
preserves SEQUENCE VALUE and FLAGS cleartext (so the receiver can dedup retransmits and
choose the cipher key set before decryption); SIGNAL INDEX and PAYLOAD are encrypted
together as a single AES-CBC block, mirroring the DATA PACKET pattern where
MEASUREMENT COUNT + PAYLOAD are encrypted while only FLAGS stays cleartext.
When both COMPRESSED and KEY INDEX apply, compression happens first per IEEE Std 2664-2024 § 5.5.10 ("If both UDP compression and encryption is enabled, data is compressed first and then encrypted") - so the receiver decrypts before decompressing.
Implementation status:
- gsfapi publisher: encrypts BufferBlock payloads in
SendClientResponse, setsKEY INDEXfromconnection.CipherIndexwhenKeyIVsis non-null. Parallel to the existingCipherIndexhandling for DATA PACKET. - gsfapi subscriber: parses
KEY INDEXfrom the flags byte, AES-decryptsSIGNAL INDEX + PAYLOADusing the matching key set before reading signal index. - pyapi subscriber: same as gsfapi - parses the bit and decrypts. Reuses the existing
AES-CBC +
_key_ivsinfrastructure that already handles DATA PACKET decryption. - pyapi publisher: not applicable - pyapi has no UDP send path for BufferBlocks (sends
over the TCP command channel only, where TLS already covers confidentiality), so the
KEY INDEXbit is never set on send. Receive parity preserves byte-exact interop with a gsfapi peer that does emit encrypted BufferBlocks.
Receive-side validation: subscribers print key_index=True/False per block alongside
req_confirm and compressed. The default test harness does not negotiate UDP cipher keys,
so all observed flags read key_index=False; the bit is exercised end-to-end whenever a
real session establishes UDP cipher keys via ROTATE CIPHER KEYS.
Per IEEE 2664-2024 Table 8 bit 0x01, each buffer block can opt in or out of receiver
acknowledgement. Both publishers expose --no-confirm to clear the bit; receivers report
the parsed flag state inline (flags=0x09 = REQUIRE CONFIRMATION | COMPRESSED,
flags=0x08 = COMPRESSED only).
When the bit is set (the default):
- Publisher caches each block in the retransmission cache and arms the retransmission timer
- Subscriber emits
CONFIRM BUFFER BLOCK(ServerCommand 0x08) on receipt - Publisher trims the cache as acks arrive; retransmits unacked blocks on timer expiry
When the bit is clear:
- Publisher fire-and-forget — no cache entry, timer stays stopped
- Subscriber skips the ack but still advances sequence-number bookkeeping (ordering / dropout detection still works for a peer that knows to look)
pyapi.transport.bufferblock.BufferBlock.flags and
Gemstone.Timeseries.BufferBlockMeasurement.Flags expose the parsed flag byte on the
receive side; Gemstone.Timeseries.BufferBlockMeasurement.RequireConfirmation is the
parallel publisher-side knob (defaults true). pyapi's send methods take a
require_confirmation: bool = True keyword.
Three real issues turned up while building it, all fixed before declaring the harness green:
- gsfapi over-allocated the on-wire buffer by 6 bytes —
SubscriberAdapter.cs::ProcessMeasurementsdeclaredbyte[] bufferBlock = new byte[BufferBlockHeaderSize + 4 + bufferBlockMeasurement.Length](15 + L) but only wrote 9 + L bytes of content. The trailing 6 zero-init bytes leaked onto the wire and gsfapi's own subscriber consumed them (new BufferBlockMeasurement(buffer, +9, responseLength - 9)is 6 too long). Fixed by allocating exactly9 + Length. - gsfapi's TSSC path silently dropped buffer blocks —
ProcessTSSCMeasurementshad no BufferBlockMeasurement branch and fed each one through the TSSC encoder as if it were a regular measurement (usingAdjustedValue, which is NaN). Since TSSC is essentially always on in production, this made buffer blocks unusable in real deployments. Fixed by detecting BufferBlockMeasurement at the top of the TSSC loop, flushing any pending TSSC payload, and sending the buffer block on its own frame with GZip-compressed payload (BufferBlockFlags.Compressed). - gsfapi wrote
cacheIndexas a raw int (0 or 1) at offset +4 — not matching IEEE Table 8 where CACHE INDEX is bit0x10of a flags byte. Fixed by introducing aBufferBlockFlagsenum (parallel toDataPacketFlags) and writingCacheIndexas bit0x10, withCompressedat0x08andRequireConfirmationat0x01. pyapi matches.
Build prerequisites: the gsfapi sources at ..\..\gsfapi and a Python 3.10+ interpreter
with numpy. The harness uses ProjectReferences into local Gemstone source, so a build
out of Configuration=Development is required — the build.cmd / run.cmd wrappers
set this for you.
# Terminal 1
cd C:\Projects\sttp\test-interop\bufferblock\csharp-publisher
.\build.cmd
.\run.cmd --port=7201 --auto --auto-count=2 --auto-delay=3
# Terminal 2 (or wait ~3s after publisher logs "waiting for subscriber to connect...")
cd C:\Projects\sttp\test-interop\bufferblock\python-subscriber
python main.py --host localhost --port 7201 --expect-sets=2 --timeout=45To force the non-TSSC publisher path, add --no-tssc on the C# side.
# Terminal 1
cd C:\Projects\sttp\test-interop\bufferblock\python-publisher
python main.py --port=7202 --auto-count=2 --auto-delay=3
# Terminal 2
cd C:\Projects\sttp\test-interop\bufferblock\csharp-subscriber
.\build.cmd
.\run.cmd --host=localhost --port=7202 --publisher=python --expect-sets=2 --timeout=45The pyapi publisher's send_buffer_block automatically sets the COMPRESSED flag and
GZip-compresses when the session has payload compression negotiated.
# Terminal 1
cd C:\Projects\sttp\test-interop\bufferblock\csharp-publisher
.\run.cmd --port=7197 --auto --auto-count=2 --auto-delay=3 # TSSC off path
# or with TSSC on (default):
.\run.cmd --port=7198 --auto --auto-count=2 --auto-delay=3
# Terminal 2
cd C:\Projects\sttp\test-interop\bufferblock\csharp-subscriber
.\run.cmd --host=localhost --port=7197 --publisher=csharp --expect-sets=2 --timeout=45All three variants exit with Mismatches: 0 and status code 0.
| Variant | Receiver report |
|---|---|
| gsfapi → gsfapi (TSSC off) | Buffer blocks received: 6, Mismatches: 0 |
| gsfapi → gsfapi (TSSC on) | Buffer blocks received: 6, Mismatches: 0 |
| gsfapi → pyapi (TSSC on) | Buffer blocks received: 6, Mismatches: 0 |
| pyapi → gsfapi | Buffer blocks received: 6, Mismatches: 0 |