@blockchaincommons/dcbor is a port of bc-dcbor-rust
(crate dcbor). Its committed vectors (tests/vectors/*.json) are
cross-validated against the Rust reference, pinned at dcbor = 0.25.2,
by the harness in tests/rust-validation/. It runs twice: once against the
default build every Rust consumer uses, once with dcbor's num-bigint
feature. Both runs are a CI job.
cd tests/rust-validation
cargo run --release -- ../vectors
cargo run --release --features bignum -- ../vectorsValidation result (2026-09-14), default build:
encode: 416 vectors - 348 match, 30 reference-throw, 8 emulated-throw, 30 skipped, 0 expected-divergence, 0 MISMATCH
decode: 267 vectors - 79 match, 188 reference-throw, 0 expected-divergence, 0 MISMATCH
format: 102 vectors - 94 match, 8 skipped, 0 expected-divergence, 0 MISMATCH
date: 60 vectors - 39 match, 13 reference-throw, 8 emulated-throw, 0 expected-divergence, 0 MISMATCH
uint: 19 vectors - 11 match, 8 reference-throw, 0 expected-divergence, 0 MISMATCH
--features bignum:
encode: 416 vectors - 367 match, 30 reference-throw, 8 emulated-throw, 11 skipped, 0 expected-divergence, 0 MISMATCH
decode: 267 vectors - 79 match, 188 reference-throw, 0 expected-divergence, 0 MISMATCH
format: 102 vectors - 99 match, 3 skipped, 0 expected-divergence, 0 MISMATCH
date: 60 vectors - 39 match, 13 reference-throw, 8 emulated-throw, 0 expected-divergence, 0 MISMATCH
uint: 19 vectors - 11 match, 8 reference-throw, 0 expected-divergence, 0 MISMATCH
- Encoded bytes match byte for byte, and every decode rejection matches by
error code and by the error's
Displaymessage (areference-throwon the decode, date and uint lines is a rejection both sides produce). - reference-throw counts inputs the reference itself rejects
(
Date::from_string, decoders). emulated-throw counts TypeScript guards for inputs the reference cannot express (cbor(bigint)outside the CBOR integer range, a negativebiguintToCbor) or on which it panics (fromEpochSeconds(±Infinity), whole seconds outside chrono's range); the harness probes chrono'stimestamp_optwith the reference's own arithmetic before calling into it. - skipped counts JS-only input shapes (
Symbol, function, malformed bare node), the rejected legacy protocol shapes ({tag, value}literals andtaggedCbor()-only objects), rows pinned to the other build, and the bignum recipes in the default build. - Decode vectors whose accepted input re-encodes differently (whole-valued f32/f64 heads) carry the reference's re-encoded bytes.
- Format vectors cover diagnostic notation (plain, annotated, flat,
summarized) and annotated hex under
TagsStoreOpt::Noneand a fresh store afterregister_tags_in. Each row that depends on the build is pinned to it. - Date vectors cover
CborDatedecoding and display, including leap seconds, the range bounds andNaN. - Unsigned vectors compare
u8/u16/u32/u64::try_fromwithexpectUnsigned(cbor, { width, wrapNegative: true })(§1.1). - The allowlist
expected_divergences()intests/rust-validation/src/main.rsis empty. - This verifies the committed vectors, not every input. The entries below are the behavioral differences that remain.
Where the reference panics (an impossible date, a tag registered without a
name or under a conflicting name), the port throws a CborError at the same
call. Where the outcome depends on the host rather than the port (recursion
depth, clock resolution, cloning of visitor state), neither side defines a
contract. Neither kind is a divergence and neither is recorded here.
These cases differ in accepted inputs or error handling. Changes should be reviewed against both the TypeScript tests and the Rust reference.
The reference's u8…u64 TryFrom<CBOR> wraps a negative integer instead
of rejecting it (int.rs, Ok((-1 - a) as $type); executed:
u8::try_from(-1) = 255, u64::try_from(-2^64) = 0).
expectUnsigned(cbor, { width, wrapNegative: true }) matches
u8…u64/usize::try_from, including the wrap and the OutOfRange
beyond the width (harness uint vectors). Without options,
expectUnsigned still throws WrongType for any negative integer. A
consumer that ports a field decoded with u*::try_from records whether it
matches the wrap: bc-components-ts uses the helper, and bc-known-values-ts
records its choice.
hexToBytes strips whitespace before decoding, so "a1 61 61 01" yields
the bytes of "a1616101"; an odd length or a non-hex digit throws
CborError Custom. The reference's CBOR::try_from_hex unwraps
hex::decode, which rejects whitespace, so it panics on every malformed
input, whitespace included. The port therefore accepts an input the
reference does not. Covered by tests/hex.property.test.ts.
The reference's exact_from_f16 for i16 tests source <= -32768.0
(exact.rs), so -32768.0, which binary16 represents exactly, is None,
while exact_from_f32(-32768.0) and exact_from_f64(-32768.0) are
Some(-32768). ExactI16.exactFromF16(-32768) is undefined to match;
tests/exact.test.ts pins it. Nothing on the wire depends on it.
- Adding vectors: a vector that diverges from Rust is a bug to fix,
unless it belongs to a class above. Document such a case here and, when
the harness covers it, add it to
expected_divergences()intests/rust-validation/src/main.rsin the same change. The allowlist is empty today. - Re-run the cross-validation in both builds (default and
--features bignum) after every fixture regeneration (bun run vectors:generate), and update the header result lines. - Version pin: the harness pins
dcbor = 0.25.2. When bumping, re-run both builds and update the header.