This thread is a discussion of optimal design of proof-carrying components of data structures
Context
Actions contain two types of proofs: resource logic proofs and compliance proofs, transaction has just one proof of one type: delta proof. I’ll start the description from the action data structure as it is more nuanced and later get back to transaction.
In the current spec version all action proofs are bundled together in a set, which is suboptimal for verification. Recall that proof verification function is defined as
PS.Verify(PS.VerifyingKey, PS.Instance, PS.Proof) : Bool
With all action proofs in one set, we need to answer two questions to be able to verify the proof:
- how do we know which proof corresponds to which statement?
- where do we get the corresponding instance?
Proof records
Initially I tried to address the second question by introducing proof records, which are a data structure that has all of the required inputs for the verify function: ProofRecord = (PS, PS.VerifyingKey, PS.Instance, PS.Proof)
, with these, verification becomes easy:
all(map(lambda pr: verify(pr), action.proofRecords))
However, that was suboptimal because this way we have some data duplicated - all other action components - commitments, nullifiers, and app_data - are parts of the instances too.
Well, we could ditch them all and only keep the proofRecords
component, but every time anyone needs a set of commitments/nullifiers, they would have to call some function that assembles the required set from proofRecords
, and for applicationData
I don’t know how annoying it would be to have it split into proofRecord chunks, probably not much actually. Another issue would be with compliance proofs that require a higher-level structure - Merkle tree root - as input - the roots would have to be explicitly a part of each proofRecord
for each compliance proof, which we clearly don’t want.
So, I removed ProofRecord
. Having sets of proofs, as mentioned above, doesn’t make verification easy, so here is another option: have some structure in between plain proofs and overkill proof records.
Proposed structure for action proofs
- We split
proofs
into two structures:resourceLogicProofs
andcomplianceProofs
- These data structures are not simple sets: `
resourceLogicProofs: Map Tag (PS.VerifyingKey*, PS.Proof)` - so for each RL proof, we are told which resource is the self resource, since app_data also uses tags as keys, we can easily get the app_data instance + add all the commitments/nullifiers from the existing components
*Instead of a verifying key we actually want to have LogicRefHash
, but for simplicity let’s think of it as verifying key
For compliance proofs, we can either use a similar map with a set of tags instead of a single tag, or use proper proof records, given that
- for compliance proofs, we also need some data structure similar to
appData
for logic proofs - we don’t have to have it in a separate set
This makes binding proofs and their inputs easier:
- For RL proofs, take a proof, set the tag as the self resource, add the rest of the commitments/nullifiers as inputs, find the relevant app_data using the same tag and add it to the inputs - this should give us all the inputs required to verify RL proofs
- For compliance proofs, either take the whole proof record and verify it, or use the key as the input resource commitments/nullifiers, then use the same key for another data structure that stores the rest of the required inputs
- To ensure all resources have associated proofs, we can get all of the keys and compare them to the sets of commitments/nullifiers (should be equal), and also make sure no tag is used twice
Transaction
For the transaction delta, we might also want to modify the delta proof type to have the associated public inputs/verifier key easily discoverable, but we can’t fully describe the new structure yet because:
- there are interoperability issues (if the same transaction contains multiple RM resources, who decides which delta proof proving system to use? → what verifying key to associate?)
- delta proof constraints and inputs are not properly described yet
So I’m mostly adding this part to signal that it might require slight modifications as well
Merkle roots
I think the same problem applies to the merkle roots set. How does the verifier know which root to use for verification of a particular compliance proof? Having it included in the proof record will solve the problem, but will introduce duplication of data. At the same time, we can’t easily switch to a map because the same root might be used for multiple proofs (and we might even need multiple roots for the same compliance proof, which i now realise might be a problem for the circuit). I can’t see a smarter solution that would make root discovery easy and not imply too much data duplication. The simple solution would probably be:
- include roots in proof records
- provide not the roots in the instance, but accumulate them all together to have a single input instead of variable number of inputs, and provide the actual roots in the witness. Then verification would be:
check that the provided roots in the witness accumulate to the publicly provided value from the instance
for each consumed resource commitment, verify the path using the corresponding root
Note that fixing the compliance unit size to 1 input and 1 output resource solves half of the problem: we still need a way to figure out what root corresponds to what proof, but the circuit would expect just one root (so the solution part number two won’t be needed).
I’m not sure what advantages it might bring to have compliance units of a different size, so I would:
- fix the compliance unit size in the spec (right now it isn’t) - but that would mean the transparent case would have to follow this too
- move roots to compliance proof records from the transaction data structure