Generic Private Logic Upgrades(Migration) via the Conversion Circuit

Background

In the Anoma Resource Machine (ARM), a resource kind is derived from the resource logic verifying key and the resource label.

resource_kind = H(logic_vk || resource_label)

This property is desirable because it cryptographically commits every resource to the exact logic that governs it. Resource kinds are immutable, and historical resources remain verifiable forever.

However, this also makes application upgrades difficult. Any modification to the resource logic produces a new verifying key, resulting in a new resource kind. Consequently, existing resources cannot be directly balanced against resources created by the new logic.

Instead of introducing mutable logic registries or application-specific migration circuits, we can adapt Namada’s conversion circuit mechanism to support resource-kind conversions.

The key idea is that a logic upgrade is simply an authorized equivalence between two resource kinds.

Unlike migration circuits, the conversion circuit never consumes resources or creates resources. It only contributes an additional balancing term to the transaction.


Logic Conversion Tree

Introduce a global Logic Conversion Tree, analogous to Namada’s Conversion Tree.

Each leaf contains an authorized conversion pair:

(old_resource_kind, new_resource_kind)

For example:

(KindV1, KindV2)
(KindV2, KindV3)
(KindA, KindB)

Every conversion is implicitly 1:1.

Registering a conversion pair indicates that resources of the two kinds are considered equivalent for balancing purposes.

How conversion pairs are registered is intentionally orthogonal to this proposal. They may be authorized by application governance, multisig approval, protocol governance, or any other mechanism.

Once registered, every user may utilize the conversion permissionlessly.


Generic Conversion Circuit

ARM provides a single generic conversion circuit.

The circuit receives as public inputs:

  • the Logic Conversion Tree root,
  • the conversion commitment.

The circuit receives as private witness:

  • the old resource kind,
  • the new resource kind,
  • the converted quantity,
  • a Merkle proof against the above root,
  • a random blinding factor.

The circuit proves:

  • the Merkle proof is valid against the declared tree root;
  • the conversion pair exists at the proven leaf;
  • the conversion ratio is 1:1;
  • the conversion commitment is correctly formed from the witness.

The tree root is committed on-chain at each block. A transaction proof must reference a root that the protocol considers current or within an acceptable anchor window. Proofs against stale roots are rejected.

Importantly, the circuit never verifies resource logic, consumes resources, or creates resources.

Its sole purpose is to prove membership in the authorised conversion set and produce a well-formed balancing commitment.


Conversion Commitment

Rather than revealing the conversion publicly, the circuit outputs a commitment:

C = Commit(
    old_kind,
    new_kind,
    quantity,
    randomness
)

The commitment is both binding and hiding. Only the commitment is revealed; the following remain private:

  • the source resource kind,
  • the destination resource kind,
  • the converted quantity,
  • the randomness.

To external observers, every conversion commitment appears as a random group element.


Transaction Balancing

Suppose a transaction consumes a resource of KindV1 with quantity q and creates a resource of KindV2 with the same quantity. Without a conversion proof, the balancing equation fails because the resource kinds differ.

The conversion circuit contributes a hidden balancing term:

Commit(KindV1, KindV2, q, r)

The transaction proof then demonstrates:

Inputs − Outputs + ConversionCommitments = 0

without revealing the underlying kinds or quantities.

Conceptually, the conversion commitment acts exactly like Namada’s conversion balance: it adjusts the balancing equation without touching any resources.


Composable Upgrades

Since conversion only affects balancing, upgrades compose naturally with arbitrary application logic.

For example:

Consume:    OldToken
Create:     NewToken, Payment, Change
Conversion: Commit(OldKind, NewKind, amount, r)

The conversion is invisible to the rest of the transaction. Applications do not require dedicated migration transactions; users simply spend old resources as usual and the balancing proof accounts for the version upgrade.


Sunset of Old Resource Kinds(optional)

Registering a conversion pair does not invalidate the old resource kind. Resources of KindV1 remain creatable under V1 logic indefinitely. This matters when a logic upgrade is motivated by a security fix: an attacker can still exploit the vulnerability in V1 to mint illegitimate resources and immediately convert them to KindV2, bypassing the fix entirely.

To close this gap, the conversion tree leaf may carry an optional sunset block height:

(old_kind, new_kind, sunset_height)

