Skip to content

fix(core-contract): register_resolver has no caller authentication — critical auth bypass#244

Open
Userhorlie wants to merge 5 commits intoAlien-Protocol:mainfrom
Userhorlie:fix/core-register-resolver-auth
Open

fix(core-contract): register_resolver has no caller authentication — critical auth bypass#244
Userhorlie wants to merge 5 commits intoAlien-Protocol:mainfrom
Userhorlie:fix/core-register-resolver-auth

Conversation

@Userhorlie
Copy link
Copy Markdown
Contributor

@Userhorlie Userhorlie commented Mar 27, 2026

close #173

Summary by CodeRabbit

  • New Features

    • Added zero-knowledge proof-based username availability checking.
  • Security

    • Enforced authentication requirement in resolver registration.
  • Tests

    • Added test coverage for availability checking functionality.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Warning

Rate limit exceeded

@Userhorlie has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 52 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 52 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c965664c-07a9-4914-8fb9-147de9fe4851

📥 Commits

Reviewing files that changed from the base of the PR and between b45ae69 and c8db627.

📒 Files selected for processing (1)
  • gateway-contract/contracts/core_contract/src/lib.rs
📝 Walkthrough

Walkthrough

This PR addresses a critical authentication vulnerability in the register_resolver contract function by adding caller.require_auth() enforcement, and introduces zk-based username availability checking through a new SDK module using Merkle non-inclusion proofs with corresponding tests.

Changes

Cohort / File(s) Summary
Authentication Fix
gateway-contract/contracts/core_contract/src/lib.rs
Added caller.require_auth() call at the start of register_resolver to enforce caller authentication before processing resolver registration. No changes to downstream logic or error handling.
Username Availability Checking
zk/sdk/src/availability.ts
Introduced new isUsernameAvailable() async function and SMTData interface for zk-based username availability verification via Merkle non-inclusion proof. Includes hashing, proof generation with snarkjs Groth16, verification key fetching, and proof verification.
Availability Tests
zk/sdk/src/__tests__/availability.test.ts
Added test suite for isUsernameAvailable() covering two scenarios: valid username check returning boolean, and existing username returning false.
Module Exports
zk/sdk/src/index.ts
Added wildcard re-export from ./availability module to expose new public API exports.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant SDK as isUsernameAvailable
    participant Hash as Username Hashing
    participant Proof as Proof Generation
    participant Circuit as snarkjs Circuit
    participant VKey as Verification Key
    participant Verify as groth16.verify

    Client->>SDK: isUsernameAvailable(username, smtRoot, merkleTree)
    SDK->>Hash: hashUsername(username)
    Hash-->>SDK: usernameHash
    SDK->>Proof: generateNonInclusionProof(usernameHash, smtRoot, merkleTree)
    Proof-->>SDK: proof, witness inputs
    SDK->>Circuit: fullProve(inputs, wasm, zkey)
    Circuit-->>SDK: { proof, publicSignals }
    SDK->>VKey: fetchVerificationKey()
    VKey-->>SDK: vkey (JSON)
    SDK->>Verify: groth16.verify(vkey, publicSignals, proof)
    Verify-->>SDK: boolean result
    SDK-->>Client: true/false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Issue #173: This PR directly addresses the critical authentication bypass by implementing the required caller.require_auth() check in register_resolver as the first operation.
  • Issue #201: This PR implements the isUsernameAvailable(username, smtRoot, merkleTree) function with accompanying tests and SDK module exports as specified.

Possibly related PRs

Suggested labels

Approved

Suggested reviewers

  • ryzen-xp

Poem

