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.