Skip to content

Commit

Permalink
feat(ch2): spy master contract and test
Browse files Browse the repository at this point in the history
  • Loading branch information
kidneyweakx committed Feb 25, 2024
1 parent de2ff4c commit 4bee071
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 5 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/ch2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#
# Run tests for all pushed commits and opened pull requests on Github.
#

name: CH1 Test
on:
push:
paths:
- 'src/CH2SpyMessage.ts'
- 'src/CH2SpyMessage.test.ts'
pull_request:
paths:
- 'src/CH2SpyMessage.ts'
- 'src/CH2SpyMessage.test.ts'
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Set up NodeJS
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Git checkout
uses: actions/checkout@v4
- name: NPM ci, build, & test
run: |
npm install
npm run build --if-present
npm test -t CH2
env:
CI: true
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ch1",
"version": "0.1.0",
"name": "mina-challenge",
"version": "0.2.0",
"description": "",
"author": "",
"license": "Apache-2.0",
Expand Down Expand Up @@ -38,4 +38,4 @@
"peerDependencies": {
"o1js": "0.15.*"
}
}
}
189 changes: 189 additions & 0 deletions src/CH2SpyMessage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { SpyMaster, SpyMasterContract, Message } from './CH2SpyMessage.js'
import { Field, Mina, PrivateKey, PublicKey, AccountUpdate } from 'o1js';

let proofsEnabled = false;
function randomNumner (min:number, max:number) {
return Math.floor(Math.random() * (max - min) + min)
}



describe('CH2.SpyMasterContract', () => {
let deployerAccount: PublicKey,
deployerKey: PrivateKey,
senderAccount: PublicKey,
senderKey: PrivateKey,
zkAppAddress: PublicKey,
zkAppPrivateKey: PrivateKey,
zkApp: SpyMasterContract;

beforeAll(async () => {
if (proofsEnabled) await SpyMasterContract.compile();
});

beforeEach(() => {
const Local = Mina.LocalBlockchain({ proofsEnabled });
Mina.setActiveInstance(Local);
({ privateKey: deployerKey, publicKey: deployerAccount } =
Local.testAccounts[0]);
({ privateKey: senderKey, publicKey: senderAccount } =
Local.testAccounts[1]);
zkAppPrivateKey = PrivateKey.random();
zkAppAddress = zkAppPrivateKey.toPublicKey();
zkApp = new SpyMasterContract(zkAppAddress)
});

async function localDeploy() {
const txn = await Mina.transaction(deployerAccount, () => {
AccountUpdate.fundNewAccount(deployerAccount);
zkApp.deploy();
});
await txn.prove();
// this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
await txn.sign([deployerKey, zkAppPrivateKey]).send();
}

function generateValidMessage(amount: number, start = 0): {messageNumber: Field, message: Message} [] {
let messages: {messageNumber: Field, message: Message} [] = []
for (let i = 0; i < amount; i++) {
const agentId = randomNumner(1, 3000)
const xLocation = randomNumner(1, 15000)
const yLocation = randomNumner(5000, 20000)
const checksum = agentId + xLocation + yLocation

messages.push({
messageNumber: Field(i + start),
message: new Message({
agentId: Field(agentId),
xLocation: Field(xLocation),
yLocation: Field(yLocation),
checksum: Field(checksum)
})
})
}
return messages;
}

describe('spy master', () => {
it('process valid messages', async () => {
await localDeploy();
await SpyMaster.compile()
let proof = await SpyMaster.init(Field(0));

const messages = generateValidMessage(3)

for (const msg of messages) {
proof = await SpyMaster.processMessage(msg.messageNumber, proof, msg.message);
}

let txn = await Mina.transaction(deployerAccount, () => {
zkApp.processBatch(proof);
});
await txn.prove();
await txn.sign([deployerKey]).send();

expect(zkApp.highestMessageNumber.get()).toEqual(Field(2)); // 0 1 2
});
})


it('process messages with agentId = 0', async () => {
await localDeploy();
await SpyMaster.compile()
let proof = await SpyMaster.init(Field(0));

const messages = [{
messageNumber: Field(1),
message: new Message({
agentId: Field(0),
// invalid xLocation, yLocation, checksum
xLocation: Field(15001),
yLocation: Field(0),
checksum: Field(0)
})
}]

for (const msg of messages) {
proof = await SpyMaster.processMessage(msg.messageNumber, proof, msg.message);
}

let txn = await Mina.transaction(deployerAccount, () => {
zkApp.processBatch(proof);
});
await txn.prove();
await txn.sign([deployerKey]).send();

expect(zkApp.highestMessageNumber.get()).toEqual(Field(1));
});

it('process messages with consider is duplicate', async () => {
await localDeploy();
await SpyMaster.compile()
let proof = await SpyMaster.init(Field(0));

const messages = generateValidMessage(2, 5)

for (const msg of messages) {
proof = await SpyMaster.processMessage(msg.messageNumber, proof, msg.message);
}

let txn = await Mina.transaction(deployerAccount, () => {
zkApp.processBatch(proof);
});
await txn.prove();
await txn.sign([deployerKey]).send();

const newMessage = [{
messageNumber: Field(1),
message: new Message({
agentId: Field(1),
xLocation: Field(100),
yLocation: Field(5001),
checksum: Field(5102)
})
}]

for (const msg of newMessage) {
proof = await SpyMaster.processMessage(msg.messageNumber, proof, msg.message);
}

expect(zkApp.highestMessageNumber.get()).toEqual(Field(6)); // 5 6
});

it('process messages: message details are incorrect, discard the message and proceed to the next.', async () => {
await localDeploy();
await SpyMaster.compile()
let proof = await SpyMaster.init(Field(0));
// 1 invalid message, 2 valid message
const messages = [{
messageNumber: Field(0),
// invalid xLocation, yLocation, checksum
message: new Message({
agentId: Field(1),
xLocation: Field(100),
yLocation: Field(5001),
checksum: Field(5102)
})
}, {
messageNumber: Field(1),
message: new Message({
agentId: Field(2),
xLocation: Field(100),
yLocation: Field(5001),
checksum: Field(5102)
})
}]

for (const msg of messages) {
proof = await SpyMaster.processMessage(msg.messageNumber, proof, msg.message);
}

let txn = await Mina.transaction(deployerAccount, () => {
zkApp.processBatch(proof);
});
await txn.prove();
await txn.sign([deployerKey]).send();

expect(zkApp.highestMessageNumber.get()).toEqual(Field(1));
})
})
102 changes: 102 additions & 0 deletions src/CH2SpyMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// **Scenario:**
// The Mina spy headquarters receives messages from agents, typically in batches of 50 to 200 messages. Each message is identified by a unique message number.

