-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ch2): spy master contract and test
- Loading branch information
1 parent
de2ff4c
commit 4bee071
Showing
5 changed files
with
328 additions
and
5 deletions.
There are no files selected for viewing
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
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 |
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
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
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)); | ||
}) | ||
}) |
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
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); | ||
} | ||
} |
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
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 }; |