Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions features/signature-hint-auditor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Transaction Signature Hint Auditor

Decode a pasted transaction envelope offline and map every decorated signature
hint to optional public-key candidates — without treating a hint match as a
verified signature.

## How it works

The tool reads base64 transaction-envelope XDR with `@stellar/stellar-sdk` in
the browser. It walks every decorated signature in original order and records
the four-byte hint (lowercase hex). When you paste optional `G…` public keys,
each key's hint is derived the same way the network does — the last four bytes
of the raw public key — and compared to the envelope.

A hint can match zero, one or many provided keys. Multiple matches are
collisions: every candidate is listed and none is preferred. Fee-bump envelopes
keep outer and inner signature vectors in separate groups so a fee-source hint
is never confused with an inner signer.

## The non-obvious decision

**A matching hint is labelled a hint match, never a verified signature.** Four
bytes are not a proof. Two unrelated keys can share them, and this tool will
happily show both as candidates. Cryptographic verification, threshold checks
and submission are all out of scope on purpose.

Secret seeds (`S…`) are refused on the prefix alone — in the envelope field and
in the signer list — and the form is remounted so the seed does not sit in the
textarea. Input stays in memory for the session; `msw/handlers.ts` is empty.

## Error outcomes

| Code | Meaning |
| --- | --- |
| `empty_xdr` | Nothing was pasted in the envelope field |
| `invalid_xdr` | Not usable envelope XDR (including refused secret seeds) |
| `invalid_public_signer` | A signer token is not a valid `G…` public key |
| `unsupported_envelope` | Envelope discriminant is not classic v0/v1 or fee-bump |
| `too_many_signers` | More than 64 public keys in one paste |

## Fixtures

Envelopes are built from fixed raw seeds. The collision fixture forces two
genuine public keys to share a recorded hint so the ambiguous-hint UI path is
deterministic without hunting for a real on-chain collision.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, it } from "vitest";
import { renderFeature, screen } from "@/core/testing/render";
import { SignatureHintAuditorPanel } from "@/features/signature-hint-auditor/components/SignatureHintAuditorPanel";
import { copy, errorCopy, groupLabels } from "@/features/signature-hint-auditor/copy";
import {
feeBumpXdr,
feeSource,
notBase64,
secretSeed,
signedClassicXdr,
source,
sourceHint,
unrelatedSigner
} from "@/features/signature-hint-auditor/fixtures/signatureHintAuditor.fixture";
import {
collidingCandidates,
collisionHint
} from "@/features/signature-hint-auditor/fixtures/hint-collision.fixture";
import { formatHint } from "@/features/signature-hint-auditor/lib/format";

type User = ReturnType<typeof renderFeature>["user"];

async function audit(user: User, envelope: string, publicSigners = "") {
await user.click(screen.getByLabelText(copy.envelopeLabel));
await user.paste(envelope);
if (publicSigners) {
await user.click(screen.getByLabelText(copy.signersLabel));
await user.paste(publicSigners);
}
await user.click(screen.getByRole("button", { name: copy.submit }));
}