After sunset_height, the protocol refuses to consume or create resources of old_kind. The conversion circuit includes sunset_height in the leaf commitment, and the transaction proof checks that the current block is at or before the sunset.

This gives users a finite window to migrate their resources, after which the old logic is effectively retired by consensus rather than by logic modification.

Two approaches are worth considering:

Protocol-enforced sunset. The sunset height is committed in the conversion tree leaf and enforced by the balance circuit. This requires no change to existing resource logic but adds a protocol-level mechanism outside the conversion circuit itself.

Logic-gated sunset. The V1 logic itself gates spending on the absence of a registered conversion pair, or checks a block-height condition. This keeps enforcement inside resource logic but requires V1 to have been written with migration awareness.

For security-motivated upgrades, a protocol-enforced sunset is preferred because it does not depend on the old logic having been written correctly.


Privacy

Publishing conversion pairs directly would reveal which application is upgrading, which logic versions are involved, and how many resources were migrated.

Using commitments hides all of this. An observer cannot distinguish whether a conversion KindV1 → KindV2 or KindX → KindY was used, nor can they determine the quantity converted. This preserves ARM’s privacy guarantees even during application upgrades.


Homomorphic Construction

Ideally, the conversion commitment shares the same algebraic structure as the commitments already used in transaction balancing. A natural choice is:

ConversionCommitment = q · G(new_kind) − q · G(old_kind) + r · H

where G(kind) is a generator deterministically derived from the resource kind, H is the blinding generator, and r is fresh randomness.

The generic conversion circuit proves that the generators correspond to a registered conversion pair and that the commitment is correctly formed. Transaction balancing then reduces to simple group addition: the verifier observes only the final sum and a zero-knowledge proof, learning nothing about the resource kinds or quantity.


Advantages

Compared with application-specific migration circuits, this approach has several advantages.

Generic

A single conversion circuit supports every application.

Immutable Logic

Resource kinds remain cryptographically bound to immutable verification keys.

Permissionless

Once a conversion pair is registered, every user can upgrade resources without requiring application-specific circuits.

Private

Neither the converted resource kinds nor the migrated quantity are revealed.

Composable

Conversions participate only in balancing, so they compose naturally with arbitrary resource operations.

Protocol Minimality

No mutable logic registry, version identifiers, or upgrade mechanisms are introduced into ARM itself.

The only additional component is a conversion tree and a reusable conversion circuit.


Open Questions

Several design questions remain open.

Registration Policy

Who may register conversion pairs? Possible choices include:

  • application governance,
  • protocol governance,
  • multisignature approval,
  • cryptographic authorization from both logic versions.

This proposal intentionally separates authorization from the conversion mechanism itself.

Resource Equivalence

A registered conversion asserts that two resource kinds represent equivalent resources. How this equivalence is established is outside the scope of the conversion circuit. The registration authority is responsible for ensuring that the upgraded logic preserves the intended semantics.

Sunset Policy

If a sunset height is included in the leaf, who chooses it and can it be extended? Extending a sunset requires updating the tree (and the root), which may invalidate in-flight proofs. A conservative default is to make sunset heights immutable once registered and to require a new conversion pair registration if an extension is needed.

Conversion Ratios

This proposal assumes all logic upgrades are 1:1 conversions. Future extensions could support more general ratios, although these are unlikely to be necessary for logic upgrades.


Conclusion

This proposal adapts Namada’s conversion-circuit design from asset conversions to logic upgrades.

Instead of treating a logic upgrade as a resource transformation, it is modeled as an additional balancing relation between two resource kinds.

A global Logic Conversion Tree authorizes valid conversions, while a single generic conversion circuit proves membership in that tree and produces a private conversion commitment. The commitment participates in transaction balancing without revealing the resource kinds or quantities involved.

The result is a generic, composable, and privacy-preserving upgrade mechanism that preserves ARM’s fundamental property that every resource kind is permanently bound to an immutable resource logic.

1 Like

Merging the Conversion Mechanism into the Compliance Circuit

The conversion circuit design introduces two new on-chain structures:

  • a Logic Conversion Tree storing authorized (old_kind, new_kind) pairs, and
  • a generic conversion circuit that proves membership in that tree and produces a balancing commitment.

This is clean as a standalone proposal. But looking at what the compliance circuit already does — computing homomorphic resource commitments keyed on resource kind — these two structures turn out to be the same thing in different clothes. This post works through that observation and explores what a merged design would look like.


What the Compliance Circuit Already Does

ARM’s compliance circuit enforces the balance equation. Each resource contributes a resource commitment:

ResourceCommitment(kind, q, r) = q · G(kind) + r · H

where G(kind) is a generator derived deterministically from the resource kind, H is the blinding generator, and r is fresh randomness. The circuit sums these commitments over all consumed and created resources and checks that the total is zero:

Σ ResourceCommitment(input_i)  −  Σ ResourceCommitment(output_j)  =  0

To do this, the circuit needs a kind table: a mapping from resource kind to its generator G(kind). This table is trusted infrastructure — the circuit takes a Merkle proof from it for each resource kind it encounters.


What the Conversion Circuit Adds

A conversion commitment for upgrading q units from old_kind to new_kind is:

ConversionCommitment = q · G(new_kind) − q · G(old_kind) + r · H

This can be rewritten as:

ConversionCommitment = q · ΔG(old_kind, new_kind) + r · H

where:

ΔG(old_kind, new_kind) = G(new_kind) − G(old_kind)

Compare this to a resource commitment:

ResourceCommitment(kind, q, r) = q · G(kind) + r · H

The structure is identical. A conversion commitment is a resource commitment whose “generator” is the difference of two kind generators. The conversion circuit is the compliance circuit, evaluated on a delta generator instead of a plain generator.

The kind table maps kind → G(kind). The Logic Conversion Tree maps (old_kind, new_kind) → ΔG. Both map a key to a generator. The compliance circuit uses the generator to compute a commitment. The balance check sums commitments.

There is no structural reason for these to be separate.


A Unified Generator Table

Merge the two structures into a single Unified Generator Table — a Merkle tree whose leaves are one of two types:

Kind leaf:
    type:       KIND
    key:        kind
    generator:  G(kind)

Conversion leaf:
    type:       CONVERSION
    old_kind:   old_kind
    new_kind:   new_kind
    generator:  ΔG = G(new_kind) − G(old_kind)
    sunset:     block_height (optional)

The leaf hash includes the type tag, so kind leaves and conversion leaves are cryptographically distinguished. The tree has a single root committed on-chain at each block.

Kind leaves are permissionless: G(kind) is a deterministic function of kind, so anyone can add a correctly-formed kind leaf and anyone can verify it independently. The protocol checks correctness at registration time.

Conversion leaves require external authorization. The protocol enforces whatever authorization policy is in place (governance vote, multisig approval, etc.) before inserting a conversion leaf. The circuit itself does not verify authorization — it trusts that any leaf present in the committed tree has already been authorized.


The Unified Compliance Circuit

With the unified table, the compliance circuit is extended to handle both leaf types.

Public inputs:

  • The Unified Generator Table root
  • One combined balance commitment C

Private witness, per resource:

  • The resource kind and quantity
  • A Merkle proof of the kind leaf → G(kind)

Private witness, per conversion:

  • The old and new kind
  • The converted quantity
  • A Merkle proof of the conversion leaf → ΔG
  • The sunset height (if present in the leaf)

Private witness, global:

  • A single randomness r

What the circuit proves:

For each resource:

  1. The Merkle proof is valid against the declared tree root.
  2. The leaf is of type KIND.

For each conversion:

  1. The Merkle proof is valid against the same tree root.
  2. The leaf is of type CONVERSION.
  3. If sunset is present, the current block is at or before sunset.

Delta commitment:

The circuit computes the net generator sum over all resources and conversions in this unit:

S = Σ_i  sign_i · q_i · G(kind_i)
  + Σ_k  q_k · ΔG_k

where sign_i = +1 for inputs and −1 for outputs.

S is the balance contribution of this compliance unit. It is not required to be zero within a single unit — a transaction may span multiple compliance units, and the zero-sum check is enforced at the transaction level:

Σ_units  C_unit = 0

The unit publishes one delta commitment with a single randomness r:

C = S + r · H

Since S is not fixed to zero, r can be chosen freely and C is a proper hiding commitment. All intermediate values — the individual resource kinds, quantities, and conversion details — stay private inside the circuit. The ZK proof attests that C is correctly formed from the witness and that each Merkle proof is valid.


Worked Example

Suppose a user holds q units of KindV1 and wants q units of KindV2, where (KindV1, KindV2) is a registered conversion pair.

