Skip to content

Instantly share code, notes, and snippets.

@liamzebedee
Created July 27, 2022 23:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save liamzebedee/8d2efbb105d3b474dbe752d526cc9d27 to your computer and use it in GitHub Desktop.
Save liamzebedee/8d2efbb105d3b474dbe752d526cc9d27 to your computer and use it in GitHub Desktop.
import {
SelfProof,
Field,
ZkProgram,
verify,
Poseidon,
SmartContract,
Scalar,
arrayProp,
Circuit,
Bool,
} from 'snarkyjs';
import { CircuitValue, prop, Signature, PrivateKey, PublicKey } from 'snarkyjs';
//
// Helpers.
//
class Wallet {
constructor(public pubKey: PublicKey, public privKey: PrivateKey) { }
static createFromPrivKey(privateKeyBase58: string) {
const privKey = PrivateKey.fromBase58(privateKeyBase58)
const pubKey = PublicKey.fromPrivateKey(privKey)
return new Wallet(pubKey, privKey)
}
static createRandom() {
let privKey = PrivateKey.random();
let pubKey = PublicKey.fromPrivateKey(privKey);
return new Wallet(pubKey, privKey);
}
}
//
// Circuit data types.
//
const NUM_OUTPUTS = 10
class Coin extends CircuitValue {
@prop amount: Field;
@prop owner: PublicKey;
}
class RawTransaction extends CircuitValue {
@prop inputIdx: Field;
@arrayProp(Coin, NUM_OUTPUTS) outputs: [Coin, Coin, Coin, Coin, Coin];
sighash() {
return Poseidon.hash(this.toFields());
}
}
class Transaction extends CircuitValue {
@prop inputIdx: Field;
@prop sig: Signature;
@arrayProp(Coin, NUM_OUTPUTS) outputs: [Coin, Coin, Coin, Coin, Coin];
sighash(): Field {
return RawTransaction.fromObject(this).sighash();
}
sign(privKey: PrivateKey) {
let msg = [this.sighash()];
let sig = Signature.create(privKey, msg);
this.sig = sig;
}
}
//
// The ZK program.
//
// Minicoin is a proof-of-concept for the Quark blockchain architecture.
//
// what's the general idea? what if we could make free crypto txs? literally no fees.
// everyone just had to sequence and contribute to the network.
// that'd be fucking sick.
// okay so how?
// (1) anyone can join the sequencer and contribute to sequencing randomness.
// (2) all blockchain state is hosted via torrents.
// (3) there's no such thing as a full node. If you wanna transact, just logon:
// what will happen?
// - you'll download your wallet's balance from some indexer
// - you are responsible for your own data availability. literally - your keys, your coins.
// - to send money to a user, it takes like 10s. 8s to generate the proof, 2s to sequence your tx.
// - after that, it's finalised! no waiting for confirmations.
//
// how does any of this work?
// - minicoin is like bitcoin, in that there are coins (fungible), transferred in transactions,
// and accounted for as UXTO objects (unspent transaction outputs).
// - however, the blockchain architecture is entirely different.
// - nodes agree on the sequence of transactions via byzantine atomic broadcast
// - and then aside from that, not much else.
// - the state of the chain is hosted through torrents. these provide lightweight data availability
// consortiums. keeping your coins is as important as keeping proofs of your txs.
// - indexer nodes keep track of the UXTO's, and periodically generate proofs-of-balance which allow for
// quick catchup on state without re-verifying all proofs.
//
// Some quick napkin calculations:
// - average proof size - 3kB
// - average # UXTO's per user - 1000
// - size of wallet: 3MB
// What about proofing time for a joinsplit?
// Sequencer requirements?
// - random beacon (use ETH 2.0)
// - stake (anti-Sybil)
// - 10K TPS
// 21 million minicoins.
const GENESIS_SUPPLY = 21_000_000
const GENESIS_ACCOUNT = PublicKey.fromBase58('B62qmhzQ1gptdzQeXa3nhfNYE1hJts1uzEhbURK7qmVjYHutiw9Krvc')
const Minicoin = ZkProgram({
publicInput: Transaction,
methods: {
// Genesis.
genesis: {
privateInputs: [],
method(tx: Transaction) {
// Genesis involves minting a single UXTO with 21M coins to the genesis account.
const genesisUxto = tx.outputs[0]
genesisUxto.amount.assertEquals(new Field(GENESIS_SUPPLY));
genesisUxto.owner.assertEquals(GENESIS_ACCOUNT)
},
},
transfer: {
privateInputs: [SelfProof],
method(t1: Transaction, t0: SelfProof<Transaction>) {
// Verify the state at t=0 was computed correctly.
t0.verify();
// State: utxo signature validation.
const t1_sighash = t1.sighash();
const msg = [t1_sighash];
// Verify all inputs of the tx, calculate sum inputs.
let sumInputs = Field.zero
for (let i = 0; i < NUM_OUTPUTS; i++) {
const coin = t0.publicInput.outputs[i]
// isSpent = (t1.inputIdx == i)
const isSpent = t1.inputIdx.equals(new Field(i))
// if(isSpent) {
// sumInputs += coin.amount
// }
sumInputs = Circuit.if(
isSpent,
sumInputs.add(coin.amount),
sumInputs
)
// We don't actually need to verify the sequence inside the proof.
// Proofs can exist without needing to prove they were ordered.
// Nodes can agree out-of-band as to the correct ordering.
// These proofs just prove the state transition.
// const sequenceValid = false
// const sigValid = t1.sig.verify(coin.owner, msg)
const sigValid = Circuit.if(
isSpent,
t1.sig.verify(coin.owner, msg),
Bool(false)
)
// Tx validity:
// (cond (isSpent) (sequenceValid sigValid)
// (true))
const valid = Bool.or(
Bool.and(isSpent, sigValid),
Bool.not(isSpent)
)
valid.assertTrue()
}
// Verify all outputs of the tx, calculate sum(input) = sum(output)
let sumOutputs = Field.zero
for (let i = 0; i < NUM_OUTPUTS; i++) {
const coin = t1.outputs[i]
// sum(inputs) += coin.amount
sumOutputs = sumInputs.add(coin.amount)
// assert: 0 <= coin.amount <= sum(inputs)
coin.amount.assertGte(0)
coin.amount.assertLte(sumInputs)
}
// assert: sum(outputs) = sum(inputs)
sumInputs.assertEquals(sumOutputs)
},
},
},
});
//
// Run.
//
// Accounts.
//
const genesisAccount = Wallet.createFromPrivKey('EKFbsoaY43xRRsmpNPnNSTQUNS7uNr7UkDVneZYzaYQ5XvD5iEgf')
// console.log(`Genesis account private key: ${genesisAccount.privKey.toBase58()}`)
// console.log(` public key: ${genesisAccount.pubKey.toBase58()}`)
const account1 = genesisAccount
const account2 = Wallet.createRandom();
// Null types.
//
const SIGNATURE_NULL = Signature.create(account1.privKey, [new Field(1)]);
const COIN_NULL = Coin.fromObject({
amount: new Field(0),
owner: account1.pubKey,
})
console.log('compiling Minicoin...');
const { verificationKey } = await Minicoin.compile();
// Genesis.
//
let genesisTx = Transaction.fromObject({
inputIdx: Field.zero,
sig: SIGNATURE_NULL,
outputs: [
Coin.fromObject({
amount: new Field(GENESIS_SUPPLY),
owner: GENESIS_ACCOUNT,
}),
COIN_NULL,
COIN_NULL,
COIN_NULL,
COIN_NULL,
],
});
console.log('Genesis...');
let genesis = await Minicoin.genesis(genesisTx);
console.log(`genesis proof: ${JSON.stringify(genesis.toJSON())}`)
// Test a transfer from the genesis UXTO.
//
console.log('transfer #1...');
let inputIdx = Field.zero;
let outputs = [
{
amount: new Field(500),
owner: account1.pubKey,
},
{
amount: new Field(500),
owner: account2.pubKey,
},
].map(x => Coin.fromObject(x)).concat([
COIN_NULL,
COIN_NULL,
COIN_NULL,
]);
let tx = new Transaction(inputIdx, SIGNATURE_NULL, outputs);
tx.sign(account1.privKey);
let proof = await Minicoin.transfer(tx, genesis);
proof.verify();
process.exit(0);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment