Skip to content

Commit e1670dd

Browse files
committed
fix: debugged integration of feature into didmultisigcontroler contract, finalized readme for setup, usage, and testing
1 parent c862c4b commit e1670dd

7 files changed

Lines changed: 1391 additions & 352 deletions

File tree

packages/trust-anchor-did-ethr/.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,8 @@ build/
6161

6262
# --- Testing ---
6363
reports/
64-
.nyc_output/
64+
.nyc_output/
65+
66+
# Ignore files generated by circom-ecdsa fork in order to comply with gpl-3 license
67+
contracts/verifiers/*
68+
circom-zkp-generator/*

packages/trust-anchor-did-ethr/README.md

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,29 @@
1515
4. only all trust anchor admins together can call the changeOwner function of their companys' did:ethr identifier (stage of feature: Implemented, Tested, Reviewed)
1616
5. one trust anchor admin uses the deployed DIDMultisigController contract of its trust anchor to add a static pointer to a smart contract that enables company admins to change the CID of BFC of company's CRSet by calling the setAttribute function on the "service" section of the company's did:ethr identifier (stage of feature: Implemented, Tested, Reviewed)
1717

18+
## Desired feature for private digital asset publication
19+
This feature of the DIDMultisigController smart contract enables a company admin to show a Zero-Knowledge Proof that authorizes the DIDMultisigController to act as a relayer and digital asset publisher without revealing the identity of the authorizing company admin.
20+
21+
### Set up and testing private digital asset publication
22+
To unlock the feature for private digital asset publication, the Zero-Knowledge Proof generator must be added to this ``trust-anchor-did-ethr`` package, which can be done following the steps described in the ``README`` of the [ASCS circom-ecdsa fork](https://github.com/ASCS-eV/circom-ecdsa?tab=readme-ov-file#private-secure-on-chain-group-signature-verification-with-variable-group-size). After adding the Zero-Knowledge Proof generator to the folder `./circom-zkp-generator`, the feature for private digital asset publicaiton can be tested by running `npx hardhat test ./test/DIDMultisigController.privatePublish.test.ts` in the terminal while in the folder `./packages/trust-anchor-did-ethr`.
23+
24+
### Workflow of private digital asset publication
25+
1. **Preparation**: The Trust Anchor admin uses the [ASCS circom-ecdsa fork](https://github.com/ASCS-eV/circom-ecdsa) to create ZKP-verifier smart contracts (see `./contracts/verifiers`) and ZKP generator (see `./circom-zkp-generator`) specific to different group sizes. To create both, follow the [instructions from above](#set-up-and-testing-private-digital-asset-publication).
26+
2. **Registration**: The Trust Anchor deploys these verifier smart contracts and stores their addresses in the `verifiers` mapping within the `DIDMultisigController`.
27+
3. **Generation**: Company admins (or a DApp) generate a ZKP off-chain using the ASCS toolset (see `./circom-zkp-generator`).
28+
4. **Publication**: Admins call `privatelyPublishMarketplaceData` to publish digital data assets to the ASCS marketplace (simulated via `DigitalAssetMarketplaceStub`).
29+
30+
**Note:** see test script `./test/DIDMultisigController.privatePublish.test.ts` to understand the code behind the workflow steps of *registration*, *generation*, and *publication*.
31+
32+
### Improvements for future
33+
The goal is to make verifier registration obsolete by making membership proofs independent of group size, as discussed in the [ASCS circom-ecdsa fork](https://github.com/ASCS-eV/circom-ecdsa) under "Membership Proof Is O(m)".
34+
35+
Alternatively, a **Fixed-Size Padding** model could be used:
36+
* **Simpler contract architecture**: Only one verifier contract (e.g., fixed at 100 slots) is maintained.
37+
* **Easier extensibility**: Smaller groups are padded with null-address placeholders.
38+
* **Reduced complexity**: Client-side logic remains consistent across all group sizes.
39+
* **Lower overhead**: Fewer artifacts (WASM, zkey) need to be managed.
40+
1841
## Desired security features for production
1942
### ... for identity of trust anchor
2043
1. trust anchor admins cannot administer company DID of other trust anchor
@@ -24,45 +47,10 @@
2447
1. company admins cannot administer DID of trust anchor or other company
2548
2. ...
2649

27-
## Usage of Hardhat
28-
29-
### Running Tests
30-
31-
To run all the tests in the project, execute the following command:
32-
33-
```shell
34-
npx hardhat test
35-
```
36-
37-
You can also selectively run the Solidity or `node:test` tests:
38-
39-
```shell
40-
npx hardhat test solidity
41-
npx hardhat test nodejs
42-
```
43-
44-
### Make a deployment to Sepolia
45-
46-
This project includes an example Ignition module to deploy the contract. You can deploy this module to a locally simulated chain or to Sepolia.
47-
48-
To run the deployment to a local chain:
49-
50-
```shell
51-
npx hardhat ignition deploy ignition/modules/Counter.ts
52-
```
53-
54-
To run the deployment to Sepolia, you need an account with funds to send the transaction. The provided Hardhat configuration includes a Configuration Variable called `SEPOLIA_PRIVATE_KEY`, which you can use to set the private key of the account you want to use.
55-
56-
You can set the `SEPOLIA_PRIVATE_KEY` variable using the `hardhat-keystore` plugin or by setting it as an environment variable.
57-
58-
To set the `SEPOLIA_PRIVATE_KEY` config variable using `hardhat-keystore`:
59-
60-
```shell
61-
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
62-
```
50+
## Acknowledgements
51+
We extend our gratitude to **0xParc** for their pioneering work on [circom-ecdsa](https://github.com/0xPARC/circom-ecdsa). Their implementation served as the foundational building block for the ZKP-based private digital asset publication system featured in this repository.
6352

64-
After setting the variable, you can run the deployment with the Sepolia network:
53+
To meet the specific requirements of the `trust-anchor-did-ethr` software system, we have adapted 0xParc's original code within our own [fork of circom-ecdsa](https://github.com/ASCS-eV/circom-ecdsa). This fork is instrumental in our workflow, specifically for:
6554

66-
```shell
67-
npx hardhat ignition deploy --network sepolia ignition/modules/Counter.ts
68-
```
55+
* **Verifier Smart Contracts**: Generating the ZK-SNARK verification logic located in `./contracts/verifiers`.
56+
* **ZKP Artifacts**: Producing the circuit compilation and proving keys found within the `./circom-zkp-generator` directory.

packages/trust-anchor-did-ethr/contracts/DIDMultisigController.sol

Lines changed: 217 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@ interface IDigitalAssetMarketplaceStub {
1414
function publishData(string calldata data, address assetOwner) external;
1515
}
1616

17+
// Interface for Groth16 verifier of group with size 2 to 4
18+
interface IVerifierM2 {
19+
function verifyProof(
20+
uint256[2] calldata a,
21+
uint256[2][2] calldata b,
22+
uint256[2] calldata c,
23+
uint256[6] calldata input
24+
) external view returns (bool);
25+
}
26+
27+
interface IVerifierM3 {
28+
function verifyProof(
29+
uint256[2] calldata a,
30+
uint256[2][2] calldata b,
31+
uint256[2] calldata c,
32+
uint256[8] calldata input
33+
) external view returns (bool);
34+
}
35+
36+
interface IVerifierM4 {
37+
function verifyProof(
38+
uint256[2] calldata a,
39+
uint256[2][2] calldata b,
40+
uint256[2] calldata c,
41+
uint256[8] calldata input
42+
) external view returns (bool);
43+
}
44+
1745
contract DIDMultisigController {
1846
/*//////////////////////////////////////////////////////////////
1947
STORAGE
@@ -35,6 +63,8 @@ contract DIDMultisigController {
3563
}
3664

