Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b7c5bc7
feat: selfrica circuit and tests
Vishalkulkarni45 Oct 1, 2025
ed841d7
chore: remove unused code
Vishalkulkarni45 Oct 1, 2025
a91a955
feat: test for ofac,date and olderthan
Vishalkulkarni45 Oct 2, 2025
968a74c
fix: public signal constant
Vishalkulkarni45 Oct 2, 2025
e23322f
feat: add contract tests
Nesopie Oct 8, 2025
838bc49
feat: helper function to gen TEE input
Vishalkulkarni45 Oct 15, 2025
d51ce02
feat: gen circuit inputs with signature
Vishalkulkarni45 Oct 16, 2025
de37ae4
feat: seralized base64
Vishalkulkarni45 Oct 16, 2025
e5c445e
fix: DateIsLessFullYear componenet
Vishalkulkarni45 Oct 17, 2025
fd197fe
feat: register circuit for selfrica
Vishalkulkarni45 Oct 22, 2025
3e44266
feat: selfrica disclose circuit and test
Vishalkulkarni45 Oct 22, 2025
c290010
fix: common module error
Vishalkulkarni45 Oct 23, 2025
c39dc21
feat: add more test and fix constant
Vishalkulkarni45 Oct 23, 2025
6e5a4c7
fix: commitment calculation
Vishalkulkarni45 Oct 23, 2025
cd761a9
feat: selfrica contracts
Nesopie Oct 23, 2025
48ff517
test: selfrica register using unified circuit
Vishalkulkarni45 Oct 24, 2025
750f592
feat: register persona and selfrica circuit
Vishalkulkarni45 Oct 24, 2025
08fdae0
feat: selfrica circuit and tests
Vishalkulkarni45 Oct 1, 2025
c21543b
chore: remove unused code
Vishalkulkarni45 Oct 1, 2025
7910b87
feat: test for ofac,date and olderthan
Vishalkulkarni45 Oct 2, 2025
2804959
fix: public signal constant
Vishalkulkarni45 Oct 2, 2025
a13468a
feat: add contract tests
Nesopie Oct 8, 2025
bced45e
feat: helper function to gen TEE input
Vishalkulkarni45 Oct 15, 2025
ae6fb3a
feat: gen circuit inputs with signature
Vishalkulkarni45 Oct 16, 2025
fa03e1e
feat: seralized base64
Vishalkulkarni45 Oct 16, 2025
ceff539
fix: DateIsLessFullYear componenet
Vishalkulkarni45 Oct 17, 2025
c47769f
feat: register circuit for selfrica
Vishalkulkarni45 Oct 22, 2025
ec0e41d
feat: selfrica disclose circuit and test
Vishalkulkarni45 Oct 22, 2025
8397155
fix: common module error
Vishalkulkarni45 Oct 23, 2025
a2bc9c9
feat: add more test and fix constant
Vishalkulkarni45 Oct 23, 2025
dbcbd76
fix: commitment calculation
Vishalkulkarni45 Oct 23, 2025
be2f557
feat: selfrica contracts
Nesopie Oct 23, 2025
770bebe
test: selfrica register using unified circuit
Vishalkulkarni45 Oct 24, 2025
ae10ae4
feat: register persona and selfrica circuit
Vishalkulkarni45 Oct 24, 2025
043dd4e
Merge remote-tracking branch 'origin/feat/selfrica' into feat/selfrica
Tranquil-Flow Oct 28, 2025
74fb2f6
refactor: contract size reduction for IdentityVerificationHubImplV2
Tranquil-Flow Oct 28, 2025
dcea2b8
feat: disclose circuit for persona
Vishalkulkarni45 Nov 4, 2025
036646a
feat: update persona ofac trees
Vishalkulkarni45 Nov 4, 2025
2708539
feat; register circuit for selfper
Vishalkulkarni45 Nov 5, 2025
db1435b
feat: disclose test for selfper
Vishalkulkarni45 Nov 5, 2025
ed91479
chore: refactor
Vishalkulkarni45 Nov 7, 2025
9f767b0
chore : remove unused circuits
Vishalkulkarni45 Nov 7, 2025
7ff7d36
chore: rename selfper to kyc
Vishalkulkarni45 Nov 7, 2025
9c27329
Merge branch 'dev' into feat/selfrica
Nesopie Nov 11, 2025
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
169 changes: 169 additions & 0 deletions circuits/circuits/disclose/vc_and_disclose_selfrica.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
pragma circom 2.1.9;

include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/bitify.circom";
include "../utils/passport/signatureVerifier.circom";
include "../utils/passport/customHashers.circom";
include "@openpassport/zk-email-circuits/lib/sha.circom";
include "@openpassport/zk-email-circuits/lib/bigint.circom";
include "../utils/selfrica/constants.circom";
include "../utils/selfrica/disclose/disclose.circom";

template VC_AND_DISCLOSE(
MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH,
namedobTreeLevels,
nameyobTreeLevels,
n,
k
) {
var selfrica_length = SELFRICA_MAX_LENGTH();
var country_length = COUNTRY_LENGTH();
var compressed_bit_len = selfrica_length/2;

signal input SmileID_data_padded[SMILE_DATA_PADDED()];
signal input compressed_disclose_sel[2];

signal input pubKey[k];
signal input msg_sig[k];
signal input id_num_sig[k];

signal input scope;

signal input forbidden_countries_list[MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * country_length];

signal input ofac_name_dob_smt_leaf_key;
signal input ofac_name_dob_smt_root;
signal input ofac_name_dob_smt_siblings[namedobTreeLevels];

signal input ofac_name_yob_smt_leaf_key;
signal input ofac_name_yob_smt_root;
signal input ofac_name_yob_smt_siblings[nameyobTreeLevels];

signal input selector_ofac;
signal input user_identifier;
signal input current_date[8];
signal input majority_age_ASCII[3];
signal input selector_older_than;

signal output attestation_id <== 4;

// Convert the two decimal inputs back to bit array
signal disclose_sel[selfrica_length];

// Convert disclose_sel_low (first 133 bits) to bit array
component low_bits = Num2Bits(compressed_bit_len);
low_bits.in <== compressed_disclose_sel[0];

// Convert disclose_sel_high (next 133 bits) to bit array
component high_bits = Num2Bits(compressed_bit_len);
high_bits.in <== compressed_disclose_sel[1];

// Combine the bit arrays (little-endian format)
for(var i = 0; i < compressed_bit_len; i++){
disclose_sel[i] <== low_bits.out[i];
}
for(var i = 0; i < compressed_bit_len; i++){
disclose_sel[compressed_bit_len + i] <== high_bits.out[i];
}


//skiped paddedInLength to be within `ceil(log2(8 * maxByteLength))` bits bez we are using hardcoded values
component msg_hasher = Sha256Bytes(SMILE_DATA_PADDED());
msg_hasher.paddedIn <== SmileID_data_padded;
msg_hasher.paddedInLength <== SMILE_DATA_PADDED();

//verify Hash(smiledata) signatur
component msg_sig_verify = SignatureVerifier(1, n, k);
msg_sig_verify.hash <== msg_hasher.out;
msg_sig_verify.pubKey <== pubKey;
msg_sig_verify.signature <== msg_sig;


//Calculate IDNUMBER hash
signal id_num[SMILE_ID_PADDED()];
var idNumberIdx = ID_NUMBER_INDEX();

// Fill the first 20 bytes with actual ID number data
for (var i = 0; i < ID_NUMBER_LENGTH(); i++) {
id_num[i] <== SmileID_data_padded[idNumberIdx + i];
}

// Add SHA-256 padding for 20-byte message
// Add padding bit '1' (0x80)
id_num[ID_NUMBER_LENGTH()] <== 128; // 0x80 in decimal

// Fill with zeros up to position 56 (64 - 8 for length field)
for (var i = ID_NUMBER_LENGTH() + 1; i < SMILE_ID_PADDED() - 8; i++) {
id_num[i] <== 0;
}

// Add 64-bit length field (20 bytes * 8 = 160 bits)
// Length in bits as 64-bit big-endian integer
for (var i = SMILE_ID_PADDED() - 8; i < SMILE_ID_PADDED() - 1; i++) {
id_num[i] <== 0; // High bytes are 0 for small lengths
}
id_num[SMILE_ID_PADDED() - 1] <== 160; // 20 * 8 = 160 bits

component id_num_hasher = Sha256Bytes(SMILE_ID_PADDED());
id_num_hasher.paddedIn <== id_num;
id_num_hasher.paddedInLength <== SMILE_ID_PADDED();

//verify Hash(IdNumber) signature
component id_num_sig_verify = SignatureVerifier(1, n, k);
id_num_sig_verify.hash <== id_num_hasher.out;
id_num_sig_verify.pubKey <== pubKey;
id_num_sig_verify.signature <== id_num_sig;


// Identity Commitment = Hash( IdNumCommit sig )
component idCommCal = CustomHasher(k);
idCommCal.in <== msg_sig;

//Nullifier = HASH( nullifier sig , scope )
component nullifierCal = CustomHasher(k + 1);
for (var i = 0; i < k; i++) {
nullifierCal.in[i] <== id_num_sig[i];
}
nullifierCal.in[k] <== scope;

component disclose_circuit = DISCLOSE_SELFRICA(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH, namedobTreeLevels, nameyobTreeLevels);

for (var i = 0; i < selfrica_length; i++) {
disclose_circuit.smile_data[i] <== SmileID_data_padded[i];
}
disclose_circuit.selector_smile_data <== disclose_sel;
disclose_circuit.forbidden_countries_list <== forbidden_countries_list;

disclose_circuit.ofac_name_dob_smt_leaf_key <== ofac_name_dob_smt_leaf_key;
disclose_circuit.ofac_name_dob_smt_root <== ofac_name_dob_smt_root;
disclose_circuit.ofac_name_dob_smt_siblings <== ofac_name_dob_smt_siblings;

disclose_circuit.ofac_name_yob_smt_leaf_key <== ofac_name_yob_smt_leaf_key;
disclose_circuit.ofac_name_yob_smt_root <== ofac_name_yob_smt_root;
disclose_circuit.ofac_name_yob_smt_siblings <== ofac_name_yob_smt_siblings;

disclose_circuit.selector_ofac <== selector_ofac;
disclose_circuit.current_date <== current_date;
disclose_circuit.majority_age_ASCII <== majority_age_ASCII;
disclose_circuit.selector_older_than <== selector_older_than;

var revealed_data_packed_chunk_length = computeIntChunkLength(selfrica_length + 2 + 3);
signal output revealedData_packed[revealed_data_packed_chunk_length] <== disclose_circuit.revealedData_packed;

var forbidden_countries_list_packed_chunk_length = computeIntChunkLength(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * country_length);
signal output forbidden_countries_list_packed[forbidden_countries_list_packed_chunk_length] <== disclose_circuit.forbidden_countries_list_packed;

signal output identity_commitment <== idCommCal.out;
signal output nullifier <== nullifierCal.out;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bind user_identifier to the derived identity commitment

user_identifier is declared as a public input but never constrained, so a prover can emit any identifier while the circuit still verifies, defeating downstream identity binding. Tie it to idCommCal.out (or whichever commitment you expect verifiers to trust).

     component idCommCal = CustomHasher(k);
     idCommCal.in <== msg_sig;
+    user_identifier === idCommCal.out;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
signal input selector_ofac;
signal input user_identifier;
signal input current_date[8];
signal input majority_age_ASCII[3];
signal input selector_older_than;
signal output attestation_id <== 4;
// Convert the two decimal inputs back to bit array
signal disclose_sel[selfrica_length];
// Convert disclose_sel_low (first 133 bits) to bit array
component low_bits = Num2Bits(compressed_bit_len);
low_bits.in <== compressed_disclose_sel[0];
// Convert disclose_sel_high (next 133 bits) to bit array
component high_bits = Num2Bits(compressed_bit_len);
high_bits.in <== compressed_disclose_sel[1];
// Combine the bit arrays (little-endian format)
for(var i = 0; i < compressed_bit_len; i++){
disclose_sel[i] <== low_bits.out[i];
}
for(var i = 0; i < compressed_bit_len; i++){
disclose_sel[compressed_bit_len + i] <== high_bits.out[i];
}
//skiped paddedInLength to be within `ceil(log2(8 * maxByteLength))` bits bez we are using hardcoded values
component msg_hasher = Sha256Bytes(SMILE_DATA_PADDED());
msg_hasher.paddedIn <== SmileID_data_padded;
msg_hasher.paddedInLength <== SMILE_DATA_PADDED();
//verify Hash(smiledata) signatur
component msg_sig_verify = SignatureVerifier(1, n, k);
msg_sig_verify.hash <== msg_hasher.out;
msg_sig_verify.pubKey <== pubKey;
msg_sig_verify.signature <== msg_sig;
//Calculate IDNUMBER hash
signal id_num[SMILE_ID_PADDED()];
var idNumberIdx = ID_NUMBER_INDEX();
// Fill the first 20 bytes with actual ID number data
for (var i = 0; i < ID_NUMBER_LENGTH(); i++) {
id_num[i] <== SmileID_data_padded[idNumberIdx + i];
}
// Add SHA-256 padding for 20-byte message
// Add padding bit '1' (0x80)
id_num[ID_NUMBER_LENGTH()] <== 128; // 0x80 in decimal
// Fill with zeros up to position 56 (64 - 8 for length field)
for (var i = ID_NUMBER_LENGTH() + 1; i < SMILE_ID_PADDED() - 8; i++) {
id_num[i] <== 0;
}
// Add 64-bit length field (20 bytes * 8 = 160 bits)
// Length in bits as 64-bit big-endian integer
for (var i = SMILE_ID_PADDED() - 8; i < SMILE_ID_PADDED() - 1; i++) {
id_num[i] <== 0; // High bytes are 0 for small lengths
}
id_num[SMILE_ID_PADDED() - 1] <== 160; // 20 * 8 = 160 bits
component id_num_hasher = Sha256Bytes(SMILE_ID_PADDED());
id_num_hasher.paddedIn <== id_num;
id_num_hasher.paddedInLength <== SMILE_ID_PADDED();
//verify Hash(IdNumber) signature
component id_num_sig_verify = SignatureVerifier(1, n, k);
id_num_sig_verify.hash <== id_num_hasher.out;
id_num_sig_verify.pubKey <== pubKey;
id_num_sig_verify.signature <== id_num_sig;
// Identity Commitment = Hash( IdNumCommit sig )
component idCommCal = CustomHasher(k);
idCommCal.in <== msg_sig;
//Nullifier = HASH( nullifier sig , scope )
component nullifierCal = CustomHasher(k + 1);
for (var i = 0; i < k; i++) {
nullifierCal.in[i] <== id_num_sig[i];
}
nullifierCal.in[k] <== scope;
component disclose_circuit = DISCLOSE_SELFRICA(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH, namedobTreeLevels, nameyobTreeLevels);
for (var i = 0; i < selfrica_length; i++) {
disclose_circuit.smile_data[i] <== SmileID_data_padded[i];
}
disclose_circuit.selector_smile_data <== disclose_sel;
disclose_circuit.forbidden_countries_list <== forbidden_countries_list;
disclose_circuit.ofac_name_dob_smt_leaf_key <== ofac_name_dob_smt_leaf_key;
disclose_circuit.ofac_name_dob_smt_root <== ofac_name_dob_smt_root;
disclose_circuit.ofac_name_dob_smt_siblings <== ofac_name_dob_smt_siblings;
disclose_circuit.ofac_name_yob_smt_leaf_key <== ofac_name_yob_smt_leaf_key;
disclose_circuit.ofac_name_yob_smt_root <== ofac_name_yob_smt_root;
disclose_circuit.ofac_name_yob_smt_siblings <== ofac_name_yob_smt_siblings;
disclose_circuit.selector_ofac <== selector_ofac;
disclose_circuit.current_date <== current_date;
disclose_circuit.majority_age_ASCII <== majority_age_ASCII;
disclose_circuit.selector_older_than <== selector_older_than;
var revealed_data_packed_chunk_length = computeIntChunkLength(selfrica_length + 2 + 3);
signal output revealedData_packed[revealed_data_packed_chunk_length] <== disclose_circuit.revealedData_packed;
var forbidden_countries_list_packed_chunk_length = computeIntChunkLength(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * country_length);
signal output forbidden_countries_list_packed[forbidden_countries_list_packed_chunk_length] <== disclose_circuit.forbidden_countries_list_packed;
signal output identity_commitment <== idCommCal.out;
signal output nullifier <== nullifierCal.out;
// Identity Commitment = Hash( IdNumCommit sig )
component idCommCal = CustomHasher(k);
idCommCal.in <== msg_sig;
user_identifier === idCommCal.out;
🤖 Prompt for AI Agents
In circuits/circuits/disclose/vc_and_disclose_selfrica.circom around lines 42 to
158, user_identifier is declared as a public input but never constrained; add a
binding after idCommCal is computed to force the public input to equal the
derived identity commitment (i.e., constrain user_identifier to idCommCal.out).
Ensure the types/sizes match (convert or index if idCommCal.out is an array) so
the circuit enforces user_identifier <== idCommCal.out (or element-wise
equality) immediately after idCommCal is available.

}

component main {
public [
scope,
user_identifier,
current_date,
ofac_name_dob_smt_root,
ofac_name_yob_smt_root
]
} = VC_AND_DISCLOSE(40, 64, 64, 121, 17);
109 changes: 109 additions & 0 deletions circuits/circuits/utils/selfrica/constants.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
pragma circom 2.1.9;

function COUNTRY_INDEX() {
return 0;
}

function COUNTRY_LENGTH() {
return 3;
}

function ID_TYPE_INDEX() {
return COUNTRY_INDEX() + COUNTRY_LENGTH();
}

function ID_TYPE_LENGTH() {
return 27;
}

function ID_NUMBER_INDEX() {
return ID_TYPE_INDEX() + ID_TYPE_LENGTH();
}

function ID_NUMBER_LENGTH() {
return 20;
}

function ISSUANCE_DATE_INDEX() {
return ID_NUMBER_INDEX() + ID_NUMBER_LENGTH();
}

function ISSUANCE_DATE_LENGTH() {
return 8;
}

function EXPIRATION_DATE_INDEX() {
return ISSUANCE_DATE_INDEX() + ISSUANCE_DATE_LENGTH();
}

function EXPIRATION_DATE_LENGTH() {
return 8;
}

function FULL_NAME_INDEX() {
return EXPIRATION_DATE_INDEX() + EXPIRATION_DATE_LENGTH();
}