// **Requirements:**
// 1. The contract should process batches of transactions and store the highest processed message number.
// 2. Each message contains:
// - Message number
// - Message details (private input)
// - Agent ID (between 0 and 3000)
// - Agent X coordinate (between 0 and 15000)
// - Agent Y coordinate (between 5000 and 20000)
// - Checksum
// 3. Checkpoints:
// - If Agent ID is zero, no need to check other values, but still a valid message.
// - If message details are incorrect, discard the message and proceed to the next.
// - If the message number is not greater than the previous one, consider it a duplicate (process it without detailed checks).
// 4. Run on low-spec hardware, ensuring the circuit size remains low.
// 5. Store the highest processed message number permanently.

import { Field, Struct,Provable, SelfProof, ZkProgram, SmartContract, State, state, method } from "o1js"

// Define the message details structure
export class Message extends Struct({
agentId: Field,
xLocation: Field,
yLocation: Field,
checksum: Field,
}) {
agentIdCheck() {
return this.agentId.equals(0)
}
check() {
return this.agentId.greaterThanOrEqual(0).and(this.agentId.lessThanOrEqual(3000))
.and(this.xLocation.greaterThanOrEqual(0).and(this.xLocation.lessThanOrEqual(15000))
.and(this.yLocation.greaterThanOrEqual(5000).and(this.yLocation.lessThanOrEqual(20000))
.and(this.agentId.add(this.xLocation).add(this.yLocation).equals(this.checksum))))
}
}

// ===============================================================
export const SpyMaster = ZkProgram({
name: "spy-master",
publicInput: Field,
publicOutput: Field,

methods: {
init: {
privateInputs:[],
method (publicInput: Field) {
publicInput.assertEquals(0)
return publicInput
}
},
processMessage: {
privateInputs: [SelfProof, Message],

method(messageNumber: Field, earlierProof: SelfProof<Field, Field>, message: Message) {
earlierProof.verify()
// If Agent ID is zero, no need to check other values, but still a valid message.
const checkpoints1 = message.agentIdCheck();
// If the message number is not greater than the previous one, consider it a duplicate
const checkpoints2 = earlierProof.publicInput.greaterThan(messageNumber);


const noNeedCheck = checkpoints1.or(checkpoints2);
const isValid = noNeedCheck.or(message.check());

// messageNumber.set(msgNumber)
return Provable.if(
isValid,
Field,
messageNumber,
earlierProof.publicOutput
)
}
}
}
})

// ===============================================================
const { verificationKey } = await SpyMaster.compile();
class SpyMasterProof extends ZkProgram.Proof(SpyMaster) {}

export class SpyMasterContract extends SmartContract {
@state(Field) highestMessageNumber = State<Field>();

@method processBatch(proof: SpyMasterProof) {
proof.verify();

const messageNumber = this.highestMessageNumber.getAndRequireEquals();
const calculatedMessageNumber = proof.publicOutput;
const statement = calculatedMessageNumber.greaterThan(messageNumber);

const newMessageNumber = Provable.if(
statement,
Field,
calculatedMessageNumber,
messageNumber
);
this.highestMessageNumber.set(newMessageNumber);
}
}
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { MessageContract } from './CH1Message.js';

export { MessageContract };
import { SpyMasterContract } from './CH2SpyMessage.js';
export { MessageContract, SpyMasterContract };

0 comments on commit 4bee071

Please sign in to comment.