When we ran npm run setup
a file named zkBattleship.json
was generated and copied to src/contracts/
. This file is called the contract artifact and it contains a complete description of our compiled battleship contract.
We can simply import it in our React app:
import artifact from './contracts/zkBattleship.json'
We instantiate and deploy the contract every time the game starts in the startTurn
callback function.
Because we already have our contracts artifact, we don't have to compile the contract again. We just simply load the artifact via the contract classes loadArtifact()
method and instantiate the contract:
BattleShip.loadArtifact(artifact) // ... const instance = new BattleShip( PubKey(pubKeyPlayer), PubKey(pubKeyComputer), BigInt(playerHash), BigInt(computerHash), falseArr, falseArr, vk);
Before deploying the contract instance, we need to connect it to a signer. In our app we use the SensiletSigner
, which is already built into scrypt-ts
itself.
const provider = new DefaultProvider() const signer = new SensiletSigner(provider) await signer.getConnectedTarget() as any // ... instance.connect(signer)
After we connected the signer to our contracts instance, we can deploy the contract:
const rawTx = await instance.deploy(amountSats);
After a successful deployment, we save the UTXO of the deployed contract to local storage so that the transaction can be constructed when the contract is invoked:
ContractUtxos.add(rawTx, 0, -1); const txid = ContractUtxos.getdeploy().utxo.txId setDeployTxid(txid)
As described in the previous chapter, whenever a player fires, we generate a zkSNARK proof, which proves whether the coordinate that was fired at was a hit or not.
First, we add different outputs to the transaction depending on the game state. If a player has already hit 17
times, the game is over. The contract sends the total balance to the winner by adding an output containing the winner's address to the transaction. Then the contract terminates. Otherwise, we continue the contracts execution by just adding an output with our contracts code that contains updated state properties.
if (nextInstance.successfulPlayerHits == 17n) { unsignedTx.addOutput(new bsv.Transaction.Output({ script: bsv.Script.buildPublicKeyHashOut(pubKeyPlayer), satoshis: initBalance })) .change(changeAddress) return Promise.resolve({ tx: unsignedTx, atInputIndex: 0, nexts: [ ] }) } else if (newStates.successfulComputerHits == 17n) { unsignedTx.addOutput(new bsv.Transaction.Output({ script: bsv.Script.buildPublicKeyHashOut(pubKeyComputer), satoshis: initBalance })) .change(changeAddress) return Promise.resolve({ tx: unsignedTx, atInputIndex: 0, nexts: [ ] }) } else { unsignedTx.addOutput(new bsv.Transaction.Output({ script: nextInstance.lockingScript, satoshis: initBalance, })) .change(changeAddress) return Promise.resolve({ tx: unsignedTx, atInputIndex: 0, nexts: [ { instance: nextInstance, atOutputIndex: 0, balance: initBalance } ] }) }
Next, we call the contract's move
public function. The arguements of the move
function include the player's signature, and the position of the firing, the result of the hit reported by the opponent, and the zkSNARK proof provided by the opponent.
const { tx: callTx } = await currentInstance.methods.move( (sigResponses: SignatureResponse[]) => { return findSig(sigResponses, pubKey) }, position.x, position.y, hit, proof, initBalance, { pubKeyOrAddrToSign: pubKey, } as MethodCallOptions<BattleShip> )
Note that the generated zkSNARK proof needs to be converted into the Proof
type defined in our verifier contracts code.
import { Proof } from './contracts/verifier' // ... const proof: Proof = { a: { x: BigInt(proof.proof.a[0]), y: BigInt(proof.proof.a[1]), }, b: { x: { x: BigInt(proof.proof.b[0][0]), y: BigInt(proof.proof.b[0][1]), }, y: { x: BigInt(proof.proof.b[1][0]), y: BigInt(proof.proof.b[1][1]), } }, c: { x: BigInt(proof.proof.c[0]), y: BigInt(proof.proof.c[1]), }, }
WelcomeScreen.ts
.startTurn
function.move
method.