describe("SignatureHintAuditorPanel", () => {
it("shows the empty state first", () => {
renderFeature(<SignatureHintAuditorPanel />);
expect(screen.getByText(copy.emptyTitle)).toBeInTheDocument();
});

it("lists a hint match without calling it verified", async () => {
const { user } = renderFeature(<SignatureHintAuditorPanel />);
await audit(user, signedClassicXdr, source.publicKey());

expect(await screen.findByText(copy.resultTitle)).toBeInTheDocument();
expect(screen.getByText(copy.disclaimer)).toBeInTheDocument();
expect(screen.getByText(copy.oneCandidate)).toBeInTheDocument();
expect(screen.getAllByText(formatHint(sourceHint)).length).toBeGreaterThanOrEqual(1);
expect(screen.getByText(copy.matchSingle)).toBeInTheDocument();
});

it("separates fee-bump outer and inner groups", async () => {
const { user } = renderFeature(<SignatureHintAuditorPanel />);
await audit(user, feeBumpXdr, `${feeSource.publicKey()}\n${source.publicKey()}`);

expect(await screen.findByText(groupLabels.fee_bump_outer)).toBeInTheDocument();
expect(screen.getByText(groupLabels.fee_bump_inner)).toBeInTheDocument();
});

it("explains an unmatched hint", async () => {
const { user } = renderFeature(<SignatureHintAuditorPanel />);
await audit(user, signedClassicXdr, unrelatedSigner.publicKey());

expect(await screen.findByText(copy.unmatchedLabel)).toBeInTheDocument();
expect(screen.getByText(copy.collisionNoticeTitle)).toBeInTheDocument();
});

it("shows actionable copy for invalid xdr", async () => {
const { user } = renderFeature(<SignatureHintAuditorPanel />);
await audit(user, notBase64);

expect(await screen.findByText(errorCopy.invalid_xdr.title)).toBeInTheDocument();
expect(screen.getByText(errorCopy.invalid_xdr.description)).toBeInTheDocument();
});

it("refuses a secret seed and clears it from the field", async () => {
const { user } = renderFeature(<SignatureHintAuditorPanel />);
await audit(user, secretSeed);

expect(await screen.findByText(errorCopy.invalid_xdr.title)).toBeInTheDocument();
expect(screen.queryByDisplayValue(secretSeed)).not.toBeInTheDocument();
});

it("states that input stays in memory only", async () => {
const { user } = renderFeature(<SignatureHintAuditorPanel />);
await audit(user, signedClassicXdr);

expect(await screen.findByText(copy.memoryNote)).toBeInTheDocument();
});

it("documents colliding candidates share a forced hint in fixtures", () => {
expect(collidingCandidates).toHaveLength(2);
expect(collidingCandidates[0].hint).toBe(collisionHint);
expect(collidingCandidates[1].hint).toBe(collisionHint);
});
});
38 changes: 38 additions & 0 deletions features/signature-hint-auditor/__tests__/a11y.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it } from "vitest";
import { renderFeature, screen } from "@/core/testing/render";
import { expectNoAxeViolations } from "@/core/testing/axe";
import { SignatureHintAuditorPanel } from "@/features/signature-hint-auditor/components/SignatureHintAuditorPanel";
import { copy, errorCopy } from "@/features/signature-hint-auditor/copy";
import {
signedClassicXdr,
source
} from "@/features/signature-hint-auditor/fixtures/signatureHintAuditor.fixture";

describe("SignatureHintAuditorPanel accessibility", () => {
it("has no WCAG A/AA violations in its initial state", async () => {
const { container } = renderFeature(<SignatureHintAuditorPanel />);
await expectNoAxeViolations(container);
});

it("has no WCAG A/AA violations with a successful audit on screen", async () => {
const { container, user } = renderFeature(<SignatureHintAuditorPanel />);

await user.click(screen.getByLabelText(copy.envelopeLabel));
await user.paste(signedClassicXdr);
await user.click(screen.getByLabelText(copy.signersLabel));
await user.paste(source.publicKey());
await user.click(screen.getByRole("button", { name: copy.submit }));
await screen.findByText(copy.resultTitle);

await expectNoAxeViolations(container);
});

it("has no WCAG A/AA violations while showing an error", async () => {
const { container, user } = renderFeature(<SignatureHintAuditorPanel />);

await user.click(screen.getByRole("button", { name: copy.submit }));
await screen.findByText(errorCopy.empty_xdr.title);

await expectNoAxeViolations(container);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest";
import { decodeDecoratedSignatures } from "@/features/signature-hint-auditor/lib/decorated-signatures";
import {
feeBumpXdr,
feeSourceHint,
multiSignedClassicXdr,
notAnEnvelopeXdr,
signedClassicXdr,
signedV0Xdr,
sourceHint,
unsignedClassicXdr,
unsignedFeeBumpXdr,
extraSignerHint
} from "@/features/signature-hint-auditor/fixtures/signatureHintAuditor.fixture";

describe("decodeDecoratedSignatures", () => {
it("lists classic v1 hints in envelope order", () => {
const result = decodeDecoratedSignatures(multiSignedClassicXdr);

expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.variant).toBe("classic-v1");
expect(result.value.entries.map((entry) => entry.hint)).toEqual([
sourceHint,
extraSignerHint
]);
expect(result.value.entries.every((entry) => entry.group === "transaction")).toBe(true);
});

it("preserves zero-based indexes inside the group", () => {
const result = decodeDecoratedSignatures(multiSignedClassicXdr);

expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.entries.map((entry) => entry.index)).toEqual([0, 1]);
});

it("handles an empty signature vector", () => {
const result = decodeDecoratedSignatures(unsignedClassicXdr);

expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.entries).toEqual([]);
});

it("separates fee-bump outer and inner groups", () => {
const result = decodeDecoratedSignatures(feeBumpXdr);

expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.variant).toBe("fee-bump");
expect(result.value.entries).toEqual([
{ index: 0, hint: feeSourceHint, group: "fee_bump_outer" },
{ index: 0, hint: sourceHint, group: "fee_bump_inner" }
]);
});

it("keeps empty fee-bump groups as empty entries rather than inventing rows", () => {
const result = decodeDecoratedSignatures(unsignedFeeBumpXdr);

expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.entries).toEqual([]);
});

it("decodes classic v0 envelopes", () => {
const result = decodeDecoratedSignatures(signedV0Xdr);

expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.variant).toBe("classic-v0");
expect(result.value.entries[0]?.hint).toBe(sourceHint);
});