function FULL_NAME_LENGTH() {
return 40;
}

function DOB_INDEX() {
return FULL_NAME_INDEX() + FULL_NAME_LENGTH();
}

function DOB_LENGTH() {
return 8;
}

function PHOTO_HASH_INDEX() {
return DOB_INDEX() + DOB_LENGTH();
}

function PHOTO_HASH_LENGTH() {
return 32;
}

function PHONE_NUMBER_INDEX() {
return PHOTO_HASH_INDEX() + PHOTO_HASH_LENGTH();
}

function PHONE_NUMBER_LENGTH() {
return 12;
}

function DOCUMENT_INDEX() {
return PHONE_NUMBER_INDEX() + PHONE_NUMBER_LENGTH();
}

function DOCUMENT_LENGTH() {
return 2;
}

function GENDER_INDEX() {
return DOCUMENT_INDEX() + DOCUMENT_LENGTH();
}

function GENDER_LENGTH() {
return 6;
}

function ADDRESS_INDEX() {
return GENDER_INDEX() + GENDER_LENGTH();
}

function ADDRESS_LENGTH() {
return 100;
}

function SELFRICA_MAX_LENGTH() {
return ADDRESS_INDEX() + ADDRESS_LENGTH();
}

function SMILE_DATA_PADDED() {
return 320;
}