The transaction has:

  • One input resource: (KindV1, q)
  • One output resource: (KindV2, q)
  • One conversion entry: (KindV1 → KindV2, q)

The circuit looks up three generators from the unified tree:

Component Leaf type Generator
Input KindV1 KIND G(KindV1)
Output KindV2 KIND G(KindV2)
Conversion (KindV1, KindV2) CONVERSION G(KindV2) − G(KindV1)

The net generator sum for this unit:

S = +q · G(KindV1)               [input]
  −  q · G(KindV2)               [output]
  +  q · (G(KindV2) − G(KindV1)) [conversion]
  =  0

In this single-unit transaction the contributions happen to cancel, so S = 0. In general, a transaction with multiple compliance units would have each unit contribute a non-zero S, with the deltas summing to zero across all units.

The circuit publishes one delta commitment:

C = S + r · H

All three Merkle proofs target the same tree root. The unit produces one ZK proof and one public group element C. No individual per-resource or per-conversion commitments are exposed.


Comparison: Separate vs. Unified

Separate circuits Unified compliance
On-chain roots Kind table root + Conversion tree root One unified root
Public commitments per transaction One per resource + one per conversion One combined commitment
Randomness values One per resource + one per conversion One
Proof types Compliance proof + Conversion proof One compliance proof
Tree updates Two independent trees to update One tree update per change
Circuit complexity Two simpler circuits One slightly more complex circuit
Non-migrating transactions No conversion overhead Still one tree root in all transactions
Authorization enforcement Separate per-tree policy Single tree update policy, two leaf types

The unified approach reduces the number of on-chain commitments from N (one per resource plus one per conversion) to one, and collapses all randomness to a single value. Every transaction already produces a compliance proof; the merge folds conversion authorization into that proof with no additional ZK proof or additional randomness required.

The main trade-off is inclusion overhead: transactions that perform no conversions still reference the unified tree root. In the separate design, a non-migrating transaction has no knowledge of the conversion tree at all.


Authorization Across Leaf Types

The most important design decision in the unified tree is how leaf-type authorization is enforced.

Kind leaves are self-authorizing: the generator G(kind) is a deterministic function of kind, which is itself a deterministic function of (logic_vk, label). Anyone can compute and verify a kind leaf without external approval.

Conversion leaves are governance-authorized: the choice of which pairs are equivalent is a trust decision that cannot be derived from the pair alone. It must come from an external authority.

In the unified tree, these two authorization modes coexist. The cleanest way to handle this is to push authorization to the tree update protocol rather than the circuit:

  • Protocol transactions that add a kind leaf check: G == hash_to_curve(kind). No further approval needed.
  • Protocol transactions that add a conversion leaf check: a valid authorization proof (governance signature, multisig threshold, etc.). The circuit then trusts the tree root, which only contains leaves that passed this check.

The circuit remains agnostic to authorization policy. It checks leaf type and verifies the Merkle proof. The tree root is the trust anchor.


Summary

The conversion mechanism and the compliance circuit are doing the same thing: mapping a key (a resource kind or a conversion pair) to a generator, then using that generator to compute a homomorphic commitment. The balance equation is the same group equation in both cases.

Merging them collapses two Merkle trees into one and eliminates the conversion circuit as a separate proof type. Because all terms are computed inside a single circuit, only one combined balance commitment is published and only one randomness is needed — down from one per resource and one per conversion in the separate design. The unified compliance circuit handles both resource kinds and conversion pairs against one tree root, producing one proof and one public group element per transaction regardless of whether a conversion is present.

The key design decision is keeping authorization enforcement in the tree update mechanism rather than the circuit. Kind leaves are self-verifying; conversion leaves are governance-authorized. The circuit trusts the tree root as the sole trust anchor and remains agnostic to how that root was formed.

1 Like

This is great. I think, we can slightly generalize the generator field in the conversion leafs such that we can (hot-)fix (overly permissive) resource logics, namely

generator: ΔG = G(new_kind) − G(old_kind) + G(bug_fix_kind)

where bug_fix_kind’s resource logic imposes additional constraints that rule out all those scenarios that were considered overly permissive. I think, then we also don’t need to name any sunset block height: the bug fix becomes effective immediately.