it("rejects valid base64 that is not an envelope", () => {
expect(decodeDecoratedSignatures(notAnEnvelopeXdr)).toEqual({
ok: false,
code: "invalid_xdr"
});
});

it("returns a single signed classic hint", () => {
const result = decodeDecoratedSignatures(signedClassicXdr);

expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.entries).toHaveLength(1);
expect(result.value.entries[0]?.hint).toMatch(/^[0-9a-f]{8}$/);
});
});
80 changes: 80 additions & 0 deletions features/signature-hint-auditor/__tests__/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";
import {
formatCandidateCount,
formatEnvelopeVariant,
formatHint,
formatIndex,
formatMatchKind,
formatSignatureCount,
formatSignatureGroup
} from "@/features/signature-hint-auditor/lib/format";
import {
isUnsupportedEnvelope,
toSignatureHintAuditorErrorCode
} from "@/features/signature-hint-auditor/lib/signatureHintAuditor.errors";
import { copy } from "@/features/signature-hint-auditor/copy";

describe("formatHint", () => {
it("prefixes lowercase hex with 0x", () => {
expect(formatHint("aabbccdd")).toBe("0xaabbccdd");
});
});

describe("formatEnvelopeVariant", () => {
it("names every supported variant", () => {
expect(formatEnvelopeVariant("classic-v0")).toBe(copy.variantClassicV0);
expect(formatEnvelopeVariant("classic-v1")).toBe(copy.variantClassicV1);
expect(formatEnvelopeVariant("fee-bump")).toBe(copy.variantFeeBump);
});
});

describe("formatSignatureGroup", () => {
it("labels classic and fee-bump groups distinctly", () => {
expect(formatSignatureGroup("transaction")).toBe(copy.groupTransaction);
expect(formatSignatureGroup("fee_bump_outer")).toBe(copy.groupOuter);
expect(formatSignatureGroup("fee_bump_inner")).toBe(copy.groupInner);
});
});

describe("formatMatchKind", () => {
it("never calls a match a verified signature", () => {
expect(formatMatchKind("single")).toBe(copy.matchSingle);
expect(formatMatchKind("single")).not.toMatch(/verif/i);
expect(formatMatchKind("collision")).toBe(copy.matchCollision);
expect(formatMatchKind("none")).toBe(copy.matchNone);
});
});

describe("formatSignatureCount", () => {
it("uses singular and plural forms", () => {
expect(formatSignatureCount(1)).toBe("1 signature");
expect(formatSignatureCount(0)).toBe("0 signatures");
expect(formatSignatureCount(2)).toBe("2 signatures");
});
});

describe("formatCandidateCount", () => {
it("explains zero, one and many candidates", () => {
expect(formatCandidateCount(0)).toBe(copy.noCandidates);
expect(formatCandidateCount(1)).toBe(copy.oneCandidate);
expect(formatCandidateCount(2)).toBe(copy.collisionLabel);
});
});

describe("formatIndex", () => {
it("renders a readable index marker", () => {
expect(formatIndex(0)).toBe("#0");
expect(formatIndex(3)).toBe("#3");
});
});

describe("error classification", () => {
it("maps unexpected throws to invalid_xdr", () => {
expect(toSignatureHintAuditorErrorCode(new Error("boom"))).toBe("invalid_xdr");
});

it("flags unsupported envelopes for an informational notice", () => {
expect(isUnsupportedEnvelope("unsupported_envelope")).toBe(true);
expect(isUnsupportedEnvelope("invalid_xdr")).toBe(false);
});
});
Loading
Loading