function SMILE_ID_PADDED() {
return 64;
}
65 changes: 65 additions & 0 deletions circuits/circuits/utils/selfrica/date/dateIsLess.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
pragma circom 2.1.9;

include "circomlib/circuits/comparators.circom";

/// @title DateIsLess
/// @notice compares two dates in the YYYYMMDD numeric format
/// @param day_1 is the day of the first date
/// @param day_2 is the day of the second date
/// @param month_1 is the month of the first date
/// @param month_2 is the month of the second date
/// @param year_1 is the year of the first date
/// @param year_2 is the year of the second date
/// @output out is the result of the comparison
/// @dev output is not constrained — verifier has to handle this check

template DateIsLessFullYear() {
signal input day_1;
signal input day_2;

signal input month_1;
signal input month_2;

signal input year_1;
signal input year_2;

signal output out;

// ----
component year_less = LessThan(8);
year_less.in[0] <== year_1;
year_less.in[1] <== year_2;
signal is_year_less <== year_less.out;

component month_less = LessThan(8);
month_less.in[0] <== month_1;
month_less.in[1] <== month_2;
signal is_month_less <== month_less.out;

component day_less = LessThan(8);
day_less.in[0] <== day_1;
day_less.in[1] <== day_2;
signal is_day_less <== day_less.out;

// ----
component year_equal = IsEqual();
year_equal.in[0] <== year_1;
year_equal.in[1] <== year_2;
signal is_year_equal <== year_equal.out;

component month_equal = IsEqual();
month_equal.in[0] <== month_1;
month_equal.in[1] <== month_2;
signal is_month_equal <== month_equal.out;

// ----
signal is_year_equal_and_month_less <== (is_year_equal * is_month_less);
signal is_year_equal_and_month_equal <== (is_year_equal * is_month_equal);
signal is_year_equal_and_month_equal_and_day_less <== (is_year_equal_and_month_equal * is_day_less);

component greater_than = GreaterThan(3);
greater_than.in[0] <== is_year_less + is_year_equal_and_month_less + is_year_equal_and_month_equal_and_day_less;
greater_than.in[1] <== 0;

out <== greater_than.out;
}
Comment on lines 16 to 64
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

