Inspiration

There is a wide discussion on implementing Snark accounts on Celestia (a cosmos SDK chain) for interoperability of TIA between Celestia and rollups using Celestia for data availability. This made me research implementing Snark-based accounts, where proofs would be used as equivalent to signatures, proving key as the private key, and verifying key as the public key.

What it does

This project shows a way and makes the first implementation of Snark Accounts using Hypersdk for hyperchains.

How we built it

We have implemented a new auth type called SNACS in hypersdk. Hypersdk address is derived from verifying key. SNACS currently supports verifying the pre-image of the mimic hash circuit, but with some loosened assumptions, verification can be made independent of the circuit used to generate proofs.

This auth type is used by users for generating and verifying proofs.

type SNACS struct {
    VKey  groth16.VerifyingKey `json:"vkey,omitempty"`
    Proof groth16.Proof        `json:"proof"`
    addr  codec.Address
}

This circuit defined in SNACS module. This circuit checks if the mimic hash of PreImage equals Hash in BN254 curve.

type Circuit struct {
    PreImage frontend.Variable
    Hash     frontend.Variable `gnark:",public"`
}

func (circuit *Circuit) Define(api frontend.API) error {
    mimc, _ := mimc.NewMiMC(api)
    mimc.Write(circuit.PreImage)
    api.AssertIsEqual(circuit.Hash, mimc.Sum())
    return nil
}

While signing a transaction, the sha224 hash of the msg is mimic hashed. msg is the transaction digest (obtained with tx.Digest()) sent while signing. As this auth module uses the BN254 curve for proof generation, msg should be its field element, but msg may be larger than 254 bits, so to keep it under 254 bits, we sha224 hash msg.

func (s *SNACSFactory) Sign(msg []byte) (chain.Auth, error) {
    msgHash := sha256.Sum224(msg)
    hash := MimcHash(msgHash[:])
    assignment := &Circuit{
        PreImage: frontend.Variable(msgHash[:]),
        Hash:     frontend.Variable(hash),
    }

    witness, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField())
    if err != nil {
        return nil, fmt.Errorf("error creating new witness: %s", err)
    }

    proof, err := groth16.Prove(s.CS, s.PKey, witness)
    if err != nil {
        return nil, fmt.Errorf("error generating proof: %s", err)
    }

    return &SNACS{Proof: proof, VKey: s.VKey}, nil
}

The Verification function logic defines the circuit-dependent or circuit-specific verification but holds the security guarantee. i.e the proof is somehow related to the msg. Our implementation of the verification function is circuit-specific, but this can be made circuit-independent if we avoid deriving public witness from msg(this is an extra check to verify if the proof corresponds to the public witness derived from the msg).

func (s *SNACS) Verify(_ context.Context, msg []byte) error {
    msgHash := sha256.Sum224(msg)
    hash := MimcHash(msgHash[:])
    assignement := &Circuit{
        PreImage: frontend.Variable(msgHash[:]), // preImage can be anything, as PreImage is not public input. But filling the field is necessary for the witness to be created
        Hash:     frontend.Variable(hash),
    }
    witness, err := frontend.NewWitness(assignement, ecc.BN254.ScalarField())
    if err != nil {
        return err
    }
    pubWit, err := witness.Public()
    if err != nil {
        return err
    }
    return groth16.Verify(s.Proof, s.VKey, pubWit)
}

Demo

Launch Subnet

The first step to running this demo is to launch your own morpheusvm Subnet. You can do so by running the following command from this location (it may take a few minutes):

./scripts/run.sh;

When the Subnet is running, you'll see the following logs emitted:

cluster is ready!
avalanche-network-runner is running in the background...

use the following command to terminate:

./scripts/stop.sh;

By default, this allocates all funds on the network to morpheus1qrzvk4zlwj9zsacqgtufx7zvapd3quufqpxk5rsdd4633m4wz2fdjk97rwu. The private key for this address is 0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7. For convenience, this key is also stored at demo.pk.

Build morpheus-cli

To make interacting with the morpheusvm easy, we implemented the morpheus-cli. Next, you'll need to build this tool. You can use the following command:

./scripts/build.sh

This command will put the compiled CLI in ./build/morpheus-cli.

Configure morpheus-cli

Next, you'll need to add the chains you created and the default key to the morpheus-cli. You can use the following commands from this location to do so:

./build/morpheus-cli key import ed25519 demo.pk

Next, you'll need to store the URLs of the nodes running on your Subnet:

./build/morpheus-cli chain import-anr

./build/morpheus-cli chain import-anr connects to the Avalanche Network Runner server running in the background and pulls the URIs of all nodes tracking each chain you created.

Setup SNAC factory:

for using an SNAC account, we need the factory to be initialised, The factory contains the proving key, verifying key and circuit constraints.

  ./build/morpheus-cli key generate-snacs

copy the snac address.

Send Tokens to the SNAC account

Lastly, we trigger the transfer:

./build/morpheus-cli action transfer

Send funds to the Snac Account Address copied previously.

Make transactions from the SNAC account:

transfer funds to morpheus1qqds2l0ryq5hc2ddps04384zz6rfeuvn3kyvn77hp4n5sv3ahuh6wgkt57y, using snac account

  ./build/morpheus-cli action transfer-snac

Challenges we ran into

It could have been better if there existed access to the state while verifying; this would have simplified circuit-independent verification. But, with the knowledge of recent advancements on hypersdk, removing state access makes sense.

Built With

  • avalanche
  • gnark
  • groth16
  • hypersdk
  • snarks
Share this project:

Updates