The Migration Application Design Proposal

Terminology used in this post:

  • halted PA - the PA that was stopped
  • halted state - the state of the PA that was stopped. Includes the commitment tree and the nullifier set
  • locked resource - a consumable resource associated with the halted state
  • active PA - the non-halted PA that is processing transactions
  • active state - the state of the active PA

The goal of this post is to consider how to make the resources locked in the halted state consumable in the active state.

Assumptions:

  • There is one halted PA (we want to migrate from) and one active PA (we want to migrate to).
  • The active PA is responsible for verifying the locked resources constraints, not the forwarder contract. If we want a forwarder contract to be responsible for that, it changes who performs the global checks, but not the checks themselves. [1]

Let’s make some observations:

  1. The only things that can be done to resources is creation and consumption. The only thing we might want to do with locked resources is to consume them, meaning:
    1. verifying the constraints (in- and out-of-circuit) against the halted state
    2. updating the nullifier set
  2. The PA must be aware of the halted state and must “trust” it. Being aware of the halted state means knowing the roots of the halted state commitment tree, nullifier set of the halted state, and being able to distinguish the halted state from any other state. Otherwise a malicious user can fabricate a state to pass the circuit checks.
  3. We can’t process locked resources as “normal” resources since we cannot verify their logic proofs
  4. Migration requires extra global checks, since it must check constraints against both halted and active state

The proposed solution is a migration application.

The migration application

The migration application allows consuming locked resources in a non-native way. It includes compliance constraints for the locked resource and ensures the binding between the consumed locked resource and the created persistent resources.

The high-level idea is that the target application [2] allows migration by checking the presence of a migration application resource in the same action. The migration resource is a zero-quantity ephemeral resource that is only present to enforce additional constraints on its inputs. It checks the bindings of the locked resource and the created resources of the target application.

Migration circuit (MC) + token transfer circuit

  1. TTC must include an additional constraint that allows minting new resources without the “wrap” call type in the external input if a migration resource is present in the same action. The external input might be empty (in case the PA handles the global checks for the locked resources) or non-empty (in case the FC handles them). In the latter case the call type must be “migrate”
  2. MC binds the ephemeral consumed resource of TTC (like in the wrap case it is bound to the event in FC) and the locked resource

Constraints

TTC extra constraint

The change we need to add is that now we can also mint new resources without necessarily depositing assets to the forwarder [3] if there is a migration resource present in the same transaction.

MC constraints and inputs

Constraints:

  • there is a consumed ephemeral resource mintTrigger of TTC kind in the same action s.t.:
    • lockedResource.quantity = mintTrigger.quantity
    • lockedResource.label = old_forwarder_address + erc20_address
    • mintTrigger.label = new_forwarder_address + erc20_address
    • lockedResource.logicRef = oldLogicRef
    • mintTrigger.logicRef = newLogicRef
    • lockedResource.isEphemeral = false
  • lockedCM = lockedResource.commit()
  • lockedNF = lockedResource.nullify(LockedNullifierKey)
  • MerkleVerify(lockedPath, lockedRoot, lockedCM) = true
  • migrationTrigger.cm = migrationTrigger.commit()
  • migrationTrigger.is_ephemeral = True
  • migrationTrigger value/label checks in case we need to bind the migrationTrigger resource to the lockedResource specifically (the way we did for denominationTrigger in kudos)
  • Signature check: ECDSA.verify(pk = lockedResource.value.authorityPK, signature = Sig, message = actionTreeRoot) = True

Public inputs:

  • migrationTrigger.cm
  • old_forwarder_address
  • new_forwarder_address
  • oldLogicRef
  • newLogicRef
  • locked.cm
  • locked.nf
  • mintTrigger.nf
  • mintTrigger.cm
  • lockedRoot
  • actionTreeRoot

Private inputs:

  • migrationTrigger
  • lockedResource
  • mintTrigger
  • erc20_address
  • lockedPath
  • lockedNullifierKey
  • Sig

Global checks

For the locked resource, we must check that:

  1. The halted CMtree root is a valid historical root in the halted state
  2. Locked resource nullifier is not in the halted nullifier set
  3. Locked resource nullifier is not in any other existing halted nullifier sets
  4. Locked resource nullifier is not in the active nullifier set
  5. The same locked resource is not used multiple times within the same transaction

A diagram

Here is a mint diagram for MC + TTC:

Generalized migration (any target application)

The migration application checks are largely application independent, assuming that we are migrating a resource of an application on PA (so we need to update the forwarder address and logics of the created resource, but not the rest of the label, which allows us not to care what the rest of the label is as long as it is the same for the bounded resources) and that the actions are authorized by signature. The latter seems to be the only generality bottleneck, but we still didn’t manage to figure out how to abstract authorization, so it isn’t a migration-application-specific problem.

