Troubleshooting¶
Common issues and solutions when working with VectorPin.
Verification Errors¶
VECTOR_TAMPERED¶
Problem: The vector hash does not match vec_hash.
Cause: The stored vector differs from the one originally pinned. This is the steganography kill shot — it is what VectorPin exists to catch. Treat as a security incident until proven otherwise.
Triage steps:
- Confirm you're passing the stored vector (not a re-computed one) to
verify(). - Check for
f32vsf64dtype mismatch between pin time and verify time. The pin commits to a specificvec_dtype; converting before hashing breaks the hash. - Check for FTZ/DAZ floating-point modes. Disable them, or normalize subnormals to zero before hashing.
- If 1–3 are clean, treat as a real integrity failure. Pull backups, investigate write paths, and rotate any keys whose
valid_untilwindow covers the affected pins.
SOURCE_MISMATCH¶
Problem: The source-text hash does not match source_hash.
Common causes:
- Encoding drift — Source text was UTF-16 / Latin-1 at one site, UTF-8 at the other.
- Whitespace drift — Trailing newline added or stripped; line endings normalized.
- Unicode normalization — Text was not NFC-normalized before pinning. (VectorPin always NFC-normalizes; the failure means the input was different.)
- Wrong source column — On Symbiont,
contentholds the text the embedding was produced from;sourceholds a URL. Using the wrong column breaks the hash. See Adapters > Symbiont schema.
Solution: Match the exact bytes the embedding was produced from. The pin commits to NFC-normalized UTF-8 of the source string passed to signer.pin.
MODEL_MISMATCH¶
Problem: The pin's model field differs from the caller's expected model.
Cause: The pin was produced with a different embedding model than the verifier expects.
Solution: Either the ingestion pipeline used the wrong model, or the verifier was configured with the wrong expectation. Confirm which is correct, then either re-pin or update the verifier config.
This check only fires when the caller passes an expected_model argument. If you don't supply one, model mismatch is silently accepted (the source/vector hashes still have to match, so this is only a confidentiality leak about which model the producer used — not an integrity failure).
UNKNOWN_KEY¶
Problem: The pin's kid is not in the verifier's registry.
Common causes:
- Misconfiguration — Verifier wasn't given the public key for this
kid. - Cross-environment leak — A pin from staging reached production (or vice versa).
- Forged
kid— Less likely, sincekidis in the signed canonical bytes (§4.2). Ifkidwas tampered with, you'd seeSIGNATURE_INVALIDfirst.
Solution: Add the missing public key to the registry, after confirming the key fingerprint out of band:
verifier = Verifier({
"prod-2026-05": load_public_bytes("./keys/prod-2026-05.pub"),
# add the missing one:
"prod-2025-11": load_public_bytes("./keys/prod-2025-11.pub"),
})
Do not silently auto-register unknown keys (TOFU). That makes the system a checksum, not a signature.
KEY_EXPIRED¶
Problem: The pin's ts falls outside the registered key's (valid_from, valid_until) window.
Cause: Either the key was rotated (and the new key is what should have signed this pin) or the key was revoked due to compromise.
Solution:
- If the pin's
tsis recent and the producer should still be signing with this key — the registry'svalid_untilis wrong. Fix the window. - If
valid_untilis correct and the pin is supposed to be there — the pin is suspect. It was signed after the key was retired, which means either clock skew on the producer or a real compromise. Investigate. - If the key was deliberately revoked due to compromise, this is the system working as designed.
See Pin Protocol > Key Rotation and Revocation.
SIGNATURE_INVALID¶
Problem: The Ed25519 signature does not validate against the registered public key.
Triage steps:
- Confirm the public key bytes match the private key that produced the pin. (Public-key fingerprint mismatch = wrong key in the registry.)
- Confirm both implementations are on the same protocol version. A v1 pin presented to a strict v2 verifier produces
UNSUPPORTED_VERSION, notSIGNATURE_INVALID— but a v2 pin whose canonical bytes were reconstructed wrongly will fail signature. - Check for double-decoding of the pin JSON (e.g., the JSON was JSON-decoded twice and is now a string-of-a-string).
If 1–3 are clean and the failure is consistent, treat as a security incident — someone produced a pin with a key that isn't yours.
SHAPE_MISMATCH¶
Problem: The caller's vector has a different dimensionality than vec_dim.
Cause: Wrong embedding model on either side, or wrong vector field plucked from the DB record.
Solution: Confirm the vector being passed to verify() is the one from the same record as the pin, and that both sides agree on the embedding model.
UNSUPPORTED_VERSION¶
Problem: The pin's v is not in the verifier's supported set.
Cause: A v1 pin from before the May 2026 wire-format break. v1 is not backwards-compatible with v2.
Solution: Re-pin the affected records using a v2 signer. If you need to verify v1 pins for a migration, use LegacyV1Verifier (opt-in only). See Specification §12 for the v1 → v2 change list.
PARSE_ERROR¶
Problem: The pin JSON is malformed, oversized, or contains disallowed content.
FAIL [parse_error] unknown top-level field: "foo"
FAIL [parse_error] string field "model" is not NFC-normalized
FAIL [parse_error] pin exceeds 64 KiB size limit
Common causes:
- Unknown top-level fields — v2 verifiers MUST reject them. This is a downgrade-attack defense.
- Non-string
extravalues —extrais strictlymap<string, string>. - Non-NFC strings — every string field must be NFC-normalized.
- Control characters or bidi overrides in strings — always rejected.
- Oversized pin — exceeds the 64 KiB limit, or
extraexceeds 32 entries. - Malformed timestamp —
tsmust match exactly^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$. No fractional seconds, no timezone offsets, no lowercaset/z.
Solution: Inspect the failing pin. Most PARSE_ERROR cases are buggy producers — fix the producer, re-pin. Hostile pins go to your security log.
RECORD_MISMATCH / COLLECTION_MISMATCH / TENANT_MISMATCH¶
Problem: Replay-protection check failed.
Cause: A pin from record A was attached to record B, then audited with the expected record id for B. This is exactly what the reserved vectorpin.record_id (etc.) keys are designed to catch — see Security > Replay Protection.
Solution: Investigate as a possible pin-shuffle attack. If it's a legitimate copy / re-keying operation, re-pin under the new record id.
Pinning Errors¶
vector contains NaN / vector contains Inf¶
Problem: The vector contains NaN, +Inf, or -Inf.
Cause: Numerical issue upstream of pinning — e.g., a model that produced NaN for an empty or degenerate input.
Solution: Reject the embedding upstream, don't try to pin it. Pinning NaN/Inf is forbidden by the protocol because their canonical byte representation isn't unique enough across implementations.
string field is not NFC-normalized¶
Problem: A string field (source, model, kid, an extra key/value) is not NFC.
Solution: Normalize before passing to signer.pin. The signer also normalizes, but it rejects strings containing control characters or bidi overrides outright.
Vector dtype confusion¶
The pin's vec_dtype is the dtype the signer committed to, and the verifier compares the stored vector in its native precision against it. Verifiers never narrow a wider vector before hashing: a float64 array (or, in TypeScript, a Float64Array / plain number[]) supplied against an f32 pin is rejected as SHAPE_MISMATCH with a "wider than pinned vec_dtype" detail, because the bits a cast discards would be an unverified channel for hidden data. A narrower array (float32 against an f64 pin) widens exactly but will not hash-match unless the signer's values were already representable in that precision; the VECTOR_TAMPERED detail then names the dtype you should pin under.
Rule of thumb: pin under the dtype your store persists. Signers default to the input array's native dtype, so if an embedding API hands you 64-bit floats and your store persists 32-bit, pass vec_dtype="f32" (vecDtype: 'f32', PinOptions { dtype: Some(VecDtype::F32) }) explicitly. Adapters report the store's native dtype (LanceDB and pgvector read it from the schema; Chroma, Qdrant and Pinecone persist single precision), so the audit sees exactly what the store holds.
Adapter Errors¶
LanceDB: column 'text' not found¶
Problem: --source-column text (or the programmatic equivalent) names a column that doesn't exist on the table.
Solution: Check the table schema. On Symbiont, the source text lives in content, not text. Pass --source-column content.
Chroma: audit-chroma requires either --path or --host¶
Solution: Pick one. --path /path/to/db for PersistentClient, --host chroma.host --port 8000 for HTTP.
Qdrant: pin not visible in payload¶
Problem: Pins were attached, but audit-qdrant reports them as unpinned (and fails the run).
Cause: The pin is stored under payload["vectorpin"]. If your write path uses set_payload with a partial dict, ensure the dict actually includes that key.
FAIL <id> [unpinned] / audit exits 1 on a partial backfill¶
Problem: The audit fails on records that have no pin yet.
Cause: Unpinned records fail closed by default: an attacker who can rewrite a vector can delete its pin just as easily, so "no pin" is not treated as "nothing to check". An empty or whitespace-only pin field is always a parse_error, never "unpinned".
Solution: During a deliberate backfill pass --allow-unpinned; remove it once the corpus is fully pinned. Pair the audit with a signed collection manifest (--write-manifest right after ingestion, --manifest on every audit) so deleted or inserted records are caught too.
FAIL <id> [record_mismatch] on records that were never copied¶
Problem: Pins carry vectorpin.record_id values that do not equal the id column the adapter iterates.
Cause: The audit enforces vectorpin.record_id == record id by default (that is what defeats pin-shuffle replay). If your pipeline bound a different identifier than the store's primary key, every record mismatches.
Solution: Re-pin with the store's record id, or pass --no-record-id-check while you migrate (this disables replay detection across records).
See Also¶
- Pin Protocol > Failure-Mode Reference — One-line summary of every error
- Security — When to escalate, when to fix config
- Getting Started — Sanity-check end-to-end walkthrough