Skip to content

Add PrivateLedger and PrivateNotes library #172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 97 additions & 0 deletions contracts/utils/PrivateLedger.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
162 changes: 162 additions & 0 deletions contracts/utils/PrivateNote.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading