From 8db25ab018fc1898a8a817ccb7a7f1f40b4b290c Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Fri, 23 Feb 2018 14:31:14 -0800 Subject: [PATCH] micro:coin activity (#650) * microcoincode * adding links * fix file name * a bit more docs * Some minor edits * Add a crypto blurb * missing radio package * updated info * link to wallet activity --- docs/SUMMARY.md | 1 + docs/projects.md | 7 +- docs/projects/micro-coin.md | 378 +++++++++++++++++++++++++ docs/projects/wallet.md | 5 + docs/static/mb/projects/micro-coin.png | Bin 0 -> 9340 bytes 5 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 docs/projects/micro-coin.md create mode 100644 docs/static/mb/projects/micro-coin.png diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 30116e7e..a980ac08 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -27,6 +27,7 @@ * [Infection](/projects/infection) * [Fireflies](/projects/fireflies) * [Rock Paper Scissors Teams](/projects/rps-teams) + * [micro:coin](/projects/micro-coin) * [Inchworm](/projects/inchworm) * [Milk Carton Robot](/projects/milk-carton-robot) * [Milk monster](/projects/milky-monster) diff --git a/docs/projects.md b/docs/projects.md index 8bd4cdcd..5ba8109b 100644 --- a/docs/projects.md +++ b/docs/projects.md @@ -60,6 +60,10 @@ Fun games to build with your @boardname@. "name": "Rock Paper Scissors Teams", "url": "/projects/rps-teams", "imageUrl": "/static/mb/projects/rpsteams.png" +}, { + "name": "Micro:Coin", + "url": "/projects/micro-coin", + "imageUrl": "/static/mb/projects/micro-coin.png" }] ``` @@ -171,4 +175,5 @@ Fun games to build with your @boardname@. [Milk Monster](/projects/milky-monster), [Karel the LED](/projects/karel), [Infection](/projects/infection), [Voting Machine](/projects/voting-machine) [Fireflies](/projects/fireflies), [Soil Moisture](/projects/soil-moisture), [States Of Matter](/projects/states-of-matter), [Reaction Time](/projects/reaction-time), -[Rock Paper Scissors Teams](/projects/rps-teams) +[Rock Paper Scissors Teams](/projects/rps-teams), +[micro:coin](/projects/micro-coin) diff --git a/docs/projects/micro-coin.md b/docs/projects/micro-coin.md new file mode 100644 index 00000000..3235c0ef --- /dev/null +++ b/docs/projects/micro-coin.md @@ -0,0 +1,378 @@ +# micro:coin + +## ~ avatar + +Have you heard about BitCoin and all those new Crypto currencies? Well micro:bit has **micro:coin** now! + +## ~ + +## How does it work? + +Each @boardname@ contains a **coin**, which is made of a **block chain**. To mine new blocks in the coin, the user shakes +the @boardname@ and hopes that they will be in luck! Once the block is added, it is broadcasted to the other @microbit@. +The block is public and can't be modified so it's ok to share it. Other @boardname@ the block, validate the transaction and update their block chain as needed. + +Pressing ``A`` shows the number of block you added to the chain, that's your score. +Pressing ``B`` shows you the length of the chain. + +Happy mining! + +## Coins, blocks, chains + +In the world of crypto currency, a _coin_ is a list of _blocks_ that record transfers (transactions) of the coin. A block might contain information like the time it was created (mined) and who mined it. The most important part of the block is it's _hash_. This is a special number made from the information in the last block of the block list combined with the hash number of previous block in the list. The new block contains information for the current transaction and this new hash number. The new block is added to the list of previous blocks. This list is then transmitted to the crypto currency network. + +The block list sent to the network is called a _blockchain_. Other currency miners see it and try to calculate again the same hash number found in the last block of the chain. By doing this, they are verifying that the block is correct and the transaction was valid. Crypto currency systems reward miners for doing this by adding some currency to their accounts. + +## ~ hint + +Build yourself a [@boardname@ wallet](/projects/wallet) to hold your coins! + +## ~ + +## Full source code + +**JavaScript only!** This program uses features that won't work in blocks... + +```typescript +/* +* micro:coin, A minimalistic blockchain for micro:bit +* +* DISCLAIMER: this is just an example. +* +*/ + +/** + * Message types sent over radio. + */ +enum Message { + // ask peer to get the full chain + QueryChain = 1, + // a block flying around + Block = 2 +} + + +/** + * A block is an immutable (can't change it) piece of a block chain. + * The block chain is like a list where each block is built from + * the previous block. + */ +class Block { + // index in the chain + index: number; + // timestamp on the device when the block was created + timestamp: number; + // in this implementation, data is the device serial number + data: number; + // hash of the previous block as a single unsigned byte + previousHash: number; // uint8 + // hash of the current block as a single unsigned byte + hash: number; // uint8 + + /** + * Construct the block and computes the hash + */ + constructor( + index: number, + timestamp: number, + data: number, + previousHash: number) { + this.index = index; + this.timestamp = timestamp; + this.data = data; + this.previousHash = previousHash; + this.hash = this.computeHash(); + } + + /** + * Compute the hash of the current block + */ + computeHash() { + let s = "" + this.index + this.timestamp + this.data + this.previousHash; + /** + * This function takes a string and hashes it into a number. It simply takes the sum of characters, + * it's not great but will work for a super-simple example. + */ + let sum = 0; + for (let i = 0; i < s.length; i++) + sum += s.charCodeAt(i); + return sum % 0xff; + } + + /** + * Create the next block with the given data + */ + next(data: number) { + return new Block(this.index + 1, input.runningTime(), data, this.hash); + } + + /** + * Render the block as a string + */ + toString() { + return `block ${this.index} ${this.timestamp} ${this.data} ${this.hash}`; + } + + /** + * Send the block over radio + */ + broadcast() { + serial.writeLine(`broadcast ${this}`); + /** + * We pack all the block data into a buffer and send it over radio + */ + const buf = pins.createBuffer(16); + buf.setNumber(NumberFormat.UInt8LE, 0, Message.Block); + buf.setNumber(NumberFormat.UInt8LE, 1, this.hash); + buf.setNumber(NumberFormat.UInt8LE, 2, this.previousHash); + buf.setNumber(NumberFormat.Int32LE, 4, this.index); + buf.setNumber(NumberFormat.Int32LE, 4 + 4, this.timestamp); + buf.setNumber(NumberFormat.Int32LE, 4 + 8, this.data); + radio.sendBuffer(buf) + } + + /** + * Try to read the block from the buffer. If anything is wrong, return undefined. + */ + static receive(buf: Buffer): Block { + // check the message type + if (buf.getNumber(NumberFormat.UInt8LE, 0) != Message.Block) + return undefined; + // read all the parts of the block back from the buffer + const b = new Block( + buf.getNumber(NumberFormat.Int32LE, 4), // index + buf.getNumber(NumberFormat.Int32LE, 4 + 4), // timestamp + buf.getNumber(NumberFormat.Int32LE, 4 + 8), // data + buf.getNumber(NumberFormat.UInt8LE, 2) // previoushash + ); + const h = buf.getNumber(NumberFormat.UInt8LE, 1); // hash + if (b.hash != h) { + serial.writeLine(`received invalid block ${b.hash} != ${h}`); + return undefined; + } + serial.writeLine(`received ${b}`); + return b; + } +} + +/** + * A coin is made of chains + */ +class Coin { + id: number; // device serial number + chain: Block[]; + + /** + * Constructs a new coin with the given id + */ + constructor(id: number) { + this.id = id; + this.chain = []; + // if ID is set, this coin is a mirror of a peer coin + // otherwise add genesis block + if (!this.id) { + this.chain.push(new Block(0, input.runningTime(), 0, 0)); + this.id = control.deviceSerialNumber(); + } + } + + /** + * Grab the last block in the chain + */ + lastBlock() { + return this.chain[this.chain.length - 1]; + } + + /** + * Add a new block in the chain + */ + addBlock() { + this.chain.push(this.lastBlock().next(this.id)); + this.lastBlock().broadcast(); + } + + /** + * Test if we have all the blocks in the chain available + */ + isComplete() { + for (let i = 0; i < this.chain.length; ++i) + if (!this.chain[i]) return false; // missing block + return this.lastBlock().index == this.chain.length - 1; + } + + /** + * Test if the block chain is valid + */ + isValid() { + if (!this.isComplete()) { + serial.writeLine("coin not complete"); + return false; + } + for (let i = 0; i < this.chain.length - 1; ++i) { + const prev = this.chain[i]; + const next = this.chain[i + 1]; + if (prev.index + 1 != next.index) { + serial.writeLine("invalid index"); + return false; + } + if (prev.hash != next.previousHash) { + serial.writeLine("invalid prev hash"); + } + if (next.computeHash() != next.hash) { + serial.writeLine("invalid hash"); + return false; + } + } + return true; + } + + /** + * Insert a block received over the radio + */ + insert(block: Block) { + this.chain[block.index] = block; + } + + /** + * We've received a coin and we are trying to replace the chain if it's been updated. + */ + replace(other: Coin) { + if (other.isValid() && other.chain.length > me.chain.length) { + serial.writeLine("replacing chain"); + this.chain = other.chain.slice(0, other.chain.length); + this.lastBlock().broadcast() + basic.showIcon(IconNames.SmallSquare) + } + } + + /** + * Broadcast the chains + */ + broadcastChain() { + for (let i = 0; i < this.chain.length; ++i) { + this.chain[i].broadcast(); + } + } +} + +/** + * Request all peers (or a single on) for the entire chain + */ +function broadcastQueryChain(serialNumber: number = 0) { + const msg = pins.createBuffer(6); + msg.setNumber(NumberFormat.UInt8LE, 0, Message.QueryChain); + msg.setNumber(NumberFormat.Int32LE, 2, serialNumber); + radio.sendBuffer(msg); +} + +const me = new Coin(0); +const peers: Coin[] = []; + +/** + * Get or create a coin to store the chain of a peer + */ +function peer(id: number): Coin { + for (let i = 0; i < peers.length; ++i) { + if (peers[i].id == id) return peers[i]; + } + const r = new Coin(id); + peers.push(r); + return r; +} + +/** + * Settings for the radio receiver + */ +radio.setGroup(42); +radio.setTransmitSerialNumber(true); +radio.onDataPacketReceived(({ receivedBuffer, serial: serialNumber }) => { + // processing a message received by ppers + let id: number; + switch (receivedBuffer[0]) { + case Message.QueryChain: + // so a peer asking to broadcast the chain + serial.writeLine("msg: query chain"); + id = receivedBuffer.getNumber(NumberFormat.Int32LE, 2); + // either all peers should send or just me + if (!id || id == me.id) { + me.broadcastChain(); + me.broadcastChain(); // send it twice as we might loose patterns + } + break; + case Message.Block: + // so we've received a block from a peer + serial.writeLine("msg: block"); + const other = peer(serialNumber); + const block = Block.receive(receivedBuffer); + if (!block) return; // something got corrupted + other.insert(block); + serial.writeLine(`check ${other.lastBlock().index} > ${me.lastBlock().index}`) + // if the other chain is longer, we should update ours maybe + if (other.lastBlock().index > me.lastBlock().index) { + if (!other.isComplete()) { + // we don't have the entire chain + serial.writeLine(`peer incomplete`) + broadcastQueryChain(serialNumber); + } else { + // we have a full chain, try replacing it + serial.writeLine(`peer complete, try replace`) + me.replace(other); + } + } + break; + } +}) + +// shaking is mining... +input.onGesture(Gesture.Shake, () => { + led.stopAnimation() + basic.clearScreen() + basic.pause(200) // display a short pause + if (Math.random(3) == 0) { // 30% chances to add a transaction + // gold!!! + me.addBlock(); + basic.showIcon(IconNames.Diamond); + } else { + // missed! + basic.showIcon(IconNames.Asleep); + } +}) + +// show my score +input.onButtonPressed(Button.A, () => { + led.stopAnimation() + let score = 0; + for (let i = 0; i < me.chain.length; ++i) { + if (me.chain[i].data == me.id) + score++; + } + basic.showNumber(score); +}) + +// show the block chain size +input.onButtonPressed(Button.B, () => { + led.stopAnimation() + basic.showNumber(me.chain.length - 1); +}) +input.onButtonPressed(Button.AB, () => { + led.stopAnimation() + for (let i = 1; i < me.chain.length; ++i) { + basic.showNumber(me.chain[i].data); + } +}) + +// ask neighbors for chains +broadcastQueryChain(); +basic.showString("A=SCORE B=CHAIN SHAKE=MINE", 100) +``` + +## References + +* https://medium.com/@lhartikk/a-blockchain-in-200-lines-of-code-963cc1cc0e54 +* https://medium.com/crypto-currently/lets-build-the-tiniest-blockchain-e70965a248b +* https://medium.com/@micheledaliessi/how-does-the-blockchain-work-98c8cd01d2ae + + +```package +radio +``` \ No newline at end of file diff --git a/docs/projects/wallet.md b/docs/projects/wallet.md index 797c17af..9d43a6fd 100644 --- a/docs/projects/wallet.md +++ b/docs/projects/wallet.md @@ -10,6 +10,11 @@ Make a @boardname@ wallet with this guided tutorial! ![wallet image](/static/mb/projects/wallet/wallet.jpg) +## Activities + +* [Make](/projects/wallet/make) +* [Code](/projects/wallet/code) + ## ~button /projects/wallet/make Let's get started! diff --git a/docs/static/mb/projects/micro-coin.png b/docs/static/mb/projects/micro-coin.png new file mode 100644 index 0000000000000000000000000000000000000000..2219ef13d9e4b84dc8a69f7eb765ec20a602c598 GIT binary patch literal 9340 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 MK|}tBtVQ_$084rbpa1{> literal 0 HcmV?d00001