3765
mapping(bytes32 => Proposal) public proposals;
66+
mapping(uint256 => bool) public usedNonces; // For tracking used nonces in private publishing of digital data assets
67+
mapping(uint256 => address) public verifiers; // maps group size to respectively responsible zkp verifier contract address
3868

3969
/*//////////////////////////////////////////////////////////////
4070
EVENTS
@@ -270,7 +300,8 @@ contract DIDMultisigController {
270300
}
271301

272302
/*//////////////////////////////////////////////////////////////
273-
5. COMPANY ADMIN ACTIONS VIA MULTISIG
303+
5. COMPANY ADMIN ACTION:
304+
Public Publishing of Digital Data Assets
274305
//////////////////////////////////////////////////////////////*/
275306

276307
// verifiable delegated execution of publishing data to a marketplace (WITHOUT privacy of company admin!)
@@ -331,34 +362,200 @@ contract DIDMultisigController {
331362
return ecrecover(ethSignedMessageHash, v, r, s);
332363
}
333364

334-
function privatelyPublishMarketplaceData(
335-
address marketplace,
336-
string calldata data,
337-
address company,
338-
address[] calldata companyAdmins
339-
) external onlyOwner {
365+
/*//////////////////////////////////////////////////////////////
366+
6. COMPANY ADMIN ACTION:
367+
Privately Publishing of Digital Data Assets
368+
//////////////////////////////////////////////////////////////*/
340369

341-
// require at least 1 company admin
342-
require(companyAdmins.length > 0, "no_company_admins");
370+
// Register or update verifier for a specific group size
371+
function setVerifier(uint256 m, address verifier) external onlyOwner {
372+
require(verifier != address(0), "invalid_verifier");
373+
verifiers[m] = verifier;
374+
}
375+
376+
// Remove a verifier
377+
function removeVerifier(uint256 m) external onlyOwner {
378+
delete verifiers[m];
379+
}
343380

344-
// require all provided company admins to be valid delegates
345-
for (uint i = 0; i < companyAdmins.length; i++) {
381+
struct ZKProofInput {
382+
address marketplace;
383+
string cid;
384+
address company;
385+
address[] companyAdmins;
386+
uint256 inputNonce;
387+
uint256[2] a;
388+
uint256[2][2] b;
389+
uint256[2] c;
390+
uint256[] input;
391+
}
392+
393+
function privatelyPublishMarketplaceData(ZKProofInput calldata zkInput) external onlyOwner{
394+
uint256 m = zkInput.companyAdmins.length;
395+
396+
require(m > 0, "no_admins");
397+
require(!usedNonces[zkInput.inputNonce], "nonce_used"); // protect against replay attacks by ensuring nonce is unique and unused
398+
399+
/* ---------------------------------------------------------
400+
1. Validate admins via registry
401+
----------------------------------------------------------*/
402+
for (uint256 i = 0; i < m; i++) {
346403
require(
347404
registry.validDelegate(
348-
company,
405+
zkInput.company,
349406
keccak256("CompanyAdmin"),
350-
companyAdmins[i]
407+
zkInput.companyAdmins[i]
351408
),
352-
"invalid_list_of_company_admins"
409+
"invalid_company_admin"
353410
);
354411
}
355412

356-
// verify ZKP that proves that...
357-
// 1. the trust anchor knows a signature over the message authorizing the publication of data
358-
// 2. one of the company admins signed the message authorizing the publication of data
359-
// 3. without revealing which company admin signed the message
413+
/* ---------------------------------------------------------
414+
2. Get ZKP verifier for group size m
415+
----------------------------------------------------------*/
416+
address verifierAddr = verifiers[m];
417+
require(verifierAddr != address(0), "verifier_not_set");
418+
419+
/* ---------------------------------------------------------
420+
3. Compute message hash (must match circuit)
421+
----------------------------------------------------------*/
422+
(uint256 hi, uint256 lo) = hashPublishMessage(
423+
zkInput.inputNonce,
424+
zkInput.cid,
425+
zkInput.company,
426+
zkInput.marketplace
427+
);
360428

361-
// publish asset with company as owner
362-
IDigitalAssetMarketplaceStub(marketplace).publishData(data, company);
429+
/* ---------------------------------------------------------
430+
4. Validate public inputs
431+
Circuit order:
432+
[ attestation,
433+
addr0..addr(m-1),
434+
hi,
435+
lo,
436+
nonce ]
437+
----------------------------------------------------------*/
438+
439+
// todo in future: attestation / domain separator
440+
441+
// admins
442+
for (uint256 i = 0; i < m; i++) {
443+
require(
444+
zkInput.input[i + 1]
445+
== uint256(uint160(zkInput.companyAdmins[i])),
446+
"admin_mismatch"
447+
);
448+
}
449+
450+
// hash hi / lo
451+
require(
452+
zkInput.input[m + 1] == hi,
453+
"hash_hi_mismatch"
454+
);
455+
456+
require(
457+
zkInput.input[m + 2] == lo,
458+
"hash_lo_mismatch"
459+
);
460+
461+
// nonce
462+
require(
463+
zkInput.input[m + 3] == zkInput.inputNonce,
464+
"nonce_mismatch"
465+
);
466+
467+
/* ---------------------------------------------------------
468+
5. Verify ZK proof
469+
----------------------------------------------------------*/
470+
bool ok = _verifyProof(m, zkInput);
471+
require(ok, "invalid_zk_proof");
472+
473+
/* ---------------------------------------------------------
474+
6. Consume nonce BEFORE external call (reentrancy safety)
475+
----------------------------------------------------------*/
476+
usedNonces[zkInput.inputNonce] = true;
477+
478+
/* ---------------------------------------------------------
479+
7. Publish
480+
----------------------------------------------------------*/
481+
IDigitalAssetMarketplaceStub(zkInput.marketplace)
482+
.publishData(zkInput.cid, zkInput.company);
483+
}
484+
485+
// Helper to convert dynamic input to the fixed size the Verifier interface expects
486+
function _verifyProof(uint256 m, ZKProofInput calldata zkInput)
487+
internal
488+
view
489+
returns (bool)
490+
{
491+
address verifier = verifiers[m];
492+
493+
require(verifier != address(0), "unsupported_group_size");
494+
495+
if (m == 2) {
496+
uint256[6] memory signals;
497+
498+
for (uint256 i = 0; i < 6; i++) {
499+
signals[i] = zkInput.input[i];
500+
}
501+
502+
return IVerifierM2(verifier).verifyProof(
503+
zkInput.a,
504+
zkInput.b,
505+
zkInput.c,
506+
signals
507+
);
508+
}
509+
510+
if (m == 3) {
511+
uint256[8] memory signals;
512+
513+
for (uint256 i = 0; i < 8; i++) {
514+
signals[i] = zkInput.input[i];
515+
}
516+
517+
return IVerifierM3(verifier).verifyProof(
518+
zkInput.a,
519+
zkInput.b,
520+
zkInput.c,
521+
signals
522+
);
523+
}
524+
525+
if (m == 4) {
526+
uint256[8] memory signals;
527+
528+
for (uint256 i = 0; i < 8; i++) {
529+
signals[i] = zkInput.input[i];
530+
}
531+
532+
return IVerifierM4(verifier).verifyProof(
533+
zkInput.a,
534+
zkInput.b,
535+
zkInput.c,
536+
signals
537+
);
538+
}
539+
540+
revert("invalid_group_size");
541+
}
542+
543+
function hashPublishMessage(
544+
uint256 nonce,
545+
string memory cid,
546+
address company,
547+
address marketplace
548+
) public pure returns (uint256 hi, uint256 lo) {
549+
bytes memory data = abi.encodePacked(
550+
"ZK_PUBLISH_V1",
551+
nonce,
552+
cid,
553+
company,
554+
marketplace
555+
);
556+
bytes32 h = keccak256(data);
557+
uint256 x = uint256(h);
558+
lo = x & ((1 << 128) - 1);
559+
hi = x >> 128;
363560
}
364561
}

0 commit comments

Comments
 (0)