Skip to content

Commit 2451e53

Browse files
authored
Merge pull request #141 from hyperledger-labs/burn
Add burn support
2 parents 4a63dc3 + 45367ad commit 2451e53

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2505
-140
lines changed
15.1 KB
Loading

doc-site/docs/implementations/anon_nullifier_qurrency.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
| ------------------ | ------------------ | ------------------ | --- | --------------- | ------------------- |
55
| :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | - | - | 2,066,430 |
66

7-
![pqc](../images/pqc-dark.png)
7+
![pqc](../images/pqc-dark-100px.png)
88

99
This implementation builds on top of the `anon_nullifiers` to add post-quantum cryptography inside the circuit to encrypt sensitive information for a designated authority, such as a regulator, for auditing purposes.
1010

@@ -13,15 +13,15 @@ To implement post-quantum secure encryption, these circuits use the public key e
1313
The encryption is performed in the follow step, according to the ML-KEM scheme:
1414

1515
- An AES-256 encryption key is generated for the transaction. This key is used only for this transaction
16-
- AES-256 is a quantum-secure block cipher.
16+
- AES-256 is a quantum-secure block cipher.
1717
- The secrets meant for the auditing authority are encrypted with this key, and sent as part of the transaction payload
1818
- The key is passed into the circuit as a private input and encrypted inside the circuit under the ML-KEM scheme, using the auditing authority's public key
19-
- The public key is statically programmed into the circuit. This is to avoid making it a signal which would be very inefficient due to the large size of the public key (1184 bytes)
20-
- <span style="color:red">IMPORTANT:</span> This means for a real world deployment, the deployer MUST update the circuit with the auditing authority's public key and re-compile the circuit
19+
- The public key is statically programmed into the circuit. This is to avoid making it a signal which would be very inefficient due to the large size of the public key (1184 bytes)
20+
- <span style="color:red">IMPORTANT:</span> This means for a real world deployment, the deployer MUST update the circuit with the auditing authority's public key and re-compile the circuit
2121
- The ciphertext is extracted from the circuit's witness array and sent as a part of the transaction payload
2222
- The circuit also calculates the SHA256 hash of the ciphertext to use as a public input, instead of using the ciphertext itself as a public input
23-
- This is an optimization step. The ciphertext is a large array of 768 numbers (of value 0 or 1), to represent the 768-bit ciphertext, which would make proof verification more expensive
24-
- The SHA256 hashing algorithm is picked to make verification inside EVM more efficient, due to the native sha256 support via EVM precompiles.
23+
- This is an optimization step. The ciphertext is a large array of 768 numbers (of value 0 or 1), to represent the 768-bit ciphertext, which would make proof verification more expensive
24+
- The SHA256 hashing algorithm is picked to make verification inside EVM more efficient, due to the native sha256 support via EVM precompiles.
2525
- The authority can then use the private key to decrypt the ciphertext to recover the AES encryption key, then use the recovered key to decrypt the AES ciphertext to recover the secrets
2626

2727
The statements in the proof include:

solidity/contracts/lib/interfaces/izeto.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ interface IZeto {
2525
error UTXOAlreadySpent(uint256 utxo);
2626

2727
event UTXOMint(uint256[] outputs, address indexed submitter, bytes data);
28+
event UTXOBurn(
29+
uint256[] inputs,
30+
uint256 output,
31+
address indexed submitter,
32+
bytes data
33+
);
2834
event UTXOTransfer(
2935
uint256[] inputs,
3036
uint256[] outputs,

solidity/contracts/lib/interfaces/izeto_initializable.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ interface IZetoInitializable {
2020
address verifier;
2121
address depositVerifier;
2222
address withdrawVerifier;
23+
address lockVerifier;
24+
address burnVerifier;
2325
address batchVerifier;
2426
address batchWithdrawVerifier;
25-
address lockVerifier;
2627
address batchLockVerifier;
28+
address batchBurnVerifier;
2729
}
2830

2931
function initialize(

solidity/contracts/lib/zeto_base.sol

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,18 @@ abstract contract ZetoBase is IZeto, IZetoLockable, ZetoCommon {
6868
for (uint256 i = 0; i < lockedOutputs.length; i++) {
6969
allOutputs[outputs.length + i] = lockedOutputs[i];
7070
}
71-
// sort the inputs and outputs to detect duplicates
72-
uint256[] memory sortedInputs = sortCommitments(inputs);
73-
uint256[] memory sortedOutputs = sortCommitments(allOutputs);
71+
validateInputs(inputs, inputsLocked);
72+
validateOutputs(allOutputs);
73+
74+
return true;
75+
}
7476

77+
function validateInputs(
78+
uint256[] memory inputs,
79+
bool inputsLocked
80+
) internal view {
81+
// sort the inputs to detect duplicates
82+
uint256[] memory sortedInputs = sortCommitments(inputs);
7583
// Check the inputs are all unspent
7684
for (uint256 i = 0; i < sortedInputs.length; ++i) {
7785
if (sortedInputs[i] == 0) {
@@ -111,6 +119,11 @@ abstract contract ZetoBase is IZeto, IZetoLockable, ZetoCommon {
111119
);
112120
}
113121
}
122+
}
123+
124+
function validateOutputs(uint256[] memory outputs) internal view {
125+
// sort the outputs to detect duplicates
126+
uint256[] memory sortedOutputs = sortCommitments(outputs);
114127

115128
// Check for duplicate outputs
116129
for (uint256 i = 0; i < sortedOutputs.length; ++i) {
@@ -137,51 +150,47 @@ abstract contract ZetoBase is IZeto, IZetoLockable, ZetoCommon {
137150
revert UTXOAlreadyOwned(outputs[i]);
138151
}
139152
}
140-
141-
// check the locked outputs are all new - looking in the locked and unlocked UTXOs
142-
for (uint256 i = 0; i < lockedOutputs.length; ++i) {
143-
if (
144-
_lockedUtxos[lockedOutputs[i]] == UTXOStatus.SPENT ||
145-
_utxos[lockedOutputs[i]] == UTXOStatus.SPENT
146-
) {
147-
revert UTXOAlreadySpent(lockedOutputs[i]);
148-
} else if (
149-
_lockedUtxos[lockedOutputs[i]] == UTXOStatus.UNSPENT ||
150-
_utxos[lockedOutputs[i]] == UTXOStatus.UNSPENT
151-
) {
152-
revert UTXOAlreadyOwned(lockedOutputs[i]);
153-
}
154-
}
155-
return true;
156153
}
157154

158155
function processInputsAndOutputs(
159156
uint256[] memory inputs,
160157
uint256[] memory outputs,
161158
uint256[] memory lockedOutputs,
162159
bool inputsLocked
160+
) internal {
161+
processInputs(inputs, inputsLocked);
162+
processOutputs(outputs);
163+
processLockedOutputs(lockedOutputs);
164+
}
165+
166+
function processInputs(
167+
uint256[] memory inputs,
168+
bool inputsLocked
163169
) internal {
164170
mapping(uint256 => UTXOStatus) storage utxos = inputsLocked
165171
? _lockedUtxos
166172
: _utxos;
167-
// accept the transaction to consume the input UTXOs and produce new UTXOs
173+
// consume the input UTXOs
168174
for (uint256 i = 0; i < inputs.length; ++i) {
169-
if (inputs[i] == 0) {
170-
continue;
175+
if (inputs[i] != 0) {
176+
utxos[inputs[i]] = UTXOStatus.SPENT;
171177
}
172-
utxos[inputs[i]] = UTXOStatus.SPENT;
173178
}
179+
}
180+
181+
function processOutputs(uint256[] memory outputs) internal {
174182
for (uint256 i = 0; i < outputs.length; ++i) {
175-
if (outputs[i] == 0) {
176-
continue;
183+
if (outputs[i] != 0) {
184+
_utxos[outputs[i]] = UTXOStatus.UNSPENT;
177185
}
178-
_utxos[outputs[i]] = UTXOStatus.UNSPENT;
179186
}
187+
}
188+
189+
function processLockedOutputs(uint256[] memory lockedOutputs) internal {
180190
for (uint256 i = 0; i < lockedOutputs.length; ++i) {
181-
if (lockedOutputs[i] == 0) {
182-
continue;
191+
if (lockedOutputs[i] != 0) {
192+
_lockedUtxos[lockedOutputs[i]] = UTXOStatus.UNSPENT;
183193
}
184-
_lockedUtxos[lockedOutputs[i]] = UTXOStatus.UNSPENT;
185194
}
186195
}
187196

@@ -191,18 +200,26 @@ abstract contract ZetoBase is IZeto, IZetoLockable, ZetoCommon {
191200
uint256[] memory utxos,
192201
bytes calldata data
193202
) internal virtual {
194-
for (uint256 i = 0; i < utxos.length; ++i) {
195-
uint256 utxo = utxos[i];
196-
if (_utxos[utxo] == UTXOStatus.UNSPENT) {
197-
revert UTXOAlreadyOwned(utxo);
198-
} else if (_utxos[utxo] == UTXOStatus.SPENT) {
199-
revert UTXOAlreadySpent(utxo);
200-
}
201-
202-
_utxos[utxo] = UTXOStatus.UNSPENT;
203-
}
203+
validateOutputs(utxos);
204+
processOutputs(utxos);
204205
emit UTXOMint(utxos, msg.sender, data);
205206
}
207+
208+
// The caller function must perform the proof verification
209+
function _burn(
210+
uint256[] memory inputs,
211+
uint256 output,
212+
bytes calldata data
213+
) internal virtual {
214+
validateInputs(inputs, false);
215+
uint256[] memory outputStates = new uint256[](1);
216+
outputStates[0] = output;
217+
validateOutputs(outputStates);
218+
processInputs(inputs, false);
219+
processOutputs(outputStates);
220+
emit UTXOBurn(inputs, output, msg.sender, data);
221+
}
222+
206223
// Locks the UTXOs so that they can only be spent by submitting the appropriate
207224
// proof from the Eth account designated as the "delegate". This function
208225
// should be called by a participant, to designate an escrow contract as the delegate,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright © 2025 Kaleido, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
pragma solidity ^0.8.27;
17+
18+
import {Groth16Verifier_Burn} from "../verifiers/verifier_burn.sol";
19+
import {Groth16Verifier_BurnBatch} from "../verifiers/verifier_burn_batch.sol";
20+
import {ZetoBase} from "./zeto_base.sol";
21+
import {Commonlib} from "./common.sol";
22+
23+
uint256 constant BURN_INPUT_SIZE = 3;
24+
uint256 constant BATCH_BURN_INPUT_SIZE = 11;
25+
26+
/// @title A feature implementation of a Zeto fungible token burn contract
27+
/// @author Kaleido, Inc.
28+
/// @dev Can be added to a Zeto fungible token contract to allow for burning of tokens.
29+
abstract contract ZetoFungibleBurnable is ZetoBase {
30+
Groth16Verifier_Burn internal _burnVerifier;
31+
Groth16Verifier_BurnBatch internal _batchBurnVerifier;
32+
33+
function __ZetoFungibleBurnable_init(
34+
Groth16Verifier_Burn burnVerifier,
35+
Groth16Verifier_BurnBatch batchBurnVerifier
36+
) public onlyInitializing {
37+
_burnVerifier = burnVerifier;
38+
_batchBurnVerifier = batchBurnVerifier;
39+
}
40+
41+
function burn(
42+
uint256[] memory inputs,
43+
uint256 output,
44+
Commonlib.Proof calldata proof,
45+
bytes calldata data
46+
) public virtual {
47+
// Check the proof
48+
if (inputs.length > 2) {
49+
// construct the public inputs for verifier
50+
uint256[BATCH_BURN_INPUT_SIZE] memory fixedSizeInputs;
51+
for (uint256 i = 0; i < inputs.length; i++) {
52+
fixedSizeInputs[i] = inputs[i];
53+
}
54+
fixedSizeInputs[BATCH_BURN_INPUT_SIZE - 1] = output;
55+
// Check the proof
56+
require(
57+
_batchBurnVerifier.verifyProof(
58+
proof.pA,
59+
proof.pB,
60+
proof.pC,
61+
fixedSizeInputs
62+
),
63+
"Invalid proof"
64+
);
65+
} else {
66+
// construct the public inputs for verifier
67+
uint256[BURN_INPUT_SIZE] memory fixedSizeInputs;
68+
for (uint256 i = 0; i < inputs.length; i++) {
69+
fixedSizeInputs[i] = inputs[i];
70+
}
71+
fixedSizeInputs[BURN_INPUT_SIZE - 1] = output;
72+
// Check the proof
73+
require(
74+
_burnVerifier.verifyProof(
75+
proof.pA,
76+
proof.pB,
77+
proof.pC,
78+
fixedSizeInputs
79+
),
80+
"Invalid proof"
81+
);
82+
}
83+
84+
_burn(inputs, output, data);
85+
}
86+
}

0 commit comments

Comments
 (0)