Skip to content

Troubleshooting

Common issues and solutions when working with VectorPin.


Verification Errors

VECTOR_TAMPERED

Problem: The vector hash does not match vec_hash.

FAIL [vector_tampered] vec_hash mismatch

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:

  1. Confirm you're passing the stored vector (not a re-computed one) to verify().
  2. Check for f32 vs f64 dtype mismatch between pin time and verify time. The pin commits to a specific vec_dtype; converting before hashing breaks the hash.
  3. Check for FTZ/DAZ floating-point modes. Disable them, or normalize subnormals to zero before hashing.
  4. If 1–3 are clean, treat as a real integrity failure. Pull backups, investigate write paths, and rotate any keys whose valid_until window covers the affected pins.

SOURCE_MISMATCH

Problem: The source-text hash does not match source_hash.

FAIL [source_mismatch] source_hash mismatch

Common causes:

  1. Encoding drift — Source text was UTF-16 / Latin-1 at one site, UTF-8 at the other.
  2. Whitespace drift — Trailing newline added or stripped; line endings normalized.
  3. Unicode normalization — Text was not NFC-normalized before pinning. (VectorPin always NFC-normalizes; the failure means the input was different.)
  4. Wrong source column — On Symbiont, content holds the text the embedding was produced from; source holds 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.

FAIL [model_mismatch] model "text-embedding-3-small" != expected "text-embedding-3-large"

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.

FAIL [unknown_key] kid "prod-2026-05" not in registry

Common causes:

  1. Misconfiguration — Verifier wasn't given the public key for this kid.
  2. Cross-environment leak — A pin from staging reached production (or vice versa).
  3. Forged kid — Less likely, since kid is in the signed canonical bytes (§4.2). If kid was tampered with, you'd see SIGNATURE_INVALID first.

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.

FAIL [key_expired] ts 2026-06-01T00:00:00Z is after valid_until 2026-05-15T12:00:00Z

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 ts is recent and the producer should still be signing with this key — the registry's valid_until is wrong. Fix the window.
  • If valid_until is 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.

FAIL [signature_invalid] signature verification failed

Triage steps:

  1. Confirm the public key bytes match the private key that produced the pin. (Public-key fingerprint mismatch = wrong key in the registry.)
  2. Confirm both implementations are on the same protocol version. A v1 pin presented to a strict v2 verifier produces UNSUPPORTED_VERSION, not SIGNATURE_INVALID — but a v2 pin whose canonical bytes were reconstructed wrongly will fail signature.
  3. 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.

FAIL [shape_mismatch] vector has dim 1536, pin has vec_dim 3072

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.

FAIL [unsupported_version] v=1, only v=2 supported

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:

  1. Unknown top-level fields — v2 verifiers MUST reject them. This is a downgrade-attack defense.
  2. Non-string extra values — extra is strictly map<string, string>.
  3. Non-NFC strings — every string field must be NFC-normalized.
  4. Control characters or bidi overrides in strings — always rejected.
  5. Oversized pin — exceeds the 64 KiB limit, or extra exceeds 32 entries.
  6. Malformed timestamp — ts must 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 lowercase t/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.

FAIL [record_mismatch] expected record_id "doc-123", pin attests "doc-456"

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