One last thing I wonder is whether we also want to have a mechanism to “unbrick” overly restrictive resource logics.
I think, the proper pattern for any resource logic is of the form \mathrm{main_logic} \lor \mathrm{override_logic} and the override condition can be used to upgrade logics via the above (generalized) generator field.

1 Like

Thank you for the proposal, I think it is a solid idea for soft migrations when a developer wants to update the circuit, but for hard security upgrades I see a few questions we need to answer first.

  1. When developing a migration mechanism, we were thinking of the situation when the old circuit is compromised and can no longer be trusted. The proposed approach implies that we still can verify the logic proof of the “old” resource and produce a valid compliance proof for that resource, which is not the case in that context. The resources associated with compromised logic circuits have to be verified some other, non-resource-native way, as such compromised-logic resources can no longer be considered and treated as valid ARM resources.
    • At the same time, we do need to verify them, as the conversion circuit only ensures that the resource belongs to a kind that has a conversion entry, not that it is a valid member of the kind.
    • As far as I understand, if we merge the two circuits, we won’t be able to use the conversion circuit for the “deprecated” resources at all, since it acts on the resources that are also inputs to the compliance circuit and “deprecated” resources cannot be treated this way [1].
  2. As far as I can see, this approach is bi-directional and there is nothing that prohibits from converting from new to old before the sunset timeout is reached. While it can be useful for other conversion mechanics, it shouldn’t be allowed for either soft or hard migrations

  1. If we kept the circuits separate, we could in theory run the conversion circuit for the pair (“deprecated”, “valid”) to validate the conversion, validate the new resource the usual way with the compliance circuit, and validate the deprecated resource some other way - e.g., using the migration circuit approach. ↩︎

1 Like

Indeed, the short note above does not properly capture this, because the conversion commitment is optional and a separate mechanism for avoiding exploits needs to be in place anyway.

Thank you for the feedback!

These are exactly the right questions. Let me share how I currently think about the three distinct cases, acknowledging that some of this is still in flux.


Case 1: Pure Conversion (No Migration)

The original conversion circuit design applies here unchanged. The old logic is still active and trusted. The conversion pair (KindA, KindB) simply declares that resources of two kinds are equivalent for balancing purposes, without any deprecation intent. This covers 1:1 conversions today and could extend to arbitrary-ratio conversions or native resource swaps in the future.


Case 2: Soft Migration

This is the case where a developer is upgrading the resource logic or as long as the old authorization scheme(typically a standard signature check)is still functional. The old circuit can still be executed and produce valid proofs; it is deprecated but not broken.

The conversion mechanism handles the balance gap. But you are right that we also need to prohibit creation of new resources under the old logic once a migration is registered. Without this constraint, the migration is not really a migration, users can continue minting V1 resources indefinitely and converting them to V2, which defeats the purpose of upgrading.

The right place to enforce this is the compliance circuit, may not the conversion circuit. The conversion circuit is optional and only participates in balancing; it has no visibility into whether a resource is being consumed or created. The compliance circuit, by contrast, sees every resource in every role. This is why I proposed merging conversion into compliance. Yes, it makes more sense to add a deprecated or upgrade flag to the kind table entry, the compliance circuit can:

  • Allow consumption of deprecated-kind resources (users must be able to spend what they hold)
  • Reject creation of deprecated-kind resources (no new V1 resources after the migration is registered)

Together with the conversion pair, this enforces a clean one-way migration: old resources can be spent and converted, but no new old-kind resources can enter circulation.


Case 3: Hard Migration

You are correct that the conversion proposal does not cover this case. In a hard migration, the old circuit is compromised and cannot be trusted. Resources of the old kind cannot have their logic proofs generated or verified in the normal way, so they cannot be inputs to the old logic circuit at all.

The current approach in Anoma Pay uses an ephemeral resource to consume the deprecated resource without going through the old logic. And there are dedicated constraints to handle the migration in the new logic. Deprecated resource consumption is happening in the new logic, which is a bit weird to me.

Alternatives, we could register a (deprecated_logic, updated_logic) pair in the kind table/tree and have the compliance circuit substitute the updated logic when verifying the logic proof for a deprecated resource. Concretely:

  • The deprecated resource is consumed.
  • Instead of verifying its proof against the deprecated logic, the compliance circuit looks up the registered substitution and verifies against the updated logic by putting the updated logic into the logic commitment.
  • The updated logic is responsible for authorizing the spend and the subsequent resource creation.

