From c2619e232e670557cdb90e96c0cfacb8b8c0484f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 9 Jun 2025 16:50:37 -0600 Subject: [PATCH 1/2] Add UTXOs library --- contracts/utils/UTXOs.sol | 95 ++++++++++++++++++++++++++ docs/modules/ROOT/pages/utilities.adoc | 64 +++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 contracts/utils/UTXOs.sol diff --git a/contracts/utils/UTXOs.sol b/contracts/utils/UTXOs.sol new file mode 100644 index 00000000..1a174ce2 --- /dev/null +++ b/contracts/utils/UTXOs.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @dev Library for implementing Unspent Transaction Outputs (UTXOs). + * + * UTXOs represent discrete units of value that can be spent exactly once. This minimal design + * uses bytes32 for values to support multiple value representations: plaintext amounts, + * encrypted values (FHE), zero-knowledge commitments, or other privacy-preserving formats. + */ +library UTXOs { + /** + * @dev Struct to represent a UTXO (Unspent Transaction Output) + * + * Uses bytes32 for the value to maximize flexibility. This allows the library to work with: + * + * * Regular uint256 values (cast to `bytes32`) + * * FHE encrypted value pointers (`euint64.unwrap()`) + * * Zero-knowledge commitments (commitment hashes) + * * Other privacy-preserving value representations + */ + struct Note { + address owner; // Owner of the note + bytes32 value; // Generic value representation + } + + /** + * @dev Creates a new note with the specified owner and value + * + * The value parameter can represent different formats depending on the use case: + * + * * Plaintext: `bytes32(uint256(value))` + * * FHE encrypted: `euint64.unwrap(encryptedAmount)` + * * ZK commitment: `keccak256(abi.encode(value, nonce))` + * + * NOTE: Does not verify `owner != address(0)` or that value is not zero as it + * has a different meaning depending on the context. Consider implementing checks + * before using this function. + */ + function create(Note storage note, address owner, bytes32 value) internal { + require(note.owner == address(0), "Notes: ID already exists"); + note.owner = owner; + note.value = value; + } + + /** + * @dev Checks if a note exists + * + * Uses the owner field as existence indicator since zero address + * is not a valid owner for active notes. + */ + function exists(Note storage note) internal view returns (bool) { + return note.owner != address(0); + } + + /** + * @dev Deletes a note from storage + * + * Removes the note completely. Developers should implement their own + * authorization checks and update index mappings before calling this function. + */ + function remove(Note storage note) internal { + require(note.owner != address(0), "UTXOs: note does not exist"); + note.owner = address(0); + note.value = bytes32(0); + } + + /** + * @dev Transfers a note to a new owner + * + * Allows ownership transfers. Developers should implement authorization + * checks to ensure only the current owner or authorized parties can + * transfer ownership. + * + * NOTE: Does not verify `to != address(0)`. Transferring to the zero + * address may leave the value of the note unspent. Consider using + * {remove} instead. + */ + function transfer(Note storage note, address to) internal { + require(note.owner != address(0), "UTXOs: note does not exist"); + note.owner = to; + } + + /** + * @dev Updates the value of an existing note + * + * Allows value modifications for specific use cases. Developers should + * implement proper authorization checks before calling this function. + * Useful for encrypted value updates or commitment reveals. + */ + function update(Note storage note, bytes32 newValue) internal { + require(note.owner != address(0), "UTXOs: note does not exist"); + note.value = newValue; + } +} diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index af647fc7..d9231218 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -59,3 +59,67 @@ The verification process works as follows: * Otherwise: verification is done using an ERC-7913 verifier. This allows for backward compatibility with EOAs and ERC-1271 contracts while supporting new types of keys. + +=== UTXOs (Unspent Transaction Outputs) + +_For background on UTXO concepts, see the https://en.bitcoin.it/wiki/UTXO[Bitcoin UTXO model]_ + +UTXOs represent discrete units of value that can be spent exactly once, providing transaction graph privacy by breaking linkability between transfers. The xref:api:utils.adoc#UTXOs[`UTXOs`] library offers a minimal, flexible foundation for implementing UTXO-like systems in Ethereum smart contracts. + +==== Basic UTXO Operations + +The library uses `bytes32` for values to support multiple representations: plaintext amounts, encrypted values (FHE), zero-knowledge commitments, or other privacy-preserving formats: + +[source,solidity] +---- +import {UTXOs} from "@openzeppelin/contracts/utils/UTXOs.sol"; + +contract BasicUTXOToken { + using UTXOs for UTXOs.Note; + + mapping(bytes32 => UTXOs.Note) private _notes; + mapping(address => bytes32[]) private _ownerToNotes; // Separate indexing + + function mint(address to, uint256 amount) external { + bytes32 noteId = keccak256(abi.encode(block.timestamp, to, amount)); + bytes32 value = bytes32(amount); // Convert uint256 to bytes32 + + _notes[noteId].create(to, value); + _ownerToNotes[to].push(noteId); + } + + function transfer(bytes32 fromNoteId, address to, uint256 amount) external { + UTXOs.Note storage fromNote = _notes[fromNoteId]; + require(fromNote.owner == msg.sender, "Not owner"); + + uint256 currentValue = uint256(fromNote.value); + require(currentValue >= amount, "Insufficient value"); + + // Remove spent note + _removeFromIndex(msg.sender, fromNoteId); + fromNote.remove(); + + // Create new notes + bytes32 toNoteId = keccak256(abi.encode(block.timestamp, to, "recipient")); + _notes[toNoteId].create(to, bytes32(amount)); + _ownerToNotes[to].push(toNoteId); + + // Create change note if needed + if (currentValue > amount) { + bytes32 changeNoteId = keccak256(abi.encode(block.timestamp, msg.sender, "change")); + _notes[changeNoteId].create(msg.sender, bytes32(currentValue - amount)); + _ownerToNotes[msg.sender].push(changeNoteId); + } + } +} +---- + +==== Use Cases + +* **Privacy Tokens**: Combine with FHE for maximum privacy (encrypted amounts + untraceable transaction graphs) +* **Mixer Protocols**: Break transaction links while maintaining verifiable balances +* **Layer-2 Solutions**: Efficient fraud proofs and state transitions +* **DeFi Privacy**: Anonymous lending, trading, and yield farming +* **Cross-Chain Bridges**: Interoperability with UTXO-based blockchains like Bitcoin + +The `bytes32` value design makes UTXOs a universal primitive for any privacy-preserving system on Ethereum. From ecc538cfe38b84248bf29ec526770ee29c5bd1c7 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 10 Jun 2025 14:25:53 -0600 Subject: [PATCH 2/2] rename and split in 2 libraries --- contracts/utils/PrivateLedger.sol | 97 +++++++++++++++ contracts/utils/PrivateNote.sol | 162 +++++++++++++++++++++++++ contracts/utils/UTXOs.sol | 95 --------------- docs/modules/ROOT/pages/utilities.adoc | 128 ++++++++++++++----- 4 files changed, 359 insertions(+), 123 deletions(-) create mode 100644 contracts/utils/PrivateLedger.sol create mode 100644 contracts/utils/PrivateNote.sol delete mode 100644 contracts/utils/UTXOs.sol diff --git a/contracts/utils/PrivateLedger.sol b/contracts/utils/PrivateLedger.sol new file mode 100644 index 00000000..a1ecfdb5 --- /dev/null +++ b/contracts/utils/PrivateLedger.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @dev Library for implementing private ledger entries. + * + * Private ledger entries represent discrete units of value with flexible representation. + * This minimal design uses bytes32 for values to support multiple value representations: + * plaintext amounts, encrypted values (FHE), zero-knowledge commitments, or other + * privacy-preserving formats. The library provides basic primitives for creating, + * transferring, and managing entries without imposing specific spending semantics. + */ +library PrivateLedger { + /** + * @dev Struct to represent a private ledger entry + * + * Uses bytes32 for the value to maximize flexibility. This allows the library to work with: + * + * * Regular uint256 values (cast to `bytes32`) + * * FHE encrypted value pointers (`euint64.unwrap()`) + * * Zero-knowledge commitments (commitment hashes) + * * Other privacy-preserving value representations + */ + struct Entry { + bytes32 value; // Generic value representation + address owner; // Owner of the entry + } + + /** + * @dev Creates a new entry with the specified owner and value + * + * The value parameter can represent different formats depending on the use case: + * + * * Plaintext: `bytes32(uint256(value))` + * * FHE encrypted: `euint64.unwrap(encryptedAmount)` + * * ZK commitment: `keccak256(abi.encode(value, nonce))` + * + * NOTE: Does not verify `owner != address(0)` or that value is not zero as it + * has a different meaning depending on the context. Consider implementing checks + * before using this function. + */ + function create(Entry storage entry, address owner, bytes32 value) internal { + require(entry.owner == address(0)); + entry.owner = owner; + entry.value = value; + } + + /** + * @dev Checks if an entry exists + * + * Uses the owner field as existence indicator since zero address + * is not a valid owner for active entries. + */ + function exists(Entry storage entry) internal view returns (bool) { + return entry.owner != address(0); + } + + /** + * @dev Deletes an entry from storage + * + * Removes the entry completely. Developers should implement their own + * authorization checks and update index mappings before calling this function. + */ + function remove(Entry storage entry) internal { + require(entry.owner != address(0)); + entry.owner = address(0); + entry.value = bytes32(0); + } + + /** + * @dev Transfers an entry to a new owner + * + * Allows ownership transfers. Developers should implement authorization + * checks to ensure only the current owner or authorized parties can + * transfer ownership. + * + * NOTE: Does not verify `to != address(0)`. Transferring to the zero + * address may leave the value of the entry unspent. Consider using + * {remove} instead. + */ + function transfer(Entry storage entry, address to) internal { + require(entry.owner != address(0)); + entry.owner = to; + } + + /** + * @dev Updates the value of an existing entry + * + * Allows value modifications for specific use cases. Developers should + * implement proper authorization checks before calling this function. + * Useful for encrypted value updates or commitment reveals. + */ + function update(Entry storage entry, bytes32 newValue) internal { + require(entry.owner != address(0)); + entry.value = newValue; + } +} diff --git a/contracts/utils/PrivateNote.sol b/contracts/utils/PrivateNote.sol new file mode 100644 index 00000000..2114a7fd --- /dev/null +++ b/contracts/utils/PrivateNote.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {PrivateLedger} from "./PrivateLedger.sol"; + +/** + * @dev Library for implementing spendable private notes. + * + * Private notes represent discrete units of value that can be spent exactly once, building + * on the {PrivateLedger} foundation with opinionated spending functionality. This library adds + * double-spend prevention, lineage tracking options, and transaction-like operations for + * creating privacy-focused token systems, mixers, and confidential transfer protocols. + */ +library PrivateNote { + using PrivateLedger for PrivateLedger.Entry; + + /** + * @dev Struct to represent a privacy-preserving spendable note + * + * Builds on {PrivateLedger-Entry} to add spending functionality without lineage tracking. + * The `spent` flag prevents double-spending while maintaining maximum privacy by not + * storing parent note references. Uses bytes32 for maximum flexibility with encrypted + * values, commitments, or plaintext amounts. + */ + struct SpendableBytes32 { + PrivateLedger.Entry entry; // Underlying ledger entry + bool spent; // Prevents double-spending + } + + /** + * @dev Struct to represent a trackable spendable note with lineage + * + * Builds on {PrivateLedger-Entry} to add spending functionality with lineage tracking. + * The `spent` flag prevents double-spending while `createdBy` enables transaction chain + * reconstruction for auditability. Reduces privacy but enables compliance scenarios. + */ + struct TrackableSpendableBytes32 { + PrivateLedger.Entry entry; // Underlying ledger entry + bool spent; // Prevents double-spending + bytes32 createdBy; // ID of parent note for lineage tracking + } + + /// @dev Emitted when a new privacy-preserving spendable note is created + event SpendableBytes32Created(bytes32 indexed id, address indexed owner, bytes32 value); + + /// @dev Emitted when a privacy-preserving spendable note is spent + event SpendableBytes32Spent(bytes32 indexed id, address indexed spender); + + /// @dev Emitted when a new trackable spendable note is created + event TrackableSpendableBytes32Created(bytes32 indexed id, address indexed owner, bytes32 value, bytes32 createdBy); + + /// @dev Emitted when a trackable spendable note is spent + event TrackableSpendableBytes32Spent(bytes32 indexed id, address indexed spender); + + /** + * @dev Creates a new privacy-preserving spendable note + * + * Creates a note without parent lineage tracking for maximum privacy. The note can be + * spent exactly once using the spend function. Use this for privacy-focused applications + * where transaction unlinkability is prioritized over auditability. + */ + function create(SpendableBytes32 storage note, address owner, bytes32 value, bytes32 id) internal { + note.entry.create(owner, value); + // note.spent = false; // false by default + + emit SpendableBytes32Created(id, owner, value); + } + + /** + * @dev Creates a new trackable spendable note with lineage + * + * Creates a note with parent linkage for auditability. The `createdBy` field allows + * transaction chain reconstruction but reduces privacy. Use this for compliance + * scenarios or when transaction history tracking is required. + */ + function create( + TrackableSpendableBytes32 storage note, + address owner, + bytes32 value, + bytes32 id, + bytes32 parentId + ) internal { + note.entry.create(owner, value); + note.createdBy = parentId; // Enables lineage tracking + // note.spent = false; // false by default + + emit TrackableSpendableBytes32Created(id, owner, value, parentId); + } + + /** + * @dev Spends a privacy-preserving note + * + * Spends the note while maintaining maximum privacy. The spent note cannot be used + * again. External note creation should use SpendableBytes32 to maintain privacy. + */ + function spend( + SpendableBytes32 storage note, + bytes32 noteId, + bytes32 recipientId, + bytes32 changeId + ) internal returns (bytes32 actualRecipientId, bytes32 actualChangeId) { + require(!note.spent, "PrivateNote: already spent"); + require(note.entry.owner != address(0), "PrivateNote: note does not exist"); + + note.spent = true; + emit SpendableBytes32Spent(noteId, note.entry.owner); + + // Return the provided IDs for external note creation + return (recipientId, changeId); + } + + /** + * @dev Spends a trackable note with lineage preservation + * + * Spends the note while maintaining transaction history through parent linkage. + * External note creation should use TrackableSpendableBytes32 with this note's ID + * as the parent to maintain the audit trail. + */ + function spend( + TrackableSpendableBytes32 storage note, + bytes32 noteId, + bytes32 recipientId, + bytes32 changeId + ) internal returns (bytes32 actualRecipientId, bytes32 actualChangeId) { + require(!note.spent, "PrivateNote: already spent"); + require(note.entry.owner != address(0), "PrivateNote: note does not exist"); + + note.spent = true; + emit TrackableSpendableBytes32Spent(noteId, note.entry.owner); + + // Return the provided IDs for external trackable note creation + return (recipientId, changeId); + } + + /** + * @dev Checks if a privacy-preserving note exists and is unspent + */ + function isUnspent(SpendableBytes32 storage note) internal view returns (bool) { + return note.entry.exists() && !note.spent; + } + + /** + * @dev Checks if a privacy-preserving note exists (regardless of spent status) + */ + function exists(SpendableBytes32 storage note) internal view returns (bool) { + return note.entry.exists(); + } + + /** + * @dev Checks if a trackable note exists and is unspent + */ + function isUnspent(TrackableSpendableBytes32 storage note) internal view returns (bool) { + return note.entry.exists() && !note.spent; + } + + /** + * @dev Checks if a trackable note exists (regardless of spent status) + */ + function exists(TrackableSpendableBytes32 storage note) internal view returns (bool) { + return note.entry.exists(); + } +} diff --git a/contracts/utils/UTXOs.sol b/contracts/utils/UTXOs.sol deleted file mode 100644 index 1a174ce2..00000000 --- a/contracts/utils/UTXOs.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/** - * @dev Library for implementing Unspent Transaction Outputs (UTXOs). - * - * UTXOs represent discrete units of value that can be spent exactly once. This minimal design - * uses bytes32 for values to support multiple value representations: plaintext amounts, - * encrypted values (FHE), zero-knowledge commitments, or other privacy-preserving formats. - */ -library UTXOs { - /** - * @dev Struct to represent a UTXO (Unspent Transaction Output) - * - * Uses bytes32 for the value to maximize flexibility. This allows the library to work with: - * - * * Regular uint256 values (cast to `bytes32`) - * * FHE encrypted value pointers (`euint64.unwrap()`) - * * Zero-knowledge commitments (commitment hashes) - * * Other privacy-preserving value representations - */ - struct Note { - address owner; // Owner of the note - bytes32 value; // Generic value representation - } - - /** - * @dev Creates a new note with the specified owner and value - * - * The value parameter can represent different formats depending on the use case: - * - * * Plaintext: `bytes32(uint256(value))` - * * FHE encrypted: `euint64.unwrap(encryptedAmount)` - * * ZK commitment: `keccak256(abi.encode(value, nonce))` - * - * NOTE: Does not verify `owner != address(0)` or that value is not zero as it - * has a different meaning depending on the context. Consider implementing checks - * before using this function. - */ - function create(Note storage note, address owner, bytes32 value) internal { - require(note.owner == address(0), "Notes: ID already exists"); - note.owner = owner; - note.value = value; - } - - /** - * @dev Checks if a note exists - * - * Uses the owner field as existence indicator since zero address - * is not a valid owner for active notes. - */ - function exists(Note storage note) internal view returns (bool) { - return note.owner != address(0); - } - - /** - * @dev Deletes a note from storage - * - * Removes the note completely. Developers should implement their own - * authorization checks and update index mappings before calling this function. - */ - function remove(Note storage note) internal { - require(note.owner != address(0), "UTXOs: note does not exist"); - note.owner = address(0); - note.value = bytes32(0); - } - - /** - * @dev Transfers a note to a new owner - * - * Allows ownership transfers. Developers should implement authorization - * checks to ensure only the current owner or authorized parties can - * transfer ownership. - * - * NOTE: Does not verify `to != address(0)`. Transferring to the zero - * address may leave the value of the note unspent. Consider using - * {remove} instead. - */ - function transfer(Note storage note, address to) internal { - require(note.owner != address(0), "UTXOs: note does not exist"); - note.owner = to; - } - - /** - * @dev Updates the value of an existing note - * - * Allows value modifications for specific use cases. Developers should - * implement proper authorization checks before calling this function. - * Useful for encrypted value updates or commitment reveals. - */ - function update(Note storage note, bytes32 newValue) internal { - require(note.owner != address(0), "UTXOs: note does not exist"); - note.value = newValue; - } -} diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index d9231218..e747648a 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -60,66 +60,138 @@ The verification process works as follows: This allows for backward compatibility with EOAs and ERC-1271 contracts while supporting new types of keys. -=== UTXOs (Unspent Transaction Outputs) +=== Private Ledger & Notes _For background on UTXO concepts, see the https://en.bitcoin.it/wiki/UTXO[Bitcoin UTXO model]_ -UTXOs represent discrete units of value that can be spent exactly once, providing transaction graph privacy by breaking linkability between transfers. The xref:api:utils.adoc#UTXOs[`UTXOs`] library offers a minimal, flexible foundation for implementing UTXO-like systems in Ethereum smart contracts. +Private ledger entries represent discrete units of value with flexible representation, providing transaction graph privacy by breaking linkability between transfers. OpenZeppelin provides a **layered architecture** for implementing UTXO-like systems: -==== Basic UTXO Operations +* **xref:api:utils.adoc#PrivateLedger[`PrivateLedger`]** - Foundational library with basic entry primitives +* **xref:api:utils.adoc#PrivateNote[`PrivateNote`]** - Opinionated layer with spending functionality and privacy options -The library uses `bytes32` for values to support multiple representations: plaintext amounts, encrypted values (FHE), zero-knowledge commitments, or other privacy-preserving formats: +==== Private Ledger + +The foundation layer uses `bytes32` for values to support multiple representations: plaintext amounts, encrypted values (FHE), zero-knowledge commitments, or other privacy-preserving formats: [source,solidity] ---- -import {UTXOs} from "@openzeppelin/contracts/utils/UTXOs.sol"; +import {PrivateLedger} from "@openzeppelin/contracts/utils/PrivateLedger.sol"; -contract BasicUTXOToken { - using UTXOs for UTXOs.Note; +contract BasicLedgerToken { + using PrivateLedger for PrivateLedger.Entry; - mapping(bytes32 => UTXOs.Note) private _notes; - mapping(address => bytes32[]) private _ownerToNotes; // Separate indexing + mapping(bytes32 => PrivateLedger.Entry) private _entries; + mapping(address => bytes32[]) private _ownerToEntries; // Separate indexing function mint(address to, uint256 amount) external { - bytes32 noteId = keccak256(abi.encode(block.timestamp, to, amount)); + bytes32 entryId = keccak256(abi.encode(block.timestamp, to, amount)); bytes32 value = bytes32(amount); // Convert uint256 to bytes32 - _notes[noteId].create(to, value); - _ownerToNotes[to].push(noteId); + _entries[entryId].create(to, value); + _ownerToEntries[to].push(entryId); } - function transfer(bytes32 fromNoteId, address to, uint256 amount) external { - UTXOs.Note storage fromNote = _notes[fromNoteId]; - require(fromNote.owner == msg.sender, "Not owner"); + function transfer(bytes32 fromEntryId, address to, uint256 amount) external { + PrivateLedger.Entry storage fromEntry = _entries[fromEntryId]; + require(fromEntry.owner == msg.sender, "Not owner"); - uint256 currentValue = uint256(fromNote.value); + uint256 currentValue = uint256(fromEntry.value); require(currentValue >= amount, "Insufficient value"); - // Remove spent note - _removeFromIndex(msg.sender, fromNoteId); - fromNote.remove(); + // Remove spent entry + _removeFromIndex(msg.sender, fromEntryId); + fromEntry.remove(); - // Create new notes - bytes32 toNoteId = keccak256(abi.encode(block.timestamp, to, "recipient")); - _notes[toNoteId].create(to, bytes32(amount)); - _ownerToNotes[to].push(toNoteId); + // Create new entries + bytes32 toEntryId = keccak256(abi.encode(block.timestamp, to, "recipient")); + _entries[toEntryId].create(to, bytes32(amount)); + _ownerToEntries[to].push(toEntryId); - // Create change note if needed + // Create change entry if needed if (currentValue > amount) { - bytes32 changeNoteId = keccak256(abi.encode(block.timestamp, msg.sender, "change")); - _notes[changeNoteId].create(msg.sender, bytes32(currentValue - amount)); - _ownerToNotes[msg.sender].push(changeNoteId); + bytes32 changeEntryId = keccak256(abi.encode(block.timestamp, msg.sender, "change")); + _entries[changeEntryId].create(msg.sender, bytes32(currentValue - amount)); + _ownerToEntries[msg.sender].push(changeEntryId); } } } ---- +==== Spendable Notes with Privacy Options + +The opinionated layer adds spending semantics with **two privacy models**: + +[source,solidity] +---- +import {PrivateNote} from "@openzeppelin/contracts/utils/PrivateNote.sol"; + +contract PrivacyToken { + using PrivateNote for PrivateNote.SpendableBytes32; + using PrivateNote for PrivateNote.TrackableSpendableBytes32; + + // Privacy-preserving notes (no lineage tracking) + mapping(bytes32 => PrivateNote.SpendableBytes32) private _privateNotes; + + // Auditable notes (with lineage tracking) + mapping(bytes32 => PrivateNote.TrackableSpendableBytes32) private _auditableNotes; + + function mintPrivate(address to, uint256 amount) external { + bytes32 noteId = keccak256(abi.encode(block.timestamp, to, amount)); + _privateNotes[noteId].create(to, bytes32(amount), noteId); + } + + function mintAuditable(address to, uint256 amount, bytes32 parentId) external { + bytes32 noteId = keccak256(abi.encode(block.timestamp, to, amount)); + _auditableNotes[noteId].create(to, bytes32(amount), noteId, parentId); + } + + function spendPrivate( + bytes32 noteId, + address to, + uint256 amount + ) external { + require(_privateNotes[noteId].entry.owner == msg.sender, "Not owner"); + + // Spend note (maintains privacy - no lineage) + bytes32 recipientId = keccak256(abi.encode(block.timestamp, to, "recipient")); + bytes32 changeId = keccak256(abi.encode(block.timestamp, msg.sender, "change")); + + _privateNotes[noteId].spend( + noteId, to, bytes32(amount), msg.sender, bytes32(0), recipientId, changeId + ); + + // Create new private notes + _privateNotes[recipientId].create(to, bytes32(amount), recipientId); + } + + function spendAuditable( + bytes32 noteId, + address to, + uint256 amount + ) external { + require(_auditableNotes[noteId].entry.owner == msg.sender, "Not owner"); + + // Spend note (preserves lineage for auditing) + bytes32 recipientId = keccak256(abi.encode(block.timestamp, to, "recipient")); + bytes32 changeId = keccak256(abi.encode(block.timestamp, msg.sender, "change")); + + _auditableNotes[noteId].spend( + noteId, to, bytes32(amount), msg.sender, bytes32(0), recipientId, changeId + ); + + // Create new trackable notes with lineage + _auditableNotes[recipientId].create(to, bytes32(amount), recipientId, noteId); + } +} +---- + ==== Use Cases * **Privacy Tokens**: Combine with FHE for maximum privacy (encrypted amounts + untraceable transaction graphs) * **Mixer Protocols**: Break transaction links while maintaining verifiable balances +* **Compliance Systems**: Use trackable notes for audit trails while maintaining confidentiality * **Layer-2 Solutions**: Efficient fraud proofs and state transitions * **DeFi Privacy**: Anonymous lending, trading, and yield farming * **Cross-Chain Bridges**: Interoperability with UTXO-based blockchains like Bitcoin -The `bytes32` value design makes UTXOs a universal primitive for any privacy-preserving system on Ethereum. +The layered architecture provides flexibility: use `PrivateLedger` for custom logic or `PrivateNote` for battle-tested spending semantics. The `bytes32` value design makes this a universal primitive for any privacy-preserving system on Ethereum.