No input range validation for day, month, year.

The circuit does not constrain inputs to valid ranges (day 1-31, month 1-12, year > 0). Invalid dates like 2024-02-30 or 2024-13-01 could produce incorrect comparison results. Ensure the caller (e.g., IsValidFullYear) validates date components before invoking this template, or add range checks here.

Consider adding range constraints or verify that IsValidFullYear enforces them:

// Example range constraints (add to template)
component day1_valid = LessThan(8);
day1_valid.in[0] <== day_1;
day1_valid.in[1] <== 32;  // day must be < 32
day1_valid.out === 1;

component month1_valid = LessThan(8);
month1_valid.in[0] <== month_1;
month1_valid.in[1] <== 13;  // month must be < 13
month1_valid.out === 1;

// Similar for day_2, month_2
🤖 Prompt for AI Agents
In circuits/circuits/utils/selfrica/date/dateIsLess.circom around lines 16-65,
the template accepts day/month/year inputs without validating ranges; add
explicit range checks for both date inputs (day in 1..31, month in 1..12, year >
0) or call/require the existing IsValidFullYear verifier before using this
template. Implement these checks by adding comparator components (e.g.,
LessThan/GreaterThan or custom range check) for day_1, month_1, year_1 and
day_2, month_2, year_2 and assert their outputs (e.g., out === 1) so invalid
values fail the circuit; alternatively, document and enforce that callers must
run IsValidFullYear on both dates before invoking DateIsLessFullYear.

Loading
Loading