This is a stronger protocol-level intervention than the conversion mechanism — it changes which verifying key is used for proof verification, not just how the balance is computed. It requires explicit compliance circuit support and represents a fundamentally different trust model from Case 2. It’ll probably work, but I’m not totally sure about the security, so let’s be careful.


On Bidirectionality

The conversion is single-directional by construction.

Given a leaf (old_kind, new_kind), the circuit produces:

conversioncommitment = q · (G(new_kind) − G(old_kind)),  q > 0

The balance equation is:

Σ consumed_commitments − Σ created_commitments + Σ conversion_commitments = 0

For forward conversion (consume V1, create V2):

q · G(V1) − q · G(V2) + q · (G(V2) − G(V1)) = 0  ✓

For an attempted backward conversion (consume V2, create V1) using the same leaf:

q · G(V2) − q · G(V1) + q · (G(V2) − G(V1))
= 2q · (G(V2) − G(V1))  ≠ 0  ✗

The balance does not hold. Backward conversion would require a separate (new_kind, old_kind) leaf, which the registration authority would not issue for a migration pair. Swapping the roles of old and new, or negating q, is not possible because both the leaf direction and the positivity of q are enforced by the circuit.

1 Like

Regarding the soft migration case:

So the application developer would have to contribute to the kind table with the information about the deprecated kind / sunset block. This is blurring the boundaries a bit since compliance circuit is in principle only specific to the arm, but now the developer would actively contribute to the rules according to which the compliance circuit accepts or rejects a transaction.

Another question is about the table maintenance. Who would maintain the table? How can we ensure that the table is up-to-date across multiple instances? Otherwise it might happen that some of the instances still allow the creation of such resources if the table is outdated

These are just some thoughts/questions to answer, but they are not deal breakers.

Regarding the hard migration substitutes:

I think verifying the old resources with the new logic is not the safest idea. It will add a lot of confusion.

This might be not possible if the old resource has a different “format”, e.g., if the security upgrade requires changing how value/label are computed which will cause either under- or overconstraining the old resources.

Imo we need to enable a separate mechanism for such deprecated resources if we want to change the migration design from how it works now.

General question

I think the general question about this proposal is what it is attempting to achieve. We need to compare the costs of incorporating it to the value it gives.

If it were optimizing the hard upgrade mechanism, the value of it would be extremely high.

Without the hard upgrade mechanism, the proposal becomes rather a feature and the question is if the costs of incorporating this feature (the effort of integrating the new circuits, effect on proving/verification times, LOC that enable potential bugs, and general codebase complexity) match the practical value it provides. How likely we are to be in the situation when such a mechanism would be of use?

I think the most realistic scenario to consider now is to think how the proposal would would work for TTC and GC circuits specifically. What would we be able to achieve? Upgrading these circuits wouldn’t be about adding “nice” functionality since it is just too expensive at the moment. So if we were to upgrade a circuit, we would have to have a strong motivation. The strongest motivation is security, but if our concern is security, we don’t want to enable soft migration and we might not want to allow the old logics to verify even if the verifier itself is unaffected.

Another strong motivation could be adding a game changer clause to the logic that would enable something super cool in the apps. It could be the limitation of my imagination, but I cannot think of such a mechanism that would be better incorporated in the existing circuit vs creating a completely different application, for example. After all both TTC and GC circuits are quite minimalist in what they are trying to achieve. As of now circuits can hardly be considered evolving components, they behave more like constants.

To summarize, my current impression is that this mechanism is nice for the long-term mechanics, but I’m unsure if it will be beneficial in the foreseeable future and I do think we need to focus on what we have now and how we can make it safer, faster, and simpler (without compromising the long-term, of course). Let me know if I’m missing something in my analysis.

1 Like

On Boundary Blurring

In ARM there are only two places where constraints and checks can be enforced: inside the compliance proof, or out-of-circuit by the verifier against the public inputs. Either way, the application owner must contribute information about the deprecated kind. The question is only where that enforcement happens.

The out-of-circuit approach is the simpler path given the current architecture. With the trivial logic commitment in place (before function privacy is enabled), resource kinds are visible in the public inputs, and the verifier can straightforwardly reject transactions that create resources of a deprecated kind. No changes to the compliance circuit are needed.