🐰 A rabbit hops with joy so bright,
Two features merged—auth, ZK light!
No more unguarded gates shall stand,
Usernames checked by proof so grand.
The code flows clean, the bugs now flight! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially addresses #173 requirements: caller.require_auth() was added as the first line of register_resolver [#173], but the required test test_register_resolver_unauthenticated_fails is missing from the changes shown. Add the test_register_resolver_unauthenticated_fails test case to gateway-contract/contracts/core_contract/src/test.rs to verify unauthenticated calls panic as required by #173.
Out of Scope Changes check ⚠️ Warning The PR includes unrelated changes: a new username availability check feature (zk/sdk/src/availability.ts, zk/sdk/src/tests/availability.test.ts, zk/sdk/src/index.ts exports) not mentioned in issue #173. Remove or separate the username availability feature changes into a different PR. This PR should focus solely on fixing the register_resolver authentication issue as specified in #173.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding caller authentication to register_resolver to fix a critical auth bypass vulnerability.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
zk/sdk/src/availability.ts (2)

5-10: Type nodes: any loses type safety.

The SMTData interface uses any for nodes, which bypasses TypeScript's type checking. Consider using a more specific type or generic constraint, especially since the actual circuit inputs require specific fields per NonInclusionInput in types.ts.

💡 Consider aligning with existing types
+import type { NonInclusionInput } from "./types";
+
 export interface SMTData {
-  // shape depends on your tree implementation
-  // keep generic for flexibility
-  nodes: any;
+  nodes: Record<string, string>;
   depth: number;
 }

Or use a generic:

export interface SMTData<T = Record<string, unknown>> {
  nodes: T;
  depth: number;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/availability.ts` around lines 5 - 10, The SMTData interface
currently declares nodes: any which loses type safety; change SMTData to be
generic (e.g., SMTData<T = Record<string, unknown>>) or use a concrete type that
matches your circuit inputs (align with NonInclusionInput in types.ts) so nodes
has a precise shape; update all usages of SMTData (e.g., function signatures,
variables) to supply the appropriate generic type or the concrete type so the
compiler enforces the expected fields and prevents accidental misuse.

43-46: Swallowing errors and returning false may hide critical failures.

Returning false on any error conflates "username is taken" with "system failure" (e.g., missing circuit files, network errors). Callers cannot distinguish between a valid negative result and an infrastructure problem.

Consider throwing or returning a discriminated result type for non-recoverable errors.

💡 Suggested discriminated result type
type AvailabilityResult = 
  | { status: "available"; proof: Groth16Proof }
  | { status: "taken" }
  | { status: "error"; error: Error };

export async function isUsernameAvailable(
  username: string,
  smtRoot: bigint,
  merkleTree: SMTData
): Promise<AvailabilityResult> {
  try {
    // ... proof generation
    if (isValid) {
      return { status: "available", proof };
    }
    return { status: "taken" };
  } catch (err) {
    console.error("Username availability check failed:", err);
    return { status: "error", error: err as Error };
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/availability.ts` around lines 43 - 46, The current catch block in
isUsernameAvailable swallows errors and returns false, which conflates "taken"
with infrastructure failures; change the function to return a discriminated
result type (e.g., AvailabilityResult with variants {status:"available", proof:
Groth16Proof} | {status:"taken"} | {status:"error", error: Error}) instead of a
boolean, update isUsernameAvailable to return {status:"available", proof} when
the proof validates, {status:"taken"} when it does not, and in the catch log the
original error via console.error and return {status:"error", error: err as
Error}; adjust any callers of isUsernameAvailable to handle the new
AvailabilityResult.
zk/sdk/src/__tests__/availability.test.ts (1)

24-32: Test relies on implementation detail (error path) rather than explicit behavior.

This test passes because isUsernameAvailable catches all errors and returns false. The test description "returns false when proof fails" suggests testing proof verification failure, but it's actually testing that missing circuit artifacts trigger the catch block.

For meaningful coverage, mock the dependencies and explicitly test:

