Skip to content

Noto nullifiers#957

Open
jimthematrix wants to merge 185 commits intov1-developfrom
noto-nullifiers
Open

Noto nullifiers#957
jimthematrix wants to merge 185 commits intov1-developfrom
noto-nullifiers

Conversation

@jimthematrix
Copy link
Contributor

@jimthematrix jimthematrix commented Dec 8, 2025

(should be merged after #978 )

Adding the capability to spend Noto UTXOs with nullifiers to mask the spending history.

Nullifiers

Given that the commitments for a Noto token is EIP712Hash(amount, salt, owner_address), a typical nullifier scheme would be EIP712Hash(amount, salt, owner_priv_key). However, in Noto we do not make use of ZKPs, which means when the nullifier submitted by the sender is verified by the notary, the sender can not leak its private key to the notary. As such, we design the nullifier hashing scheme differently, by using just EIP712Hash(amount, salt).

This should still preserve the main security guarantees of a nullifier:

  • unique binding to the UTXO commitment: due to the uniqueness of the salt, the nullifier is guaranteed to be 1:1 bound to the UTXO commitment
  • secret binding to the UTXO commitment: because the salt is only known by the sender and the receiver, no other parties are able to construct the nullifier

This design lacks the guarantee that only the owner of the UTXO commitment is able to construct the nullifier, because the sender also knows the secrets required to construct it (besides the receiver which is the owner). However, this is mitigated by the fact that the notary already checks that the sender must be the rightful owner of the input UTXO, thus completing the security requirements to make this design secure.

Onchain storage

Unlike the regular Noto contract that tracks the UTXOs in a simple map for spent status, the UTXOs in this new NotoNullifiers contract are tracked inside a Sparse Merkle Tree (SMT). This way, a transaction submitter can submit these parameters to the notary, as part of the private transaction payload, for verification:

  • input UTXOs
  • nullifiers
  • outputUTXOs

The job of the notary is to verify that:

  • the input UTXOs are included in the SMT maintained by the smart contract
    • to do this, the notary must maintain its own SMT
  • the nullifiers are the proper ones corresponding to the input UTXOs
  • the input UTXOs and the output UTXOs obey the spending rules (mass conservation, etc.)

Then the notary submits the following (public) parameters to the NotoNullifiers contract:

  • nullifiers
  • output UTXOs
  • merkle tree root resulted from verifying the input UTXOs against the notary's SMT (this does NOT need to be the latest root, just needs to be one of the legitimate historical root)

The job of the smart contract is to verify that:

  • the root is a legitimate historical root (validity of the input UTXOs represented by the nullifiers)
    • this check is a safety net for cases when the notary makes honest mistakes and submits transactions out of order, which would attempt to consume UTXOs that have not been produced
  • the nullifiers are not seen before (prevents double spending)
  • the notary certificate, which is as usual, same as when not using nullifiers

Dependencies

Sparse Merkle Tree supporting configurable hashing algos

This solution depends on a Sparse Merkle Tree that uses keccak256 hashing, rather than the Poseidon hashing as seen in the current implementation. The Poseidon hashing is used for its efficiency inside ZKP circuits, but it's much slower in EVM compared to the "native" hashing like keccak256.

Since for Noto uses we do not need to generate ZK proofs, we want to have an SMT implementation that's based on keccak256, making transaction processing much cheaper when the onchain SMT needs to be updated with new UTXOs.

This is accomplished by enhancing the implementations in Solidity (to use in the NotoNullifiers contract), typescript (to use in hardhat tests), and golang (to use in Paladin's domain implementation):

SMT Storage implementation using Paladin's state storage

This was originally implemented for the Zeto domain. It's now moved into a common golang package under toolkit/go/pkg/smt, to be shared by the Noto and Zeto domains.

              github.com/LFDT-Paladin/smt
            (Sparse Merkle Tree implementation)
                             |
                             |
                    toolkit/go/pkg/smt
                   (SMT storage plugin)
                             |
              --------------------------------
              |                              |
        Noto Domain                     Zeto Domain

awrichar and others added 30 commits June 21, 2025 07:43
This option will deliver all public receipts, and any private receipts that a domain
indicates are "complete". A complete receipt is one that has been correlated with all
expected private data (noting that this may not be all private data involved in the
transaction, but only the private data that this node expects to receive).

Currently all domains only register "complete" if all states are available, but Noto
and Zeto will need to be enhanced with a more dynamic notion of completion soon, based
on an understanding of transaction types and expected state distribution.

Note that this new listener behavior does not guarantee receipts will be delivered
strictly in blockchain order (although they should be approximately ordered). Older
receipts may be delivered whenever private state data arrives to make them "complete".

Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
This ensures basic retry behavior if the domain isn't available immediately
(such as during startup), rather than failing to initialize the listener
completely.

Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
This generic interface should be usable by Zeto and any other confidential
token as well.

Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
This is not yet exposed on the Noto domain, but it is supported at the base
ledger.

Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
This defines a basic confidential UTXO token interface that could potentially be
implemented by both Noto and Zeto (and any other similar tokens in the future).

Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
This interface is not specific to C-UTXO tokens, but can describe how to
lock and unlock any arbitrary operation.

Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
…into state-completion

Signed-off-by: Peter Broadhurst <peter.broadhurst@kaleido.io>
Signed-off-by: Peter Broadhurst <peter.broadhurst@kaleido.io>
Signed-off-by: Peter Broadhurst <peter.broadhurst@kaleido.io>
Signed-off-by: Peter Broadhurst <peter.broadhurst@kaleido.io>
Signed-off-by: Peter Broadhurst <peter.broadhurst@kaleido.io>
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
@github-actions
Copy link

This PR is stale because it has been open 30 days with no activity.

@github-actions github-actions bot added the stale label Jan 17, 2026
…tate IDs

Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
@github-actions
Copy link

This PR is stale because it has been open 30 days with no activity.

@github-actions github-actions bot added the stale label Jan 20, 2026
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
@github-actions github-actions bot removed the stale label Jan 21, 2026
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
@github-actions
Copy link

This PR is stale because it has been open 30 days with no activity.

@github-actions github-actions bot added the stale label Jan 22, 2026
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
@github-actions github-actions bot removed the stale label Jan 23, 2026
@github-actions
Copy link

This PR is stale because it has been open 30 days with no activity.

@github-actions github-actions bot added the stale label Jan 24, 2026
…er and burn

Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
Signed-off-by: Jim Zhang <jim.zhang@kaleido.io>
@jimthematrix jimthematrix changed the base branch from v1-develop to pr813-rebase January 27, 2026 15:05
@jimthematrix jimthematrix changed the base branch from pr813-rebase to v1-develop January 27, 2026 15:08
@github-actions github-actions bot removed the stale label Jan 28, 2026
@github-actions
Copy link

This PR is stale because it has been open 30 days with no activity.

@github-actions github-actions bot added the stale label Jan 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants