Resource logics: Connecting nullifiers and consumed resources

Within resource logics, how can I associate a nullifier present in the publicInputs : Instance (in the nullifiers : Set Nullifier field) set with the corresponding consumed resource object being part of the in the privateInputs : Witness?

In the following, I’ll provide more context before pointing to the precise problem.

First, recap that the resource logic function has the following signature:

Logic : Type :=
  (publicInputs : Instance) -> (privateInputs : Witness) -> Bool;

where

type Tag :=
  | Created Commitment
  | Consumed Nullifier;

type Instance :=
  mkInstance {
    tag : Tag;
    commitments : Set Commitment;
    nullifiers : Set Nullifier;
    appData : AppData
  };

CustomInputs : Type := AppData;

type Witness :=
  mkWitness {
    created : Set Resource;
    consumed : Set Resource;
    custom : CustomInputs
  };

Note that the sets

  • commitments : Set Commitment
  • nullifiers : Set Nullifier,
  • consumed : Set Resource
  • created : Set Resource

are all unordered.

Therefore, to connect commitments with created resources and nullifiers with consumed resources, one has to recompute the commitments and nullifiers, respectively (which also has some computational cost that we ignore here).

For commitments, this is straightforward, but for nullifiers you additionally need to pass the nullifier keys to the resource logic function prove function. Recap that nullifier keys are sensitive information, because they might allow anyone to consume the resource in a different context.
The only way I see is to provide them into the CustomInputs field of the privateInputs : Instance data (e.g., in as a Map Nullifier NullifierKey or Map Resource NullifierKey).
This might be sufficient for the shielded case. However, in the transparent case, privateInputs data is also publicly visible, so the nullifier keys would get leaked to the public (to transaction relayers/gossipers or solvers) which is not acceptable.

Checking/connecting nullifiers with consumed resources is a pattern that, IMO, will be needed frequently in apps.
What’s the solution here? Should/can we require the sets listed above to be ordered?

@vveiln @paulcadman @degregat @cwgoes

This has to be done either way - the mandatory constraint of each resource logic is to check that the commitments/nullifiers correspond to plaintexts by recomputing commitments/nullifiers.

The thing is that logics constraint the plaintexts, but we have to make sure that the plaintexts passed to the logics correspond to the publicly available commitments/nullifiers, and the way to do it is by explicitly recomputing commitments/nullifiers.

I think this is the right intuition

Why is it not acceptable? There is no such limitation on the RM level. If the transparent RM designers decide that one of the parameters has to be secret, they have to ensure there is a way to pass this secret parameter - making private inputs properly private. We can’t keep the nullifier key private by simply not using it

1 Like

Why is it not acceptable? There is no such limitation on the RM level. If the transparent RM designers decide that one of the parameters has to be secret, they have to ensure there is a way to pass this secret parameter - making private inputs properly private. We can’t keep the nullifier key private by simply not using it

I am not sure that I follow. Must private inputs be private in the transparent case or not? As of now and AFAIK, all resource logic function inputs (publicInputs : Instance and privateInputs : Witness) are part of transparent resource logic Proof and are therefore publicly visible inside Action objects to everyone getting hold of the Transaction object.
If I put sensitive keys in the CustomInputs, they would leak.

Depends on the transparent RM design. One thing is clear - you can’t have a private nullifier key without private inputs being properly private. My question is: what makes you think that nullifier keys are sensitive in the transparent case?

My question is: what makes you think that nullifier keys are sensitive in the transparent case?

If the resource has no further constraints implemented, knowledge of the nullifier key allows observers to consume the resource in a different context. This could pose a security risk.
If nullifier keys should be treated as being disposable, I need to know and have to implement other authorization mechanisms (i.e., require a signature for everything) and we need sophisticated handling of all the disposable secrets. This might be ok.

More context on how I used nullifierKeyCommitment and nullifierKey so far:
So far and since we were just PoC-ing apps, I’ve put the owning user’s external identity (i.e., the user’s public key) as the nullifierKeyCommitment so that the nullifierKey would be the internal identity (i.e., the user’s private key).
This has the convenience that I don’t need to ask users to provide a bunch of (disposable) nullifier keys to the transaction function, but it might be wrong since this could leak the private key (as explained above).

If private inputs would be properly private, then using the external and internal identity for nullifierKeyCommitment and nullifierKey might work, at least for the private testnet.
Don’t get me wrong, I don’t think that this is a good security practice. This is more a workaround because we are lacking tooling to manage/simplify the handling of disposable/non-sensitive nullifier keys.

Nullifier secret keys can be a private input to a resource logic: yes/no?

Discussion between @Michael @mariari @cwgoes :dragon:

  • Nullifier keys are not kept private in the transparent RM case, and we should not assume that they can perform any kind of role in authorization in general.
  • For now, for the transparent applications, we can use a fixed key as the nullifier key.
  • Need to have a longer-term discussion with @vveiln @xuyang et al. on how nullifier keys should be used in general and if we even need them at all.
2 Likes

To maintain consistency with shielded RM, it looks good to derive the same nullifier generation and either use a fixed key or disclose the nullifier key.

Essentially, the nullifier is designed to nullify the shielded resource without revealing which resource is consumed. The nullifier key is a necessary component for creating shielded proofs. If the nullifier key is leaked, it may only impact the privacy of linkages under certain assumptions regarding authorization methods (such as commonly used signature schemes) in resource logic.

The privacy requirement for transparent resources is unnecessary since they are all public. I’m wondering if we could eliminate the generation of nullifiers in transparent RM and simply use the resource commitment, which should be also unique as the transparent nullifier, to record consumed resources? Then nullifier keys are no longer necessary in transparent RM. Does it make any sense?