  1. When groth16.verify returns false
  2. When generateNonInclusionProof throws
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/__tests__/availability.test.ts` around lines 24 - 32, The test is
relying on a catch-all error path instead of explicitly exercising proof-failure
and generator-error behaviors; update the tests for isUsernameAvailable to mock
dependencies directly: create one test where you stub groth16.verify to return
false (ensure generateNonInclusionProof returns a valid proof object) and assert
isUsernameAvailable resolves to false, and a separate test where you stub
generateNonInclusionProof to throw (and optionally have groth16.verify not
called) and assert isUsernameAvailable resolves to false; use
jest.mock/jest.spyOn to mock groth16.verify and generateNonInclusionProof,
restore mocks after each test, and keep test descriptions matching the explicit
scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gateway-contract/contracts/core_contract/src/lib.rs`:
- Around line 67-89: There are formatting/whitespace issues around the commit
block (lines with env.storage().persistent().has, panic_with_error!,
smt_root::SmtRoot::get_root, public_signals.old_root,
zk_verifier::ZkVerifier::verify_groth16_proof, ResolveData,
env.storage().persistent().set, and smt_root::SmtRoot::update_root) causing
cargo fmt --check to fail; run rustfmt (or execute `cargo fmt` in the
core_contract crate) to fix trailing whitespace and indentation, then re-run the
CI check and commit the formatted changes.

In `@zk/sdk/src/__tests__/availability.test.ts`:
- Around line 13-22: The test "returns true for username not in tree" asserts
only a boolean but actually exercises the catch path because circuit artifacts
and helpers are not mocked; update the test to either (A) mock
snarkjs.groth16.fullProve and groth16.verify, hashUsername,
generateNonInclusionProof, and the fetch for the verification key so
isUsernameAvailable("new_user_123", mockRoot, mockTree) executes the happy path
and assert result === true, or (B) change the test name and expectation to
reflect that it only verifies a boolean/handles the error path; target symbols:
isUsernameAvailable, groth16.fullProve, groth16.verify, hashUsername,
generateNonInclusionProof, and the global fetch used to load the verification
key.

In `@zk/sdk/src/availability.ts`:
- Around line 32-36: The code in isUsernameAvailable uses hardcoded relative
circuit files ("circuits/merkle_non_inclusion.wasm",
"circuits/merkle_non_inclusion.zkey"); change it to use the existing
MerkleProofGenerator and MerkleProofGeneratorConfig instead or accept circuit
paths via configuration: add a parameter of type MerkleProofGeneratorConfig to
isUsernameAvailable, instantiate new MerkleProofGenerator(config) and call its
proveNonInclusion (or equivalent) to obtain proof/publicSignals, and remove the
hardcoded groth16.fullProve call; also update imports to include
MerkleProofGenerator and MerkleProofGeneratorConfig from "./proof" or "./types"
as appropriate.
- Around line 52-56: The fetchVerificationKey function uses browser-only fetch
with an absolute path which fails in Node.js and assumes an HTTP endpoint;
modify fetchVerificationKey to be environment-agnostic by either (a) accepting
the verification key (or its JSON string) as a parameter to avoid I/O, or (b)
detect runtime (Node vs browser) and if Node use fs.readFile to load
"circuits/merkle_non_inclusion_vkey.json" and JSON.parse it, otherwise use fetch
and res.json(); update callers of fetchVerificationKey accordingly (or add an
overload) so the verification key can be injected when running in non-browser
environments.
- Around line 1-3: The import of generateNonInclusionProof from a non-existent
./merkleProofGenerator should be replaced with the actual implementation in
proof.ts: import the MerkleProofGenerator symbol from "./proof" and update
availability.ts to call the correct method (either
MerkleProofGenerator.generateNonInclusionProof if static, or instantiate new
MerkleProofGenerator(...) and call its generateNonInclusionProof method)
wherever generateNonInclusionProof is referenced so the module resolves at
runtime.

---

Nitpick comments:
In `@zk/sdk/src/__tests__/availability.test.ts`:
- Around line 24-32: The test is relying on a catch-all error path instead of
explicitly exercising proof-failure and generator-error behaviors; update the
tests for isUsernameAvailable to mock dependencies directly: create one test
where you stub groth16.verify to return false (ensure generateNonInclusionProof
returns a valid proof object) and assert isUsernameAvailable resolves to false,
and a separate test where you stub generateNonInclusionProof to throw (and
optionally have groth16.verify not called) and assert isUsernameAvailable
resolves to false; use jest.mock/jest.spyOn to mock groth16.verify and
generateNonInclusionProof, restore mocks after each test, and keep test
descriptions matching the explicit scenarios.

In `@zk/sdk/src/availability.ts`:
- Around line 5-10: The SMTData interface currently declares nodes: any which
loses type safety; change SMTData to be generic (e.g., SMTData<T =
Record<string, unknown>>) or use a concrete type that matches your circuit
inputs (align with NonInclusionInput in types.ts) so nodes has a precise shape;
update all usages of SMTData (e.g., function signatures, variables) to supply
the appropriate generic type or the concrete type so the compiler enforces the
expected fields and prevents accidental misuse.
- Around line 43-46: The current catch block in isUsernameAvailable swallows
errors and returns false, which conflates "taken" with infrastructure failures;
change the function to return a discriminated result type (e.g.,
AvailabilityResult with variants {status:"available", proof: Groth16Proof} |
{status:"taken"} | {status:"error", error: Error}) instead of a boolean, update
isUsernameAvailable to return {status:"available", proof} when the proof
validates, {status:"taken"} when it does not, and in the catch log the original
error via console.error and return {status:"error", error: err as Error}; adjust
any callers of isUsernameAvailable to handle the new AvailabilityResult.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8e86064-1dbf-46a4-9f65-3da430f3aa11

📥 Commits

Reviewing files that changed from the base of the PR and between 34e1d0a and b45ae69.

📒 Files selected for processing (4)
  • gateway-contract/contracts/core_contract/src/lib.rs
  • zk/sdk/src/__tests__/availability.test.ts
  • zk/sdk/src/availability.ts
  • zk/sdk/src/index.ts

Comment on lines +13 to +22
it("returns true for username not in tree", async () => {
const result = await isUsernameAvailable(
"new_user_123",
mockRoot,
mockTree as any
);

expect(typeof result).toBe("boolean");
// In real test: expect(result).toBe(true)
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test assertion does not match the test description.

The test claims to verify "returns true for username not in tree" but only asserts typeof result === "boolean". Without mocking groth16, hashUsername, and generateNonInclusionProof, this test will always hit the catch block and return false (because circuit artifacts don't exist in the test environment).

Either mock the dependencies to test the happy path, or rename the test to reflect what it actually verifies.

💡 Suggested approach with mocks
import { isUsernameAvailable } from "../availability";

// Mock dependencies
jest.mock("snarkjs", () => ({
  groth16: {
    fullProve: jest.fn().mockResolvedValue({ proof: {}, publicSignals: [] }),
    verify: jest.fn().mockResolvedValue(true),
  },
}));

jest.mock("../usernameHasher", () => ({
  hashUsername: jest.fn().mockReturnValue("mocked_hash"),
}));

jest.mock("../merkleProofGenerator", () => ({
  generateNonInclusionProof: jest.fn().mockResolvedValue({}),
}));

// Mock fetch for verification key
global.fetch = jest.fn().mockResolvedValue({
  json: () => Promise.resolve({}),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/__tests__/availability.test.ts` around lines 13 - 22, The test
"returns true for username not in tree" asserts only a boolean but actually
exercises the catch path because circuit artifacts and helpers are not mocked;
update the test to either (A) mock snarkjs.groth16.fullProve and groth16.verify,
hashUsername, generateNonInclusionProof, and the fetch for the verification key
so isUsernameAvailable("new_user_123", mockRoot, mockTree) executes the happy
path and assert result === true, or (B) change the test name and expectation to
reflect that it only verifies a boolean/handles the error path; target symbols:
isUsernameAvailable, groth16.fullProve, groth16.verify, hashUsername,
generateNonInclusionProof, and the global fetch used to load the verification
key.

Comment on lines +32 to +36
const { proof, publicSignals } = await groth16.fullProve(
input,
"circuits/merkle_non_inclusion.wasm",
"circuits/merkle_non_inclusion.zkey"
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded circuit paths don't match project conventions.

The paths "circuits/merkle_non_inclusion.wasm" and "circuits/merkle_non_inclusion.zkey" are hardcoded and relative. Per zk/sdk/src/__tests__/proof.test.ts, actual paths follow the pattern:

  • build/merkle_non_inclusion/wasm/merkle_non_inclusion_js/merkle_non_inclusion.wasm
  • build/merkle_non_inclusion/merkle_non_inclusion_final.zkey

Consider accepting paths via configuration or using the existing MerkleProofGenerator class with MerkleProofGeneratorConfig.

💡 Suggested refactor using existing MerkleProofGenerator
import { MerkleProofGenerator } from "./proof";
import type { MerkleProofGeneratorConfig } from "./types";

export async function isUsernameAvailable(
  username: string,
  smtRoot: bigint,
  merkleTree: SMTData,
  config: MerkleProofGeneratorConfig
): Promise<boolean> {
  try {
    const generator = new MerkleProofGenerator(config);
    // ... use generator.proveNonInclusion(input)
  } catch (err) {
    console.error("Username availability check failed:", err);
    return false;
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/availability.ts` around lines 32 - 36, The code in
isUsernameAvailable uses hardcoded relative circuit files
("circuits/merkle_non_inclusion.wasm", "circuits/merkle_non_inclusion.zkey");
change it to use the existing MerkleProofGenerator and
MerkleProofGeneratorConfig instead or accept circuit paths via configuration:
add a parameter of type MerkleProofGeneratorConfig to isUsernameAvailable,
instantiate new MerkleProofGenerator(config) and call its proveNonInclusion (or
equivalent) to obtain proof/publicSignals, and remove the hardcoded
groth16.fullProve call; also update imports to include MerkleProofGenerator and
MerkleProofGeneratorConfig from "./proof" or "./types" as appropriate.

Comment on lines +52 to +56
async function fetchVerificationKey() {
// adjust path depending on your setup
const res = await fetch("/circuits/merkle_non_inclusion_vkey.json");
return res.json();
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

fetch API assumes browser environment.

fetch("/circuits/merkle_non_inclusion_vkey.json") uses an absolute URL path, which:

  1. Won't work in Node.js without polyfills (Node.js fetch requires full URLs)
  2. Assumes the verification key is served at a specific web endpoint

For SDK code that may run in Node.js, consider using fs.readFile or accepting the verification key as a parameter.

💡 Suggested fix for cross-environment support
+import fs from "fs/promises";
+import path from "path";
+
 /**
  * Loads verification key (can be cached in production)
  */
-async function fetchVerificationKey() {
-  // adjust path depending on your setup
-  const res = await fetch("/circuits/merkle_non_inclusion_vkey.json");
-  return res.json();
+async function fetchVerificationKey(vkeyPath: string) {
+  if (typeof window !== "undefined") {
+    // Browser environment
+    const res = await fetch(vkeyPath);
+    return res.json();
+  } else {
+    // Node.js environment
+    const content = await fs.readFile(vkeyPath, "utf-8");
+    return JSON.parse(content);
+  }
 }

Or accept the key as a parameter to avoid file I/O assumptions entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/availability.ts` around lines 52 - 56, The fetchVerificationKey
function uses browser-only fetch with an absolute path which fails in Node.js
and assumes an HTTP endpoint; modify fetchVerificationKey to be
environment-agnostic by either (a) accepting the verification key (or its JSON
string) as a parameter to avoid I/O, or (b) detect runtime (Node vs browser) and
if Node use fs.readFile to load "circuits/merkle_non_inclusion_vkey.json" and
JSON.parse it, otherwise use fetch and res.json(); update callers of
fetchVerificationKey accordingly (or add an overload) so the verification key
can be injected when running in non-browser environments.

@ryzen-xp ryzen-xp added CI Failed Please check why you CI is faileing fix your code Code Conflict This PR have merge conflicts, Please Resolve it !!! labels Mar 27, 2026
@ryzen-xp
Copy link
Copy Markdown
Contributor

Hello @Userhorlie , fix you code why checks failed and resolve the conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI Failed Please check why you CI is faileing fix your code Code Conflict This PR have merge conflicts, Please Resolve it !!!

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

[Bug][Contract] register_resolver has no caller authentication — critical auth bypass

2 participants