Once function privacy is enabled, resource kinds are no longer visible in the public inputs. At that point, the deprecation check must move inside the compliance proof. Alternatively, a new mandatory protocol mechanism could be introduced to handle this.

You suggested adding a “deprecated” flag in the separate conversion circuit to block deprecated resources—but I’m not clear on how it works. Could you explain? The conversion proof is optional — a transaction only includes it when performing a kind conversion. If the check lived only there, a user wanting to create or consume a deprecated-kind resource could simply omit the conversion proof and bypass the restriction entirely. The enforcement must sit in a component that runs unconditionally for every transaction: the compliance circuit or the verifier’s out-of-circuit validation.

On Table Maintenance

This infrastructure already exists. In v2 we introduced a kind lookup table (KindTableEntry, mapping (logic_ref, label_ref) to a pre-computed EC point) to avoid running hash_to_curve inside the circuit for known resource types. Its SHA-256 hash is committed as kind_table_commitment in the compliance public inputs, and the verifier already checks it.

Adding a deprecated flag to table entries is therefore a natural extension of existing infrastructure. The verifier is the single source of truth in both enforcement paths:

  • Out-of-circuit enforcement: the ARM verifier (the contract in the Anoma Pay PA) maintains the full table directly and consults it when validating transactions.

  • In-compliance-circuit enforcement: the ARM verifier maintains the table hash. Because the hash appears in the compliance public inputs, any proof generated against a different table is rejected. The verifier’s hash represents the latest authoritative state.

In either case, consistency is guaranteed by the verifier: all nodes running the same verifier contract see the same table or the same hash, and proofs referencing a stale state are rejected.

On the the hard migration substitutes

Yes, this approach is really confusing. I was actually trying to work through your own suggestion: “we could in theory run the conversion circuit for the pair (‘deprecated’, ‘valid’) to validate the conversion, validate the new resource the usual way with the compliance circuit, and validate the deprecated resource some other way — e.g., using the migration circuit approach.”

The updated logic is written to explicitly handle both formats — two distinct validation paths within the same circuit:

  • New resource path: validates the resource the normal way, checking the new format and its invariants.
  • Old resource path: accepts the old format as-is and checks migration authorization — i.e., that the owner has legitimately authorized this consumption as part of a migration.

The new logic branches on the resource format, so there is no mismatch. Old resources are never evaluated against new-format constraints; they are evaluated against migration-specific authorization constraints.

This is essentially the pattern the current Anoma Pay v2 migration circuit already implements: the v2 logic accepts both old and new resources and applies the appropriate validation to each. The updated logic carries permanent knowledge of the old format, and any further upgrades would need to handle an ever-growing list of prior formats — but it is workable and does not violate correctness.

That said, designing a truly generic migration circuit looks difficult. Resource formats and authorization schemes vary across applications: how value and label are computed, what constitutes valid authorization, and what invariants must hold during migration are all application-specific. A generic circuit would need to accommodate all of this variation, which either requires a rich format description language embedded in the circuit or pushes the application-specific logic back into a per-application component anyway. In that light, encoding both old and new format handling directly in the updated logic — as the application developer who knows both formats best — may be the most pragmatic path for hard migration, even if it is not the most elegant.

On the general question

Yes, we should never complicate the protocol in ways that make it harder to integrate, maintain, or reason about, or that introduce new sources of uncertainty. I agree the hard migration approaches I described aren’t optimal or elegant, and we won’t use them in practice. It’s essentially the same idea as our current migration circuit. A generic hard migration mechanism remains an open design problem and deserves a dedicated effort in the long term. For the short term, and for TTC specifically, soft migration should cover most realistic upgrade/migration scenarios, as long as the ECDSA keys and the authorization path in TTC remain uncompromised. Only conversion is allowed for deprecated resources, and that’s when the conversion mechanism works.

we might not want to allow the old logics to verify even if the verifier itself is unaffected.

If the verifier is unaffected, I don’t see a strong reason to prohibit the old logic from being used to consume existing resources, the security boundary is still intact. Preventing it would impose an unnecessary burden on users who hold old-kind resources through no fault of their own.

but I cannot think of such a mechanism that would be better incorporated in the existing circuit vs creating a completely different application, for example

For stateless circuits like GC, that is exactly right — you can stop the old GC, deploy a new one with updated logic, and direct users to it. There is no accumulated state to carry over, and out-of-circuit checks in the PA can currently enforce that the old GC is no longer accepted. Creating a new application is the simpler and cleaner path in that case.