Conclusion

This approach allows us to explicitly handle migration and the circuit doesn’t depend on where the migration is happening from: the active PA must be aware of the state we are transferring from and verify nullifier non-existence in all existing halted states. Otherwise, the resource that is being transferred from the “original” halted state might be already consumed in another halted state.

The migration circuit design is general (modulo the authorization check) and can be extended to other applications too. We also don’t need to change the target application circuit for migration, except adding the universal migration branch where minting new resources is explicitly allowed when a migration resource is in the same action. Given that we are not planning to migrate often, it seems to be a fair trade-off.

I’m looking forward to your feedback.

@xuyang @Michael @cwgoes


  1. I think it is strictly better to add this logic to the PA vs FC, but I don’t have a strong opinion here. If you decide to put it on the forwarder contract, everything that is said about the PA checks below applies to FC. ↩︎

  2. For example, token transfer circuit ↩︎

  3. afaik now we transfer assets from the old forwarder to the new one when migration is initiated, but this is not a requirement coming from the Anoma side. Regardless of how we go about it, we just need to be able to differentiate between “migration mint” and “wrap mint” ↩︎

2 Likes

Your proposal introduces a dedicated, general migration circuit being applicable to resources that

  • Use EVM interoperability (i.e., a forwarder contract) and encode the forwarder address in a specific position of the label.
  • Use the current authorization mechanism requiring a signature over the action tree.

This requires the said resources (i.e., token transfer resources) to implement an alternative mint case as part of their v1 resource logic implementation.
This is different from the current v1 resource design, which doesn’t require consideration of the migration case and an alternative mint cae.

Your proposal makes sense to me, and factoring out code that can be reused for other resources/apps is generally the right strategy (even if it increases the number of resources that need to be processed in the migration case).

However, I find it too early to consider such generalizations because

  1. This is the first application we are running in production (from which we will probably learn a lot)

  2. There are unknowns we cannot account for now, such as v2 components (e.g., the protocol adapter, circuits, and ERC20 forwarder) requiring changes to the

    • The EVM interoperability design
    • The authorization mechanism

    because of a vulnerability or requirements of subsequent Anoma protocol versions (i.e., Anoma Dagon), which could lead to the migration design not being usable.

This largely motivated keeping the current v1 implementation minimal and making an intentionally application-specific v2 draft[1] demonstrating how locked assets can be unlocked for an application that didn’t consider migration in its v1.

Despite these considerations for AnomaPay v1, general migration is a critically important topic that we must tackle once we have gained more experience in maintaining our own application(s) and once the Anoma Dagon protocol roadmap and specs have become clearer.


  1. The wording of it being a draft is important since we cannot know future requirements and potential vulnerabilities now. ↩︎

Not sure what you mean by specific, but yes, a migration proposal is only applicable to applications that involve interoperability, as otherwise there is nowhere to migrate from.

That is correct. I consider it a worthy trade-off in the long-term because it only requires implementing it once, and every migration after that is “free”. It isn’t necessarily intended to be a short-term solution, but in the long-term we are winning a lot security-wise and time-wise from not having to update the circuit each time we migrate.

And so it is even more important to make sure it is safe and creates a good first impression on the users.

For sure. There are many things that can happen in the future, and if something doesn’t work out, we can always update the circuits. The proposed mechanism doesn’t prohibit the existing mechanism, so it is general from that perspective as well.

I want to highlight that this design minimizes the amount of code changes required for migration, but doesn’t exclude code upgrades if they have to be done.

Again, I’m not saying that it should necessarily be implemented asap, but adopting it sooner saves us some time and worries in the future without restricting us from implementing updates.

1 Like

Thanks for the writeup!

Generalizing the migration is a good idea. Your proposal moves migration constraints to a separate resource (migrationTrigger) and triggers the new logic via minting in the token transfer (mintTrigger). The only downside, as Michael noted, is the increased resource count—unavoidable but acceptable IMP.

You’ve clearly outlined constraints in the MC, which looks great. Could you also specify the extra constraints, private and public inputs in TTC? Beyond global state checks, we may need out-of-circuit checks on public inputs of migrationTrigger and mintTrigger. For example, if two different mintTriggers use the same migrationTrigger in one Action, two resources could be minted while only one is migrated and consumed—this still satisfies the “there is a migration resource” constraint in TTC. We can prevent this with extra out-of-circuit checks to ensure migrationTrigger and mintTrigger match and reference each other. Once these out-of-circuit checks are defined, we might remove some MC public inputs if they’re no longer needed for storage or checks.

2 Likes