How to check the correspondence of nullifiers and commitments between compliance proofs and resource logic proofs

The transaction execution requires not only proofs verification but also some out-of-proof/circuit checks. It may be necessary to explicitly explain these checks in the Specs in the future. In this post, I will focus on one specific check: ensuring the correspondence of nullifiers and commitments between compliance proofs and resource logic proofs.

The following discussion assumes in shielded cases

Why we need the check

The compliance proof constrains resource consumption and creation, returning state changes (commitments, nullifiers) in public inputs. The resource logic loads resources and constrains logics for the application. To ensure that each logic only uses resources from prior compliance proofs, validators must check them from the public inputs of compliance proofs and resource logic proofs during final transaction execution. Therefore, besides application-level constraints, the resource logic should also include some mandatory constraints and return the loaded resources (commitments, nullifiers) in public inputs.

The problems we have

Ideally, resource logic should only load the necessary resources it cares about. However, supporting the loading of an arbitrary number of necessary resources for logic may not be as straightforward as depicted in the Specs. When considering function privacy, a fundamental principle is to have all resource logic with the same circuit layout and public inputs layout. Loading different numbers of resources results in varying lengths of public inputs. It would be easy to distinguish logics even with the function privacy feature enabled. Additionally, decoding the logic’s public inputs and performing out-of-circuit checks would be challenging since public inputs are just lists of finite fields and commitments (and other items) that do not remain fixed in position.

A few potential solutions

1. The listed problems don’t matter much

The decoding problem can be solved by designing a standardized format for logic public inputs, such as including the length of loaded resources in the public inputs.

2. The current design in Taiga

The number of resources in the partial transaction is fixed, and each logic would load all resources (2 inputs and 2 outputs) based on the concerns mentioned above. Although there were padding resources, it works well but was not efficient.

3. Use a small merkle tree to encode the resources

The idea is inspired by memory sharing in some zkvm design. The original proposal comes from here in Taiga, not yet verified in practice.

To efficiently share memory and resources between compliance circuits and resource logic, we can create a Merkle tree based on inputs and outputs from compliance in one partial transaction. Instead of loading all the resources directly, the resource logic can access specific ones by following their respective paths, adding only the Merkle root to the public inputs. This approach allows for flexibility in terms of how many resources are involved in each resource logic (partial transaction), while maintaining the same structure with one root.

The partial transaction still requires a maximum number of resources, but compliance proofs for the padding resources are unnecessary. The merkle tree in the resource logic must have a fixed depth with the maximum number of resources since dynamic constraints are not permitted in circuits.

Additionally, we can also use other vector commitments to avoid checking the merkle path for further performance improvement.

If it makes sense, I’ll implement it in Taiga first to verify the design before using it in other RMs.

Discussion and feedback are appreciated.

@vveiln @cwgoes @mariari

3 Likes

While I’m not qualified to have an opinion on the solutions, I do think from a user and developer perspective, there maybe benefits to maintaining flexibility wrt. how many resources a partial transaction can contain (create and consume).

1 Like

Yes, That’s exactly what I’m looking to improve in Taiga, without giving up on privacy, and then I want to recommend it to other RMs if it makes sense.

1 Like

If I understand your proposals correctly, (3) seems most flexible and doesn’t entail any additional information leakage, but would carry some extra performance costs - is that accurate? Still, I think we should try it - we will need to heavily optimized Merkle tree lookup verifications anyways.

1 Like

I generally like the idea a lot, I think it is a nice abstraction for inputs, but have a few questions to understand it better:

  • Currently, compliance proofs are computed over pairs (r_{in}, r_{out}).
    • Would the propsed change somehow affect the Taiga compliance proof design?

(Quote for the question below)

  • Assuming that the proposed change allows us to have a flexible transaction size in Taiga, I would like to understand a bit better on an example how it would work. Imagine we have a transaction with 6 input resources, 5 output resources, 1 output padding resource.

    • Do we have one Merkle tree that contains all 12 resources, or is it one per compliance unit, so 6 Merkle trees (that is how I understood the quote above)?
    • How are the input and output resources are distinguished in the tree?
    • Does a node correspond to a resource or to a compliance unit (a pair of input and output resources)?
  • Do I understand it correctly that the idea is to have the root associated with the transaction as public input and specify what to load by adjusting the private input parameters of the logic?

I don’t understand this part. The transactions still reveal the total number of involved resources, so not having compliance proofs for padding resources would reveal the number of padding resources. In that case it isn’t clear to me why to have padding resources in the first place. Also, how would it work in the example above with just one padding resource? It won’t be a padding resource compliance proof since the proof is generated for a pair of resources one of which is not padding

2 Likes

No, the current pairs are good for parallelizing and proofs composition in the future. If we want to switch it up, we can talk about that separately later.

Assuming we use a small merkle tree of 16 leaves in the instance, we only need to pad 4 leaves without creating compliance proofs for them. One merkle tree containing all 12 resources is sufficient. Each resource logic must include the merkle root as a public input and can simply read desired input and output resources along with corresponding paths. We can compute the merkle root out-of-circuit based on the compliance public inputs (6 nullifiers, 6 commitments, and 4 padding leaves). The computed out-of-circuit merkle root must be identical with the roots from resource logics’ public inputs to ensure that each resource logic actually reads resources that are from compliance circuits.

The nullifier and the commitment go in pair. We can specify that all the left children of the deepest layer in the tree are nullifiers (inputs), while the right children are commitments (outputs). It’s easy to tell them apart by just one bit from their paths. This makes sense because we need to establish a fixed order for the leaves in order to uniformly compute the out-of-circuit Merkle root.

If the node means the leaf, it should be a resource, specifically the nullifier or the commitment of a resource.

Exactly. The root is like a compressed data containing all resources, which can be decoded by the resource logic to load desired resources without revealing which ones are loaded, also ensuring they come from the root.

The proposal doesn’t help conceal the number of involved resources in the transaction. Users can create dummy resources to achieve this goal. The proposal helps resource logic to conceal loaded resources(including the number)

In the example, the output padding resource you mentioned is necessary. I mean we only need four dummy commitments or nullifiers, and not the other four padding resources and their corresponding proofs.

3 Likes