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:
- 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:
- verifying the constraints (in- and out-of-circuit) against the halted state
- updating the nullifier set
- 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.
- We can’t process locked resources as “normal” resources since we cannot verify their logic proofs
- 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
- 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”
- 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
mintTriggerof TTC kind in the same action s.t.:lockedResource.quantity = mintTrigger.quantitylockedResource.label = old_forwarder_address + erc20_addressmintTrigger.label = new_forwarder_address + erc20_addresslockedResource.logicRef = oldLogicRefmintTrigger.logicRef = newLogicReflockedResource.isEphemeral = false
lockedCM = lockedResource.commit()lockedNF = lockedResource.nullify(LockedNullifierKey)MerkleVerify(lockedPath, lockedRoot, lockedCM) = truemigrationTrigger.cm = migrationTrigger.commit()migrationTrigger.is_ephemeral = TruemigrationTriggervalue/label checks in case we need to bind themigrationTriggerresource to thelockedResourcespecifically (the way we did fordenominationTriggerin kudos)- Signature check:
ECDSA.verify(pk = lockedResource.value.authorityPK, signature = Sig, message = actionTreeRoot) = True
Public inputs:
migrationTrigger.cmold_forwarder_addressnew_forwarder_addressoldLogicRefnewLogicReflocked.cmlocked.nfmintTrigger.nfmintTrigger.cmlockedRootactionTreeRoot
Private inputs:
migrationTriggerlockedResourcemintTriggererc20_addresslockedPathlockedNullifierKeySig
Global checks
For the locked resource, we must check that:
- The halted CMtree root is a valid historical root in the halted state
- Locked resource nullifier is not in the halted nullifier set
- Locked resource nullifier is not in any other existing halted nullifier sets
- Locked resource nullifier is not in the active nullifier set
- 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.
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. ↩︎
For example, token transfer circuit ↩︎
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” ↩︎
