Files
teleport/mpc/__tests__/custody_mpc_e2e.test.ts
zeekay 3dcc1cf6a4 tests: Tier A e2e + fuzz across bridge / custody-MPC / oracle / profile-gate
Adds the test suite that backs the SUBMISSION / PROOF-CLAIMS package.

contracts/test/Bridge.e2e.test.ts (14 cases):
  - Full burn -> envelope -> claim round-trip under classical-compat.
  - Off-chain reconstruction of messageHash from BridgeBurnedV2 logs.
  - P3 envelope binding: tampering amount / recipient / nonce / token /
    sourceChainId / destChainId all refused with EnvelopeBindingMismatch
    or InvalidChainId.
  - P1 strict-PQ profile flip: classical -> strict-PQ -> rollback.
  - P5 EXTCODEHASH allow-list defence: ADMIN_ROLE compromise cannot
    install a rogue verifier even with an allow-listed policyId.
  - Pausable round-trip.

mpc/__tests__/custody_mpc_e2e.test.ts (12 cases):
  - policyId of every scheme matches @luxfi/pq-profile-ids-ts (single
    source of truth shared with on-chain StrictPQProfileGate).
  - LUX_PROFILE=quasar-strict-pq default: double-lattice round-trip
    via inproc mocks (both pulsar + corona daemons called).
  - LUX_PROFILE=classical-compat default: ecdsa-GG18 wire byte-identical.
  - Per-scheme sign/verify round-trip: frost (real noble-curves),
    cggmp21 / pulsar / corona / double-lattice (inproc mocks).
  - double-lattice composition contract: inner Pulsar + Corona exposed
    via readonly fields, sub-sig split unambiguous.

mpc/__tests__/profile_negative.test.ts (28 cases):
  - Full 4 x (6+1) profile x scheme matrix exhaustively exercised.
  - Fail-closed on unset LUX_PROFILE regardless of MPC_SCHEME value
    (closes Red HIGH #4).
  - Unknown profile / unknown scheme rejected with clear errors.

mpc/__tests__/signature_fuzz.test.ts (8 fuzz harnesses):
  - hex / unhex round-trip across 1024 random byte arrays.
  - double-lattice split unambiguous across 256 random sub-sig
    lengths in [16, 256] bytes.
  - Truncation / bit-flip / lane-swap rejection.
  - JSON-RPC hex validation across 256 random message sizes.
  - Wrong-key rejection on Corona with 32 fuzz cases.
  - Empty sub-signature refused (defensive).

api/src/pq/oracle_attestation.test.ts (24 cases):
  - WALLET_SCHEME_MLDSA65 == 0x42 byte-pin (Go-side wire-compat).
  - CSHAKE_CUSTOMIZATION_TX byte-pin (16-byte ASCII).
  - serializeEnvelope round-trip + edge values + out-of-range refusal.
  - signEnvelope dispatch refuses non-MLDSA65 account / provider.
  - envelopeDigest is byte-identical to cshake256(envelope, N, S, 48).
  - LUX_PROFILE / STRICT_PQ resolution under all four cases.

Totals:
  - contracts:  60 existing + 14 new = 74 passing
  - mpc:        31 existing + 58 new = 89 passing
  - api:        19 existing + 24 new = 43 passing

  Repo-wide: 206 tests passing.
2026-05-19 08:41:24 -07:00

336 lines
12 KiB
TypeScript

/**
* Custody-MPC end-to-end tests.
*
* Exercises the six-scheme threshold-signer bus end-to-end:
*
* 1. Resolve scheme from LUX_PROFILE.
* 2. Construct signer via getSigner().
* 3. (Where possible) run keygenInit -> sign -> verify round-trip.
* 4. Verify policyId matches the canonical @luxfi/pq-profile-ids-ts
* constants — this is the byte-level invariant shared with the
* on-chain StrictPQProfileGate.
*
* For lattice / daemon-backed signers (cggmp21, pulsar, corona,
* double-lattice), inproc mocks replicate the wire format mpcd would
* expose. For local schemes (ecdsa via gg18, frost via @noble/curves),
* the test runs against the real in-process implementation.
*
* This file is the "all-six-schemes-round-trip" integration test that
* mirrors §4.3 of SPEC.md.
*/
import { ethers } from 'ethers';
import { secp256k1 } from '@noble/curves/secp256k1.js';
import {
getSigner,
resolveSchemeFromEnv,
POLICY_ID,
EcdsaSigner,
FrostSigner,
Cggmp21Signer,
PulsarSigner,
CoronaSigner,
DoubleLatticeSigner,
SchemeName,
} from '../src/signers';
import { RpcRequest, RpcTransport } from '../src/signers/rpc';
import {
SCHEME_ECDSA_GG18,
SCHEME_ECDSA_CGGMP21,
SCHEME_FROST_RFC9591,
SCHEME_PULSAR_M_LWE,
SCHEME_CORONA_R_LWE,
SCHEME_DOUBLE_LATTICE,
} from '@luxfi/pq-profile-ids-ts';
// ─── Lattice daemon mock (shared across pulsar/corona/double-lattice) ───
function makeLatticeMock(scheme: 'pulsar' | 'corona'): {
transport: RpcTransport;
calls: RpcRequest[];
} {
const calls: RpcRequest[] = [];
const pkBytes = Buffer.from(
scheme === 'pulsar' ? '00112233aabbccdd' : 'deadbeefcafebabe',
'hex',
);
const transport: RpcTransport = {
kind: 'inproc',
handle: async (req) => {
calls.push(req);
const p = req.params as Record<string, string>;
if (req.method.endsWith('.keygen')) {
return { publicKey: pkBytes.toString('hex'), shares: ['aa', 'bb', 'cc'] };
}
if (req.method.endsWith('.sign')) {
const tag = scheme === 'pulsar' ? '70' : '63';
return { signatureHex: tag + p.messageHex.slice(0, 30) };
}
if (req.method.endsWith('.verify')) {
const tag = scheme === 'pulsar' ? '70' : '63';
const ok =
p.signatureHex.startsWith(tag) &&
p.signatureHex.slice(2) === p.messageHex.slice(0, 30) &&
p.pubKeyHex === pkBytes.toString('hex');
return { ok };
}
throw new Error('unknown method: ' + req.method);
},
};
return { transport, calls };
}
function makeCggmp21Mock(): { transport: RpcTransport; calls: RpcRequest[] } {
const calls: RpcRequest[] = [];
const pkBytes = Buffer.from('aa'.repeat(33), 'hex');
const transport: RpcTransport = {
kind: 'inproc',
handle: async (req) => {
calls.push(req);
const p = req.params as Record<string, string>;
if (req.method === 'cggmp21.keygen') {
return { publicKey: pkBytes.toString('hex'), shares: ['aa', 'bb'] };
}
if (req.method === 'cggmp21.sign') {
return { signatureHex: '01' + p.messageHex.slice(0, 30) };
}
if (req.method === 'cggmp21.verify') {
const ok =
p.signatureHex.startsWith('01') &&
p.signatureHex.slice(2) === p.messageHex.slice(0, 30) &&
p.pubKeyHex === pkBytes.toString('hex');
return { ok };
}
throw new Error('unknown method: ' + req.method);
},
};
return { transport, calls };
}
const m32 = (b: number) => new Uint8Array(32).fill(b);
describe('e2e custody MPC bus — policyId matches canonical @luxfi/pq-profile-ids-ts', () => {
// Single source of truth check: every policyId returned by the
// signer must match the same canonical constant the on-chain gate
// imports from @luxfi/pq-profile-ids. A drift between the two sides
// is the kind of bug that "passes tests locally but breaks in prod"
// — this test is the latch.
const expectedMap: Record<SchemeName, string> = {
ecdsa: SCHEME_ECDSA_GG18,
cggmp21: SCHEME_ECDSA_CGGMP21,
frost: SCHEME_FROST_RFC9591,
pulsar: SCHEME_PULSAR_M_LWE,
corona: SCHEME_CORONA_R_LWE,
'double-lattice': SCHEME_DOUBLE_LATTICE,
};
it.each(Object.entries(expectedMap))(
'scheme %s reports policyId %s',
(scheme, expected) => {
expect(POLICY_ID[scheme as SchemeName]).toBe(expected);
},
);
});
describe('e2e custody MPC bus — LUX_PROFILE drives scheme selection', () => {
// Under quasar-strict-pq the default is double-lattice. Construct
// it via the bus AND through getSigner() — both must produce a
// working signer with the right policyId.
it('strict-PQ default round-trip: double-lattice via inproc mocks', async () => {
const scheme = resolveSchemeFromEnv({ LUX_PROFILE: 'quasar-strict-pq' } as never);
expect(scheme).toBe('double-lattice');
const pulsarMock = makeLatticeMock('pulsar');
const coronaMock = makeLatticeMock('corona');
const signer = getSigner({
scheme: 'double-lattice',
config: {
pulsar: { transport: pulsarMock.transport },
corona: { transport: coronaMock.transport },
},
});
await signer.keygenInit!({ threshold: 2, participants: 3 });
const message = m32(0x55);
const sig = await signer.sign(message);
expect(await signer.verify(message, sig)).toBe(true);
// Both daemons MUST have been called.
expect(pulsarMock.calls.map((c) => c.method)).toEqual(
expect.arrayContaining(['pulsar.keygen', 'pulsar.sign', 'pulsar.verify']),
);
expect(coronaMock.calls.map((c) => c.method)).toEqual(
expect.arrayContaining(['corona.keygen', 'corona.sign', 'corona.verify']),
);
});
it('classical-compat default: ecdsa-GG18 wire format byte-identical', async () => {
const scheme = resolveSchemeFromEnv({ LUX_PROFILE: 'classical-compat' } as never);
expect(scheme).toBe('ecdsa');
// The signer constructor does not fail on missing binary — the
// verify path is exercised directly with a real ECDSA sig.
const wallet = ethers.Wallet.createRandom();
const compressedPub = ethers.getBytes(
secp256k1.getPublicKey(ethers.getBytes(wallet.privateKey), true) as Uint8Array,
);
const signer = getSigner({
scheme: 'ecdsa',
config: {
binPath: '/nonexistent/gg18_sign_client',
signingManager: 'http://localhost:8000',
keyStore: 'keys.store',
cwd: '/tmp',
publicKey: compressedPub,
},
});
expect(signer.policyId).toBe(SCHEME_ECDSA_GG18);
const message = m32(0x42);
const sig = await wallet.signingKey.sign(message);
expect(await signer.verify(message, ethers.getBytes(sig.serialized), compressedPub)).toBe(
true,
);
});
});
describe('e2e custody MPC bus — sign/verify round-trip for every scheme', () => {
// For each scheme that can be exercised in-process (or with a
// wire-format inproc mock), run the full keygenInit -> sign ->
// verify path. This is the "all six lanes work end-to-end" test.
it('frost: full noble-curves in-process round-trip', async () => {
const signer = getSigner({
scheme: 'frost',
config: { threshold: 2, participants: 3 },
});
const message = m32(0x01);
const sig = await signer.sign(message);
expect(sig.length).toBe(65);
expect(await signer.verify(message, sig)).toBe(true);
});
it('cggmp21: inproc mock round-trip', async () => {
const { transport } = makeCggmp21Mock();
const signer = getSigner({ scheme: 'cggmp21', config: { transport } });
await signer.keygenInit!({ threshold: 2, participants: 3 });
const message = m32(0x02);
const sig = await signer.sign(message);
expect(await signer.verify(message, sig)).toBe(true);
});
it('pulsar: inproc mock round-trip', async () => {
const { transport } = makeLatticeMock('pulsar');
const signer = getSigner({ scheme: 'pulsar', config: { transport } });
await signer.keygenInit!({ threshold: 2, participants: 3 });
const message = m32(0x03);
const sig = await signer.sign(message);
expect(await signer.verify(message, sig)).toBe(true);
});
it('corona: inproc mock round-trip', async () => {
const { transport } = makeLatticeMock('corona');
const signer = getSigner({ scheme: 'corona', config: { transport } });
await signer.keygenInit!({ threshold: 2, participants: 3 });
const message = m32(0x04);
const sig = await signer.sign(message);
expect(await signer.verify(message, sig)).toBe(true);
});
it('double-lattice: composition round-trip + length-prefix layout', async () => {
const pulsarMock = makeLatticeMock('pulsar');
const coronaMock = makeLatticeMock('corona');
const signer = getSigner({
scheme: 'double-lattice',
config: {
pulsar: { transport: pulsarMock.transport },
corona: { transport: coronaMock.transport },
},
});
await signer.keygenInit!({ threshold: 2, participants: 3 });
const message = m32(0x05);
const sig = await signer.sign(message);
// Layout: u32_be(len) || pulsar_sig || corona_sig
// Each mock returns 32-hex = 16 bytes per sub-signature.
const pulsarLen = (sig[0] << 24) | (sig[1] << 16) | (sig[2] << 8) | sig[3];
expect(pulsarLen).toBe(16);
expect(sig.length).toBe(4 + 16 + 16);
expect(await signer.verify(message, sig)).toBe(true);
// Composed public key: u32_be(len_pulsar) || pulsar_pk || corona_pk
const keys = await signer.keygenFinish!();
expect(keys.publicKey.length).toBe(4 + 8 + 8);
const pkPulsarLen =
(keys.publicKey[0] << 24) |
(keys.publicKey[1] << 16) |
(keys.publicKey[2] << 8) |
keys.publicKey[3];
expect(pkPulsarLen).toBe(8);
});
});
describe('e2e custody MPC bus — profile gate negative tests', () => {
// The off-chain mirror of the on-chain refusal: an explicit PQ
// scheme under classical-compat throws, and vice versa.
it('throws on PQ scheme under classical-compat (all three PQ schemes)', () => {
for (const scheme of ['pulsar', 'corona', 'double-lattice'] as const) {
expect(() =>
resolveSchemeFromEnv({
LUX_PROFILE: 'classical-compat',
MPC_SCHEME: scheme,
} as never),
).toThrow(/incompatible/);
}
});
it('throws on classical scheme under quasar-strict-pq (all three classical schemes)', () => {
for (const scheme of ['ecdsa', 'cggmp21', 'frost'] as const) {
expect(() =>
resolveSchemeFromEnv({
LUX_PROFILE: 'quasar-strict-pq',
MPC_SCHEME: scheme,
} as never),
).toThrow(/incompatible/);
}
});
it('fails-closed on unset LUX_PROFILE (no implicit ECDSA default)', () => {
// Red HIGH #4 closure: missing env used to mean classical signing
// on a strict-PQ deployment. There is no implicit default; the
// resolver throws explicit remediation text.
expect(() => resolveSchemeFromEnv({} as never)).toThrow(/LUX_PROFILE must be set/);
});
});
describe('e2e custody MPC bus — composition contract: signer can be re-bound by public key', () => {
// double-lattice exposes the inner Pulsar / Corona via readonly
// fields. Composition (not inheritance) means a caller can also
// independently verify a sub-signature against the combined
// signature's split.
it('exposes inner pulsar + corona; sub-sig split is unambiguous', async () => {
const p = makeLatticeMock('pulsar');
const c = makeLatticeMock('corona');
const signer = new DoubleLatticeSigner({
pulsar: { transport: p.transport },
corona: { transport: c.transport },
});
await signer.keygenInit({ threshold: 2, participants: 3 });
expect(signer.pulsar).toBeInstanceOf(PulsarSigner);
expect(signer.corona).toBeInstanceOf(CoronaSigner);
const message = m32(0x88);
const sig = await signer.sign(message);
const pulsarLen = (sig[0] << 24) | (sig[1] << 16) | (sig[2] << 8) | sig[3];
const pulsarSig = sig.subarray(4, 4 + pulsarLen);
const coronaSig = sig.subarray(4 + pulsarLen);
// Verify each lane via the exposed inner signer.
expect(await signer.pulsar.verify(message, pulsarSig)).toBe(true);
expect(await signer.corona.verify(message, coronaSig)).toBe(true);
});
});