2 Likes

I’m not sure this is a good idea because then the applications that work for both RMs has to explicitly acknowledge for which RM they write the logics and maintain multiple versions for different RMs. Nullifiers are used as tags for consumed resources in many places so removing nullifiers would be a substantial change, same for removing nullifier keys.

I think it isn’t a problem if the keys are not expected to be private in the transparent case, we can just use the same mechanism as in the shielded case except everything is public, which is fine for the transparent case

2 Likes

I think it makes sense to assume nullifiers key being an accidental security measure, not the main one, and count on logics first.

While it is technically possible to have such an RM,

  1. It is way too complicated to arrange, especially for the testnet (having private inputs means involving zk proving systems)
  2. Personally, I don’t think it makes sense for our instantiation
1 Like

My questions were under the assumption that nullifier keys serve a purpose in the transparent case as well. Since this is not the case (nullifier keys are, surprisingly, meaningless in the transparent case) this resolved all my questions.

I will just note that nullifier keys complicate app logics because you need to keep them around in transaction and sometimes resource logic functions to be able to compute nullifiers. I don’t think that you can hide this complexity from developers. Consumed resources have to always be treated differently than created ones.
If this could be designed differently, that would be great.

I’m wondering if we could eliminate the generation of nullifiers in transparent RM and simply use the resource commitment, which should be also unique as the transparent nullifier, to record consumed resources?

I’m not sure this is a good idea because then the applications that work for both RMs has to explicitly acknowledge for which RM they write the logics and maintain multiple versions for different RMs.

Strong yes to writing apps for both/all RMs. Especially, because testing applications in the shielded case will be much more difficult (and, for some properties, even impossible). To debug applications and obtain meaningful error messages, you want to use a transparent RM.
If nullifiers are eliminated (which would be great from a design perspective) then this should be done for all RMs (which might not be possible).

2 Likes

I’m not suggesting removing nullifiers; rather, I’m questioning if we can generate nullifiers without the need for nullifier keys, such as using commitments as nullifiers? This approach appears to be the same as using a fixed nullifier key as mentioned earlier.

It also makes sense to me to unify the nullifier generation for all RMs and just disclose the nullifier key in transparent RM.

AFAIK, with “fixed nullifier key” @cwgoes meant a fixed key per identity (e.g., user’s private key + salt) and not a fixed nullifier key for all resources (e.g., 0x00…00).
This doesn’t leak the user’s private key, still allows to formally and visibly assign resources to someone, and is easier to handle than single-use nullifier keys for individual resources (which would be hard to keep track off without wallet-tooling that we currently lack).

1 Like

I also understand that, since the nullifier key commitment now corresponds to the public key derived from the user’s salted private key, this is what needs to be queried for when asking an indexer for owned resources.

It is a bit weird. We use nullifierKeyCommitment to visibly express ownership of resources (and indexer will maintain databases over this), but kind of rely on a second authorization mechanism to enforce ownership (since nullifier keys are meaningless in the transparent case). It would be great if this design could be improved @vveiln

Alternatively, we could establish an ownership standard and store this in the value field so that indexers can index it properly.

I think we should probably do this. I would consider the nullifier key a technical detail that must be dealt with properly for shielded resources, but that we should avoid relying on for any desired properties of applications (such as authorization), indexing, etc.

@xuyang @vveiln What role exactly do nullifier keys play in shielded proofs? Is it just randomness to ensure unlinkability, or is there some additional property being provided?

2 Likes

Fully agreed! Then I will think about this standard. It doesn’t require much change in the apps, since ownership is already abstracted by a trait, so it is easy to replace.
I guess, since we want to allow app devs to put more data besides the ownership information in the value blob (referenced in the resource by the valueRef field), this should be a map.

I remember we talked about the relationship between ownership and nullifier keys. @vveiln has a doc explaining all the details. Could you kindly share the link?

In short, resources can be created by senders, third-party actors (including solvers), or owners themselves in transactions. The creator must have knowledge of the receiver’s nullifier public keys (or nullifierKeyCommitment) to create resources. However, the nullifier keys are not supposed to be disclosed to creators due to privacy concerns in shielded RMs. The nullifier key serves as a private witness for owners to consume the resource. Therefore, if we want to specify a receiver for the resource when creating, we need to obtain the nullifierKeyCommitment along with the authorization public address (such as the public key of signature scheme) from ‘value’ and constrain them in resource logic.

To remove ambiguity in the context if I misused some terms: nullifier key = nullifier private key; nullifierKeyCommitment = nullifier public key

1 Like

I find it okay because consumed resources are different from created resources.

For created resources we have the resource’s identity represented by its commitment: cm = h_{cm}(r) which requires the private data r to compute it. For consumed resources we have the resource’s identity represented by the nullifier: nf = h_{nf}(r, nk) which requires private data (r, nk) to compute it. Creating a more visual map:

  • cm \rightarrow r
  • nf \rightarrow (r, nk)

These cases surely are different, but I don’t know if it brings that much complexity.

Yeah, I think it should be treated more like the rseed component or something.

This is probably the doc you are referring to: https://hackmd.io/Zk7NVp51R9ePWjufxarhkg?both

Some of it got merged into the RM report/specs as well.

3 Likes

Are they? All fields are identical. They only differ in their lifecycle status.

In essence a resource, consumed or created, doesn’t change, but depending on its lifecycle status the resource logics have different constraints on them, in that sense they are different