The situation is different when resources carry state that is bound to the logic. You cannot simply point users at a new application — the existing resources are permanently tied to the old kind and cannot balance against resources of the new kind without an explicit bridging mechanism. Creating a completely new application does not help existing holders of old-kind resources; they have no way to bring their state across. The conversion mechanism is what makes that possible.

As of now circuits can hardly be considered evolving components, they behave more like constants.

This is true precisely because there is no upgrade mechanism, but that is the problem, not a design goal. Circuits being immutable in practice is a limitation of the current system, not an argument against building the means to evolve them. A well-designed system should allow circuits to be upgraded when needed; the absence of such a mechanism is what this proposal is trying to remedy.

This proposal grew out of work I was doing last week while porting the migration circuit to Solana. I briefly mentioned it before but details were not clear. Since we already have an emergency migration circuit and there is no urgent migration requirement at the moment, I do not plan to invest much more time in this direction for now. Happy to revisit when the need becomes more concrete.

I’m not exactly sure what you are referring to here, to be honest.

I think it works there now because it is basically a custom application circuit. As you noted, it is really hard to generalize. I think it is safe to conclude that the approach we are discussing here is not applicable to hard migrations

I’m just wondering how likely it is that we have such a scenario that doesn’t involve the critical security paths.

The reason is that we might find a vulnerability in the circuit itself, e.g., underconstraining something and leaving a path for exploitation. You see it happening even to Zcash circuits which are a quality standard for the industry

I agree, but my point is that if you want to add a game changer clause, e.g., booking plane tickets with AP, it just seems like it better to implement it as a separate application from both architectural and security perspective. So you are right that the conversion approach would solve the problem if we were to add this functionality, but the question is: do we need this functionality in this particular app in the first place?

I think it is a good point you made here. But I also wonder how much this immutability is related to security as well, the fact that we sort of need reaudit the circuit each time we upgrade it. I think another aspect is that we also just don’t have a lot of space for actions with circuits. They can’t evolve a lot because we are so limited by the cost of proving and verification.

To summarize, I think this is truly a great and elegant idea and I think we should def integrate it at some point. But perhaps integrating it at a more mature stage when there is clear demand, when we have gathered more information, and hopefully a bit less limited in compute (although I’m not counting too far on the latter..) would make more sense

Compliance circuit benchmark

All times are criterion medians on GPU NVIDIA 5090 .

Resource size scaling

Original compliance circuit without conversion constraints:

resource count empty_table lookup_table
2 2.477 s 809.3 ms
4 3.842 s 897.3 ms
8 7.006 s 1.458 s
16 13.615 s 1.582 s

Compliance circuit with conversion constraints enabled (0 conversions):

resource count empty_table lookup_table
2 2.594 s 904.4 ms
4 3.676 s 938.3 ms
8 6.929 s 1.389 s
16 12.126 s 1.507 s

lookup_table amortizes the hash_to_curve cost so proving time stays nearly flat up to 16 resources. empty_table scales roughly linearly because each resource requires a fresh curve hash.

Conversion count (1 resource pair, conversion table size 1, lookup-table path)

conversions time
0 924 ms
1 1.038 s
2 1.367 s
4 1.668 s
8 2.854 s

The 0-conversion baseline matches lookup_table/2. Each additional conversion witness adds roughly 200–350 ms; cost scales approximately linearly.

Conversion table size (1 resource pair + 1 conversion, lookup-table path)

table rows time
1 913 ms
4 1.314 s
16 1.358 s
64 2.996 s

Cost is flat between 4 and 16 rows. The jump at 64 rows reflects the conversion table scan crossing a memory threshold.

Conclusion

Conversion support has no overhead when unused. Enabling the conversion constraints with 0 witnesses and 0 conversion table rows does not affect proving time — performance is identical to the original circuit without conversion constraints. Cost only grows with actual conversion witnesses: each one adds roughly 200–350 ms, scaling approximately linearly

Conversion table size has slight impact up to ~16 entries but jumps sharply at 64 rows (+1.6 s). Keeping the conversion table under 32 entries avoids the memory-threshold penalty. The conversion table is currently a flat vector scanned linearly inside the circuit; If it requires large conversion tables, replacing it with a Merkle tree would reduce the in-circuit cost to O(log n) membership proofs.