The clean-funds proof and the block list

Every transfer proves that no party to it is on the block list, without showing who the parties are.

The two rules

The Ring’s policy is a table of two rules, pinned when the Ring was initialized.

  • A sender on the block list cannot transfer.
  • A recipient on the block list cannot be paid.
RuleSubjectList
Forbidsenderblock
Forbidoutput-ownerblock

An output owner is the owner of anything a transfer creates or pays out: each recipient’s note, the note that returns the remainder to the sender, and the destination of a withdrawal.

The Ring account holds the hash of this table. ring() returns the hash and the rules, and The Ring shows them in words.

What the proof shows

The Clean proof is a zero-knowledge statement about one transfer: its sender and every output owner are absent from the list under the block list’s current root.

The chain checks that the statement is true. It does not learn the addresses the statement is about, so the same proof that keeps listed funds out keeps everyone else unnamed.

The Ring program verifies this proof before it signs anything. A transfer without a valid one does not settle.

The block list

The block list is the Solana addresses on the OFAC Specially Designated Nationals list, published by the U.S. Treasury.

Its entries live in the Ring’s block list registry, a tree in the storage program with one root. Every transfer proves against that root. The Ring shows the list’s size, its root and the time of the last sync.

The keeper

A keeper holds the policy authority role and keeps the registry equal to the public list. On a schedule it:

  1. Fetches the list from its source.
  2. Extracts the Solana addresses.
  3. Compares them with the entries on chain.
  4. Writes the added and removed entries with set_block_list.
  5. Records the sync, with the new size and root.

A transfer built against an earlier root is rebuilt against the new root and submitted again.

A listed address in the app

The app resolves every address before it asks for an amount.

  • Sending or withdrawing to a listed address: the address field says it cannot be sent to, and the flow stops there.
  • Opening an account from a listed wallet: the app says the account cannot be opened.

In the SDK, resolve returns listed: true for such an address, and any call that would move value to or from it throws an OffrecordError with the code listed.

TypeScript
const { account, listed } = await offrecord.resolve(address)

if (listed) {
  // On the block list: no proof can be built for this address.
}