-
-
Save liamzebedee/8d2efbb105d3b474dbe752d526cc9d27 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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