├── js ├── servers │ ├── common.js │ ├── server_docs │ │ ├── admarket.js │ │ ├── demand.js │ │ └── supply.js │ ├── loader_admarket.js │ ├── loader_demand.js │ ├── loader_supply.js │ ├── index.js │ ├── admarket.js │ ├── demand.js │ └── supply.js ├── utils.js ├── storage │ ├── DATA_IMPRESSION │ ├── A_DATA_IMPRESSION │ ├── S_DATA_IMPRESSION │ ├── index.js │ ├── DATA_CHANNEL │ ├── A_DATA_CHANNEL │ └── S_DATA_CHANNEL ├── config.js ├── actions.js ├── script1.js ├── script3.js ├── script2.js ├── setup.js ├── interface │ └── api.js ├── channel.js ├── __tests__ │ └── admarket.js └── reducers.js ├── .eslintignore ├── .eslintrc ├── tests ├── contracts │ ├── loader_test.js │ └── admarket.js └── channel.js ├── .babelrc ├── .gitignore ├── circle.yml ├── package.json ├── contracts ├── ECVerify.sol └── AdMarket.sol └── README.md /js/servers/common.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /js/servers/server_docs/admarket.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: standard 3 | -------------------------------------------------------------------------------- /js/servers/loader_admarket.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | require('./admarket.js') 3 | -------------------------------------------------------------------------------- /js/servers/loader_demand.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | require('./demand.js') 3 | -------------------------------------------------------------------------------- /js/servers/loader_supply.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | require('./supply.js') 3 | -------------------------------------------------------------------------------- /tests/contracts/loader_test.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | require('./admarket.js') 3 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | export function wait (ms) { 2 | return new Promise(resolve => setTimeout(resolve, ms)) 3 | } 4 | -------------------------------------------------------------------------------- /js/servers/index.js: -------------------------------------------------------------------------------- 1 | 2 | require('babel-register') 3 | 4 | require('./admarket') 5 | require('./demand') 6 | require('./supply') 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["*.min.js"], 3 | "compact": false, 4 | "presets": [["env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | }], "stage-2"] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config.json 3 | build/ 4 | npm-debug.log 5 | testdb/ 6 | serverdb/ 7 | coverage/ 8 | 9 | *~ 10 | .*~ 11 | .*.~ 12 | *.swp 13 | *.swo 14 | .*.swp 15 | .*.swo 16 | *.log 17 | *.log.* 18 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6 4 | 5 | dependencies: 6 | post: 7 | - npm run testrpc: 8 | background: true 9 | - sleep 5 10 | 11 | test: 12 | override: 13 | # - npm run lint 14 | - npm run test 15 | -------------------------------------------------------------------------------- /js/storage/DATA_IMPRESSION: -------------------------------------------------------------------------------- 1 | {"price":1,"supplyId":"0x22222222222222222222","demandId":"0x11111111111111111111","impressionId":"1","time":1497807342.253,"signature":"0x0ee2713294aeb60de52e17c2172be26cb01e7b31ebf64510643588a4b106e049195b858ba127b127c191c7025babe401f3406b7019b3bdd0e05a8d2d9730bcc01c","_id":"FEu2g2POj2xkcaRD"} 2 | -------------------------------------------------------------------------------- /js/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | demand: { 3 | address: '0x3055a99a7faf398c57483df87826366acdbe62c7', 4 | privKey: 'a05fc3b43673fce3cfbfe92e30be397e293728d3f314d37ae21489b6a4cfc1e4' 5 | }, 6 | supply: { 7 | address: '0x43dcbf684ed06db394186624d2a1600f99c14e69', 8 | privKey: 'c3bc97034d7e7076dfb0cad842a899cb6e3b9964e8eb56148762042e2b43ad10' 9 | }, 10 | adMarket: { 11 | address: '0x880f6d91e462a06c5ba6007aaae4f0a700d428c9', 12 | privKey: '19fdeb280ed2eda8d5fafa93cbc1638e3d99c22b1509aaa4ef29dab2227b3fbd' 13 | } 14 | } 15 | 16 | export default config 17 | -------------------------------------------------------------------------------- /js/actions.js: -------------------------------------------------------------------------------- 1 | // Web Page Events 2 | export function impressionServed (impression) { 3 | return { 4 | type: 'IMPRESSION_SERVED', 5 | payload: impression 6 | } 7 | } 8 | export function trackingDataReceived () {} 9 | 10 | // Supply Peers Events 11 | export function requestImpressionCleared (impressionId, arbiterSig) {} 12 | 13 | // Human Events 14 | export function openChannel () {} 15 | export function closeChannel () {} 16 | 17 | // Demand Peers Events 18 | export function initiateCheckpoint () {} 19 | 20 | // Blockchain Events 21 | export function updateRegistry () {} 22 | export function updateChannel () {} 23 | -------------------------------------------------------------------------------- /js/storage/A_DATA_IMPRESSION: -------------------------------------------------------------------------------- 1 | {"price":1,"supplyId":"0x22222222222222222222","demandId":"0x11111111111111111111","impressionId":"1","time":1497807325.844,"_id":"vBjVv0CrX5syEDrY"} 2 | {"$$deleted":true,"_id":"vBjVv0CrX5syEDrY"} 3 | {"price":1,"supplyId":"0x22222222222222222222","demandId":"0x11111111111111111111","impressionId":"1","time":1497807342.253,"_id":"J1XhcwjbWM60HtZJ"} 4 | {"price":1,"supplyId":"0x22222222222222222222","demandId":"0x11111111111111111111","impressionId":"1","time":1497807342.253,"signature":"0x8e6182c4610f4bc1f5512b771b32ebdced120a91011398cb283073a345a92fe67e9b42ed1c810a245aabf11d5de68868b3977e98825ffea8b34230d73c6fd7a81c","impressions":1,"balance":1,"root":"0x8e16c43f3559d28eee9725af2ae48f8846aefe522700ad3928abae3ab4d9e0b4","prevRoot":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","_id":"J7MFrn7Tsu1W4dxv"} 5 | -------------------------------------------------------------------------------- /js/storage/S_DATA_IMPRESSION: -------------------------------------------------------------------------------- 1 | {"price":1,"supplyId":"0x22222222222222222222","demandId":"0x11111111111111111111","impressionId":"1","time":1497807325.844,"_id":"gpYXzmwyur7IwXvd"} 2 | {"$$deleted":true,"_id":"gpYXzmwyur7IwXvd"} 3 | {"price":1,"supplyId":"0x22222222222222222222","demandId":"0x11111111111111111111","impressionId":"1","time":1497807342.253,"_id":"7SdEXfxKYP0UBQAA"} 4 | {"price":1,"supplyId":"0x22222222222222222222","demandId":"0x11111111111111111111","impressionId":"1","time":1497807342.253,"signature":"0x8e6182c4610f4bc1f5512b771b32ebdced120a91011398cb283073a345a92fe67e9b42ed1c810a245aabf11d5de68868b3977e98825ffea8b34230d73c6fd7a81c","impressions":1,"balance":1,"root":"0x8e16c43f3559d28eee9725af2ae48f8846aefe522700ad3928abae3ab4d9e0b4","prevRoot":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","_id":"FIcXlLTadP2RF02T"} 5 | -------------------------------------------------------------------------------- /js/storage/index.js: -------------------------------------------------------------------------------- 1 | import Datastore from 'nedb' 2 | import path from 'path' 3 | 4 | // datastore for plain impressions 5 | // datastore for channel state 6 | 7 | export const impressionDB = new Datastore({ filename: path.join(__dirname, '/DATA_IMPRESSION'), autoload: true }) 8 | export const channelDB = new Datastore({ filename: path.join(__dirname, '/DATA_CHANNEL'), autoload: true }) 9 | 10 | export const supImpDB = new Datastore({ filename: path.join(__dirname, '/S_DATA_IMPRESSION'), autoload: true }) 11 | export const supChDB = new Datastore({ filename: path.join(__dirname, '/S_DATA_CHANNEL'), autoload: true }) 12 | 13 | export const amImpDB = new Datastore({ filename: path.join(__dirname, '/A_DATA_IMPRESSION'), autoload: true }) 14 | export const amChDB = new Datastore({ filename: path.join(__dirname, '/A_DATA_CHANNEL'), autoload: true }) 15 | -------------------------------------------------------------------------------- /js/storage/DATA_CHANNEL: -------------------------------------------------------------------------------- 1 | {"contractId":"0x12345123451234512345","channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","demand":"0x11111111111111111111","supply":"0x22222222222222222222","impressionId":"foo","price":1,"impressions":0,"root":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","balance":0,"state":0,"expiration":100,"challengeTimeout":100,"proposedRoot":0,"_id":"X4cYc478r0DRpJpi"} 2 | {"$$deleted":true,"_id":"X4cYc478r0DRpJpi"} 3 | {"contractId":"0x12345123451234512345","channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","demand":"0x11111111111111111111","supply":"0x22222222222222222222","impressionId":"foo","price":1,"impressions":0,"root":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","balance":0,"state":0,"expiration":100,"challengeTimeout":100,"proposedRoot":0,"_id":"AdM4hltRpoyE9hua"} 4 | {"root":"0x8e16c43f3559d28eee9725af2ae48f8846aefe522700ad3928abae3ab4d9e0b4","impressionId":"1","demandId":"0x11111111111111111111","proposedRoot":0,"price":1,"supplyId":"0x22222222222222222222","impressions":1,"demand":"0x11111111111111111111","contractId":"0x12345123451234512345","time":1497807342.253,"supply":"0x22222222222222222222","expiration":100,"state":0,"prevRoot":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","signature":"0x8e6182c4610f4bc1f5512b771b32ebdced120a91011398cb283073a345a92fe67e9b42ed1c810a245aabf11d5de68868b3977e98825ffea8b34230d73c6fd7a81c","balance":1,"channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","challengeTimeout":100,"_id":"AdM4hltRpoyE9hua"} 5 | -------------------------------------------------------------------------------- /js/storage/A_DATA_CHANNEL: -------------------------------------------------------------------------------- 1 | {"contractId":"0x12345123451234512345","channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","demand":"0x11111111111111111111","supply":"0x22222222222222222222","impressionId":"foo","price":1,"impressions":0,"root":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","balance":0,"state":0,"expiration":100,"challengeTimeout":100,"proposedRoot":0,"pendingUpdates":{"size":0,"_origin":0,"_capacity":0,"_level":5,"__altered":false},"_id":"GMnC4EGBLbesFfDm"} 2 | {"$$deleted":true,"_id":"GMnC4EGBLbesFfDm"} 3 | {"contractId":"0x12345123451234512345","channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","demand":"0x11111111111111111111","supply":"0x22222222222222222222","impressionId":"foo","price":1,"impressions":0,"root":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","balance":0,"state":0,"expiration":100,"challengeTimeout":100,"proposedRoot":0,"pendingUpdates":{"size":0,"_origin":0,"_capacity":0,"_level":5,"__altered":false},"_id":"oxo3kr1nJ5HgIgvo"} 4 | {"root":"0x8e16c43f3559d28eee9725af2ae48f8846aefe522700ad3928abae3ab4d9e0b4","impressionId":"1","demandId":"0x11111111111111111111","proposedRoot":0,"price":1,"supplyId":"0x22222222222222222222","impressions":1,"demand":"0x11111111111111111111","contractId":"0x12345123451234512345","time":1497807342.253,"supply":"0x22222222222222222222","expiration":100,"state":0,"pendingUpdates":[],"prevRoot":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","signature":"0x8e6182c4610f4bc1f5512b771b32ebdced120a91011398cb283073a345a92fe67e9b42ed1c810a245aabf11d5de68868b3977e98825ffea8b34230d73c6fd7a81c","balance":1,"channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","challengeTimeout":100,"_id":"oxo3kr1nJ5HgIgvo"} 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adchain", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint --fix .", 8 | "start": "node js/servers", 9 | "test": "jest --coverage", 10 | "mocha": "mocha ./tests/contracts/loader_test.js -R spec --timeout 2000000", 11 | "testrpc": "testrpc -d -m 'elegant ability lawn fiscal fossil general swarm trap bind require exchange ostrich'" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "async": "^2.1.5", 17 | "bluebird": "^3.5.0", 18 | "body-parser": "^1.17.1", 19 | "es6-promisify": "^5.0.0", 20 | "ether-pudding": "^3.2.0", 21 | "ethereumjs-util": "^5.1.1", 22 | "ethjs-contract": "^0.1.7", 23 | "ethjs-provider-http": "^0.1.4", 24 | "ethjs-query": "^0.2.1", 25 | "express": "^4.15.2", 26 | "immutable": "^3.8.1", 27 | "js-sha256": "^0.3.2", 28 | "left-pad": "^1.1.3", 29 | "merkle-tree-solidity": "^1.0.4", 30 | "nedb": "^1.8.0", 31 | "redux": "^3.6.0", 32 | "redux-immutable": "^3.1.0", 33 | "request": "^2.81.0", 34 | "request-promise": "^4.2.0", 35 | "solc": "^0.4.7", 36 | "web3": "^0.17.0-beta" 37 | }, 38 | "devDependencies": { 39 | "babel-eslint": "^6.0.2", 40 | "babel-jest": "^19.0.0", 41 | "babel-preset-env": "^1.4.0", 42 | "babel-preset-stage-2": "^6.5.0", 43 | "babel-register": "^6.18.0", 44 | "blue-tape": "^1.0.0", 45 | "chai": "^3.5.0", 46 | "eslint": "^3.19.0", 47 | "eslint-config-standard": "^10.2.1", 48 | "eslint-plugin-import": "^2.2.0", 49 | "eslint-plugin-node": "^4.2.2", 50 | "eslint-plugin-promise": "^3.5.0", 51 | "eslint-plugin-standard": "^3.0.1", 52 | "ethereumjs-testrpc": "^3.0.3", 53 | "jest": "^19.0.2" 54 | }, 55 | "jest": { 56 | "testEnvironment": "node" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /contracts/ECVerify.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.7; 2 | 3 | // 4 | // The new assembly support in Solidity makes writing helpers easy. 5 | // Many have complained how complex it is to use `ecrecover`, especially in conjunction 6 | // with the `eth_sign` RPC call. Here is a helper, which makes that a matter of a single call. 7 | // 8 | // Sample input parameters: 9 | // (with v=0) 10 | // "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad", 11 | // "0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf200", 12 | // "0x0e5cb767cce09a7f3ca594df118aa519be5e2b5a" 13 | // 14 | // (with v=1) 15 | // "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad", 16 | // "0xdebaaa0cddb321b2dcaaf846d39605de7b97e77ba6106587855b9106cb10421561a22d94fa8b8a687ff9c911c844d1c016d1a685a9166858f9c7c1bc85128aca01", 17 | // "0x8743523d96a1b2cbe0c6909653a56da18ed484af" 18 | // 19 | // (The hash is a hash of "hello world".) 20 | // 21 | // Written by Alex Beregszaszi (@axic), use it under the terms of the MIT license. 22 | // 23 | 24 | 25 | contract ECVerify { 26 | // event LogNum(uint8 num); 27 | // event LogNum256(uint256 num); 28 | // event LogBool(bool b); 29 | function ecrecovery(bytes32 hash, bytes sig) returns (address) { 30 | bytes32 r; 31 | bytes32 s; 32 | uint8 v; 33 | 34 | // FIXME: Should this throw, or return 0? 35 | if (sig.length != 65) { 36 | return 0; 37 | } 38 | 39 | // The signature format is a compact form of: 40 | // {bytes32 r}{bytes32 s}{uint8 v} 41 | // Compact means, uint8 is not padded to 32 bytes. 42 | assembly { 43 | r := mload(add(sig, 32)) 44 | s := mload(add(sig, 64)) 45 | v := mload(add(sig, 65)) 46 | } 47 | 48 | // old geth sends a `v` value of [0,1], while the new, in line with the YP sends [27,28] 49 | if (v < 27) 50 | v += 27; 51 | 52 | return ecrecover(hash, v, r, s); 53 | } 54 | 55 | function ecverify(bytes32 hash, bytes sig, address signer) returns (bool b) { 56 | b = ecrecovery(hash, sig) == signer; 57 | // LogBool(b); 58 | return b; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /js/script1.js: -------------------------------------------------------------------------------- 1 | // script1.js 2 | // Generates 1 impression, sends to all participants 3 | 4 | const request = require('request-promise') 5 | 6 | const mode = process.env.mode 7 | 8 | const wait = timeout => new Promise(resolve => setTimeout(resolve, timeout)) 9 | 10 | function generateImpressions (count, price, supplyId, demandId) { 11 | const impressions = [] 12 | for (let i = 0; i < count; i++) { 13 | impressions.push({ price, supplyId, demandId, impressionId: (i + 1).toString(), time: new Date().getTime() / 1000 }) 14 | } 15 | return impressions 16 | } 17 | 18 | const demandId = '0x11111111111111111111' 19 | const supplyId = '0x22222222222222222222' 20 | 21 | const impressions = generateImpressions(1, 1, supplyId, demandId) 22 | console.log('\nImpression to send:\n') 23 | console.log(impressions) 24 | console.log('\n') 25 | 26 | async function openChannel () { 27 | // Supply 28 | await request('http://localhost:3000/open') 29 | 30 | // Demand 31 | await request('http://localhost:3001/open') 32 | 33 | // AdMarket 34 | await request('http://localhost:3002/open') 35 | 36 | console.log('Connected to all adservers') 37 | } 38 | 39 | async function sendImpression (impression) { 40 | request.post({ 41 | url: 'http://localhost:3002', 42 | body: impression, 43 | json: true 44 | }) 45 | 46 | request.post({ 47 | url: 'http://localhost:3000', 48 | body: impression, 49 | json: true 50 | }) 51 | 52 | request.post({ 53 | url: 'http://localhost:3001', 54 | body: impression, 55 | json: true 56 | }) 57 | } 58 | 59 | async function main () { 60 | await openChannel() 61 | 62 | for (let impression of impressions) { 63 | await sendImpression(impression) 64 | } 65 | 66 | console.log('Impressions sent') 67 | 68 | // await wait(1000) 69 | // const body = await request('http://localhost:3001/state') 70 | 71 | /* 72 | request.get({ url: 'http://localhost:3000/verify', body: { 73 | supplyId: supplyId, 74 | demandId: demandId, 75 | root: root, 76 | start: 0, 77 | end: 2 78 | }, json: true }, function(err, res, body) { 79 | console.log('PEWPEWPEW') 80 | console.log(body) 81 | }); 82 | */ 83 | } 84 | 85 | main().catch((err) => { 86 | console.error(err.stack) 87 | process.exit(1) 88 | }) 89 | -------------------------------------------------------------------------------- /js/script3.js: -------------------------------------------------------------------------------- 1 | // script1.js 2 | // Generates 1 impression, sends to only supply 3 | 4 | const request = require('request-promise') 5 | 6 | const mode = process.env.mode 7 | 8 | const wait = timeout => new Promise(resolve => setTimeout(resolve, timeout)) 9 | 10 | function generateImpressions (count, price, supplyId, demandId) { 11 | const impressions = [] 12 | for (let i = 0; i < count; i++) { 13 | impressions.push({ price, supplyId, demandId, impressionId: (i + 1).toString(), time: new Date().getTime() / 1000 }) 14 | } 15 | return impressions 16 | } 17 | 18 | const demandId = '0x11111111111111111111' 19 | const supplyId = '0x22222222222222222222' 20 | 21 | const impressions = generateImpressions(1, 1, supplyId, demandId) 22 | console.log('\nImpression to send:\n') 23 | console.log(impressions) 24 | console.log('\n') 25 | 26 | async function openChannel () { 27 | // Supply 28 | await request('http://localhost:3000/open') 29 | 30 | // Demand 31 | await request('http://localhost:3001/open') 32 | 33 | // AdMarket 34 | await request('http://localhost:3002/open') 35 | 36 | console.log('Connected to all adservers') 37 | } 38 | 39 | async function sendImpression (impression) { 40 | request.post({ 41 | url: 'http://localhost:3002', 42 | body: impression, 43 | json: true 44 | }) 45 | 46 | /* 47 | request.post({ 48 | url: 'http://localhost:3000', 49 | body: impression, 50 | json: true 51 | }) 52 | */ 53 | 54 | request.post({ 55 | url: 'http://localhost:3001', 56 | body: impression, 57 | json: true 58 | }) 59 | } 60 | 61 | async function main () { 62 | await openChannel() 63 | 64 | for (let impression of impressions) { 65 | await sendImpression(impression) 66 | } 67 | 68 | console.log('Impressions sent') 69 | 70 | // await wait(1000) 71 | // const body = await request('http://localhost:3001/state') 72 | 73 | /* 74 | request.get({ url: 'http://localhost:3000/verify', body: { 75 | supplyId: supplyId, 76 | demandId: demandId, 77 | root: root, 78 | start: 0, 79 | end: 2 80 | }, json: true }, function(err, res, body) { 81 | console.log('PEWPEWPEW') 82 | console.log(body) 83 | }); 84 | */ 85 | } 86 | 87 | main().catch((err) => { 88 | console.error(err.stack) 89 | process.exit(1) 90 | }) 91 | -------------------------------------------------------------------------------- /js/script2.js: -------------------------------------------------------------------------------- 1 | // script1.js 2 | // Generates 1 impression, sends to only supply 3 | 4 | const request = require('request-promise') 5 | 6 | const mode = process.env.mode 7 | 8 | const wait = timeout => new Promise(resolve => setTimeout(resolve, timeout)) 9 | 10 | function generateImpressions (count, price, supplyId, demandId) { 11 | const impressions = [] 12 | for (let i = 0; i < count; i++) { 13 | impressions.push({ price, supplyId, demandId, impressionId: (i + 1).toString(), time: new Date().getTime() / 1000 }) 14 | } 15 | return impressions 16 | } 17 | 18 | const demandId = '0x11111111111111111111' 19 | const supplyId = '0x22222222222222222222' 20 | 21 | const impressions = generateImpressions(1, 1, supplyId, demandId) 22 | console.log('\nImpression to send:\n') 23 | console.log(impressions) 24 | console.log('\n') 25 | 26 | async function openChannel () { 27 | // Supply 28 | await request('http://localhost:3000/open') 29 | 30 | // Demand 31 | await request('http://localhost:3001/open') 32 | 33 | // AdMarket 34 | await request('http://localhost:3002/open') 35 | 36 | console.log('Connected to all adservers') 37 | } 38 | 39 | async function sendImpression (impression) { 40 | /* 41 | request.post({ 42 | url: 'http://localhost:3002', 43 | body: impression, 44 | json: true 45 | }) 46 | */ 47 | 48 | /* 49 | request.post({ 50 | url: 'http://localhost:3000', 51 | body: impression, 52 | json: true 53 | }) 54 | */ 55 | 56 | request.post({ 57 | url: 'http://localhost:3001', 58 | body: impression, 59 | json: true 60 | }) 61 | } 62 | 63 | async function main () { 64 | await openChannel() 65 | 66 | for (let impression of impressions) { 67 | await sendImpression(impression) 68 | } 69 | 70 | console.log('Impressions sent') 71 | 72 | // await wait(1000) 73 | // const body = await request('http://localhost:3001/state') 74 | 75 | /* 76 | request.get({ url: 'http://localhost:3000/verify', body: { 77 | supplyId: supplyId, 78 | demandId: demandId, 79 | root: root, 80 | start: 0, 81 | end: 2 82 | }, json: true }, function(err, res, body) { 83 | console.log('PEWPEWPEW') 84 | console.log(body) 85 | }); 86 | */ 87 | } 88 | 89 | main().catch((err) => { 90 | console.error(err.stack) 91 | process.exit(1) 92 | }) 93 | -------------------------------------------------------------------------------- /js/servers/server_docs/demand.js: -------------------------------------------------------------------------------- 1 | // Demand Server 2 | 3 | // Receives impression beacon events from the browser. 4 | // Receives impression clearing requests from supply. 5 | 6 | // Endpoints: 7 | 8 | // /impression -> impression event from the browser 9 | // Payload: 10 | // - impression data 11 | // Actions: 12 | // - save the impression to storage 13 | // - update channel state to include impression 14 | // - save the updated channel state to storage 15 | // - send the channel update + impression to Supply 16 | // - listening for their response and retrying is not important, 17 | // Supply will ask if they are missing data 18 | // Sample Payloads (current implementation): 19 | {"timestamp":"2017-03-01T00:00:04.018Z","did":"417","geo":"US","playersize":3,"domain":"http://www.bnd.com/news/local/article134550409.html","sid":"416","width":"640","height":"360","inventory":1} 20 | {"timestamp":"2017-03-01T00:00:03.751Z","did":"-1","geo":"US","playersize":3,"domain":"http://writingcareer.com/deadlines/nonfiction-submissions-deadlines/","sid":"424","width":640,"height":360,"inventory":1} 21 | {"timestamp":"2017-03-01T00:00:03.751Z","did":"420","geo":"US","playersize":3,"domain":"http://writingcareer.com/deadlines/nonfiction-submissions-deadlines/","sid":"424","width":640,"height":360,"playersizeblocked":1} 22 | // Payload will be updated: 23 | // - change did/sid to Ethereum 20 byte hex addresses 24 | // - include channel identifier (channelId, contractId) 25 | 26 | // /request_channel_update -> Supply bringing signed impression from AdMarket 27 | // as proof that an impression which this served didn't record did in fact 28 | // happen. 29 | // Payload: 30 | // - impression data 31 | // - AdMarket signature on impressionId 32 | // Actions: 33 | // - save the message to storage (WAL) 34 | // - verify signature of AdMarket on impressionId 35 | // - if valid: 36 | // - update channel state to include impression 37 | // - save the updated channel state to storage 38 | // - send the channel update + impression to AdMarket 39 | // - respond to Supply with updated channel state 40 | // - if invalid: 41 | // - respond that signature is invalid 42 | // - delete request_channel_update message from storage (WAL) 43 | 44 | // When Booting this server (for each Demand account): 45 | // 1. Read from Blockchain 46 | // - get all active channels 47 | // - get most recent checkpointed states 48 | // 2. Read from Storage 49 | // - get most recent channel states 50 | // - get any WAL messages 51 | // 3. create tasks for all WAL messages 52 | // - execute on request_channel_update messages 53 | -------------------------------------------------------------------------------- /js/storage/S_DATA_CHANNEL: -------------------------------------------------------------------------------- 1 | {"root":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","impressionId":0,"pendingUpdateRequests":[],"proposedRoot":0,"price":0,"impressions":0,"demand":"0x11111111111111111111","contractId":"0x12345123451234512345","supply":"0x22222222222222222222","expiration":100,"state":0,"pendingImpressions":[{"demandId":"0x11111111111111111111","supplyId":"0x22222222222222222222","impressionId":"1","price":1,"time":1497807325.844}],"pendingUpdates":[],"prevRoot":0,"balance":0,"channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","challengeTimeout":100,"_id":"5vthG3RxDOVgpfBj"} 2 | {"$$deleted":true,"_id":"5vthG3RxDOVgpfBj"} 3 | {"contractId":"0x12345123451234512345","channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","demand":"0x11111111111111111111","supply":"0x22222222222222222222","impressionId":"foo","price":1,"impressions":0,"root":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","balance":0,"state":0,"expiration":100,"challengeTimeout":100,"proposedRoot":0,"pendingImpressions":{"size":0,"_origin":0,"_capacity":0,"_level":5,"__altered":false},"pendingUpdates":{"size":0,"_origin":0,"_capacity":0,"_level":5,"__altered":false},"pendingUpdateRequests":{"size":0,"_origin":0,"_capacity":0,"_level":5,"__altered":false},"_id":"EqJe4PooKWS4YHrv"} 4 | {"root":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","impressionId":0,"pendingUpdateRequests":[],"proposedRoot":0,"price":0,"impressions":0,"demand":"0x11111111111111111111","contractId":"0x12345123451234512345","supply":"0x22222222222222222222","expiration":100,"state":0,"pendingImpressions":[{"demandId":"0x11111111111111111111","supplyId":"0x22222222222222222222","impressionId":"1","price":1,"time":1497807342.253}],"pendingUpdates":[],"prevRoot":0,"balance":0,"channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","challengeTimeout":100,"_id":"EqJe4PooKWS4YHrv"} 5 | {"root":"0x8e16c43f3559d28eee9725af2ae48f8846aefe522700ad3928abae3ab4d9e0b4","impressionId":"1","pendingUpdateRequests":[{"price":1,"supplyId":"0x22222222222222222222","demandId":"0x11111111111111111111","impressionId":"1","time":1497807342.253,"signature":"0x0ee2713294aeb60de52e17c2172be26cb01e7b31ebf64510643588a4b106e049195b858ba127b127c191c7025babe401f3406b7019b3bdd0e05a8d2d9730bcc01c"}],"demandId":"0x11111111111111111111","proposedRoot":0,"price":1,"supplyId":"0x22222222222222222222","impressions":1,"demand":"0x11111111111111111111","contractId":"0x12345123451234512345","time":1497807342.253,"supply":"0x22222222222222222222","expiration":100,"state":0,"pendingImpressions":[],"pendingUpdates":[],"prevRoot":"0x044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d","signature":"0x8e6182c4610f4bc1f5512b771b32ebdced120a91011398cb283073a345a92fe67e9b42ed1c810a245aabf11d5de68868b3977e98825ffea8b34230d73c6fd7a81c","balance":1,"channelId":"0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d","challengeTimeout":100,"_id":"EqJe4PooKWS4YHrv"} 6 | -------------------------------------------------------------------------------- /js/setup.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import p from 'es6-promisify' 3 | import TestRPC from 'ethereumjs-testrpc' 4 | import solc from 'solc' 5 | import Eth from 'ethjs-query' 6 | import EthContract from 'ethjs-contract' 7 | import Web3 from 'web3' 8 | import HttpProvider from 'ethjs-provider-http' 9 | 10 | const SOL_PATH = __dirname + '/../contracts/' 11 | const TESTRPC_PORT = 8545 12 | const MNEMONIC = 'elegant ability lawn fiscal fossil general swarm trap bind require exchange ostrich' 13 | 14 | // opts 15 | // testRPCServer - if true, starts a testRPC server 16 | // mnemonic - seed for accounts 17 | // port - testrpc port 18 | // noDeploy - if true, skip adMarket contract deployment 19 | // testRPCProvider - http connection string for console testprc instance 20 | export default async function (opts) { 21 | opts = opts || {} 22 | const mnemonic = opts.mnemonic || MNEMONIC 23 | const testRPCServer = opts.testRPCServer 24 | const port = opts.port || TESTRPC_PORT 25 | const noDeploy = opts.noDeploy 26 | const defaultAcct = opts.defaultAcct ? opts.defaultAcct : 0 27 | 28 | // default: 30 days of 15s blocks on average 29 | const channelTimeout = opts.channelTimeout || 172800 30 | 31 | // default: 1 day of 15s blocks on average 32 | const challengePeriod = opts.challengePeriod || 5760 33 | const ownerUrl = 'foo.net' 34 | 35 | // START TESTRPC PROVIDER 36 | let provider 37 | if (opts.testRPCProvider) { 38 | provider = new HttpProvider(opts.testRPCProvider) 39 | } else { 40 | provider = TestRPC.provider({ 41 | mnemonic: mnemonic, 42 | }) 43 | } 44 | 45 | // START TESTRPC SERVER 46 | if (opts.testRPCServer) { 47 | console.log('setting up testrpc server') 48 | await p(TestRPC.server({ 49 | mnemonic: mnemonic 50 | }).listen)(port) 51 | } 52 | 53 | // BUILD ETHJS ABSTRACTIONS 54 | const eth = new Eth(provider) 55 | const contract = new EthContract(eth) 56 | const accounts = await eth.accounts() 57 | 58 | // COMPILE THE CONTRACT 59 | const input = { 60 | 'AdMarket.sol': fs.readFileSync(SOL_PATH + 'AdMarket.sol').toString(), 61 | 'ECVerify.sol': fs.readFileSync(SOL_PATH + 'ECVerify.sol').toString() 62 | } 63 | 64 | const output = solc.compile({ sources: input }, 1) 65 | if (output.errors) { console.log(Error(output.errors)) } 66 | 67 | const abi = JSON.parse(output.contracts['AdMarket.sol:AdMarket'].interface) 68 | const bytecode = output.contracts['AdMarket.sol:AdMarket'].bytecode 69 | 70 | // PREPARE THE ADMARKET ABSTRACTION OBJECT 71 | const AdMarket = contract(abi, bytecode, { 72 | from: accounts[defaultAcct], 73 | gas: 3000000 74 | }) 75 | 76 | let adMarketTxHash, adMarketReceipt, adMarket 77 | 78 | if (!noDeploy) { 79 | // DEPLOY THE ADMARKET CONTRACT 80 | adMarketTxHash = await AdMarket.new(ownerUrl, channelTimeout, challengePeriod) 81 | await wait(1500) 82 | // USE THE ADDRESS FROM THE TX RECEIPT TO BUILD THE CONTRACT OBJECT 83 | adMarketReceipt = await eth.getTransactionReceipt(adMarketTxHash) 84 | adMarket = AdMarket.at(adMarketReceipt.contractAddress) 85 | } 86 | 87 | // MAKE WEB3 88 | const web3 = new Web3() 89 | web3.setProvider(provider) 90 | web3.eth.defaultAccount = accounts[0] 91 | 92 | return { adMarket, AdMarket, eth, accounts, web3 } 93 | } 94 | 95 | // async/await compatible setTimeout 96 | // http://stackoverflow.com/questions/38975138/is-using-async-in-settimeout-valid 97 | // await wait(2000) 98 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) 99 | -------------------------------------------------------------------------------- /js/servers/server_docs/supply.js: -------------------------------------------------------------------------------- 1 | // Supply Server 2 | 3 | // Receives impression beacon events from the browser. 4 | // Receives channel update events from Demand. 5 | 6 | // Endpoints: 7 | 8 | // /impression -> impression event from the browser 9 | // Payload: 10 | // - impression data 11 | // Actions: 12 | // - save the impression to storage 13 | // - add the impression to a "pendingImpressions" queue for this channel 14 | // - after some time, if Demand has not included this impression in 15 | // a channel_update, send a request_signature message to the AdMarket 16 | // - if the AdMarket has seen impression and responds w/ sig: 17 | // - verify signature 18 | // - save their response in storage (WAL) 19 | // - add the message to the "pendingUpdateRequests" queue 20 | // - send the request_channel_update to Demand 21 | // - if Demand does not reply with the impression: 22 | // - close the channel 23 | // - if Demand does reply with the channel_update: 24 | // - save the channel update to storage 25 | // - verify the channel update signature 26 | // - verify the channel update state transition 27 | // - remove the update from "pendingUpdateRequests" 28 | // - if the AdMarket has not seen the impression and responds 404: 29 | // - remove the impression from the "pendingImpressions" queue 30 | // - delete the stored impression 31 | // Sample Payloads (current implementation): 32 | {"timestamp":"2017-03-01T00:00:04.018Z","did":"417","geo":"US","playersize":3,"domain":"http://www.bnd.com/news/local/article134550409.html","sid":"416","width":"640","height":"360","inventory":1} 33 | {"timestamp":"2017-03-01T00:00:03.751Z","did":"-1","geo":"US","playersize":3,"domain":"http://writingcareer.com/deadlines/nonfiction-submissions-deadlines/","sid":"424","width":640,"height":360,"inventory":1} 34 | {"timestamp":"2017-03-01T00:00:03.751Z","did":"420","geo":"US","playersize":3,"domain":"http://writingcareer.com/deadlines/nonfiction-submissions-deadlines/","sid":"424","width":640,"height":360,"playersizeblocked":1} 35 | // Payload will be updated: 36 | // - change did/sid to Ethereum 20 byte hex addresses 37 | // - include channel identifier (channelId, contractId) 38 | 39 | // /channel_update -> channel update message from Demand 40 | // Payload: 41 | // - impression data 42 | // - channel update 43 | // - Demand's signature on channel update 44 | // Actions: 45 | // - verify the channel update signature 46 | // - verify the channel update state transition 47 | // - if we haven't already seen the impression, save it to storage 48 | // - save the channel update to storage (WAL) 49 | // - remove (if it exists) the corresponding impression from this 50 | // channel's "pendingImpressions" queue 51 | // Note: 52 | // - channel updates can come out of order, but must be verified in order, 53 | // so we store the channel update in a "pendingUpdates" queue until the next 54 | // update in order arrives. 55 | 56 | // When Booting this server (for each Supply account): 57 | // 1. Read from Blockchain 58 | // - get all active channels 59 | // - get most recent checkpointed states 60 | // 2. Read from Storage 61 | // - get most recent channel states 62 | // - get any WAL messages 63 | // 3. create tasks for all WAL messages 64 | // - repopulate pendingImpressions queue with impressions that have yet to be 65 | // included in channel updates 66 | // - repopulate pendingUpdates queue with out of order channel updates that 67 | // are missing the earlier message 68 | // - repopulate pendingChannelUpdates queue with impressions the AdMarket has 69 | // signed off on but the Demand has yet to include in a channel update 70 | -------------------------------------------------------------------------------- /tests/channel.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import p from 'es6-promisify' 3 | import Web3 from 'web3' 4 | import MerkleTree, { checkProof, merkleRoot } from 'merkle-tree-solidity' 5 | import { sha3 } from 'ethereumjs-util' 6 | import setup from './setup' 7 | import { parseChannel, getFingerprint, getRoot, solSha3, parseLogAddress, 8 | verifySignature, makeUpdate, verifyUpdate, parseBN } from './channel' 9 | import { wait } from './utils' 10 | 11 | const web3 = new Web3() 12 | const accounts = web3.eth.accounts 13 | 14 | describe('channel', async () => { 15 | it('getRoot', () => { 16 | const channel = { 17 | contractId: '0x12345123451234512345', 18 | channelId: web3.sha3('foo'), 19 | demand: '0x11111111111111111111', 20 | supply: '0x22222222222222222222', 21 | root: web3.sha3('foo'), 22 | } 23 | 24 | const root = '0x'+merkleRoot([ 25 | `impId:${channel.impressionId}`, 26 | `impPrice:${channel.impressionPrice}`, 27 | `impCount:${channel.impressions}`, 28 | `balance:${channel.balance}`, 29 | `prevRoot:${channel.root}` 30 | ].map(e => sha3(e))).toString('hex') 31 | 32 | assert.equal(root, getRoot(channel, channel.root)) 33 | }) 34 | 35 | it('verifySignature', async () => { 36 | const channel = { 37 | contractId: '0x12345123451234512345', 38 | channelId: web3.sha3('foo'), 39 | demand: '0x11111111111111111111', 40 | supply: '0x22222222222222222222', 41 | impressionId: 'foo', 42 | impressionPrice: 1, 43 | impressions: 1000, 44 | balance: 1000 45 | } 46 | 47 | channel.root = getRoot(channel, web3.sha3('foo')) 48 | 49 | const fingerprint = getFingerprint(channel) 50 | const sig = await p(web3.eth.sign)(accounts[0], fingerprint) 51 | assert.ok(verifySignature(channel, sig, accounts[0])) 52 | }) 53 | 54 | it('makeUpdate', () => { 55 | const channel = { 56 | contractId: '0x12345123451234512345', 57 | channelId: web3.sha3('foo'), 58 | demand: '0x11111111111111111111', 59 | supply: '0x22222222222222222222', 60 | impressionId: 'foo', 61 | impressionPrice: 1, 62 | impressions: 1000, 63 | balance: 1000 64 | } 65 | 66 | channel.root = getRoot(channel, web3.sha3('foo')) 67 | 68 | const input = { 69 | impressionId: web3.sha3('bar'), 70 | impressionPrice: 2 71 | } 72 | 73 | const update = makeUpdate(channel, input) 74 | 75 | assert.equal(update.impressionId, web3.sha3('bar')) 76 | assert.equal(update.impressionPrice, 2) 77 | assert.equal(update.impressions, 1001) 78 | assert.equal(update.balance, 1002) 79 | assert.equal(update.root, getRoot(update, channel.root)) 80 | assert.equal(update.prevRoot, channel.root) 81 | assert.equal(update.contractId, channel.contractId) 82 | assert.equal(update.channelId, channel.channelId) 83 | assert.equal(update.demand, channel.demand) 84 | assert.equal(update.supply, channel.supply) 85 | }) 86 | 87 | it('verifyUpdate', () => { 88 | const channel = { 89 | contractId: '0x12345123451234512345', 90 | channelId: web3.sha3('foo'), 91 | demand: '0x11111111111111111111', 92 | supply: '0x22222222222222222222', 93 | impressionId: 'foo', 94 | impressionPrice: 1, 95 | impressions: 1000, 96 | balance: 1000 97 | } 98 | 99 | channel.root = getRoot(channel, web3.sha3('foo')) 100 | 101 | const input = { 102 | impressionId: web3.sha3('bar'), 103 | impressionPrice: 2 104 | } 105 | 106 | const update = makeUpdate(channel, input) 107 | assert.ok(verifyUpdate(channel, update)) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /js/interface/api.js: -------------------------------------------------------------------------------- 1 | // Top level API 2 | 3 | /* 4 | * Demand Methods 5 | */ 6 | 7 | // Fires when demand receives the impression (after it has been saved) 8 | function recordImpressionServed (supplyId, impressionId, impressionPrice) { 9 | // 1. lookup the channel from the supplyId 10 | // 2. update the channel store with new state 11 | // 3. send the new state to the peer (set status to pending) 12 | // 4. upon receiving the ACK, update the state in memory from pending to done 13 | // 14 | // possible optimizations: 15 | // - wait for gaps to be filled to save. only save a whole batch once all 16 | // previous transactions have been saved 17 | // - supply can just ack most recent transaction 18 | // 19 | // Notes: 20 | // - In case of system failure, we should do a scan for all impressions 21 | // saved without corresponding ACKs from supply. 22 | // - This means the ACK from supply is required before the commit. 23 | // - We should save the ACK in the same DB table 24 | // - There is no reason to save the channel update until the response 25 | // - No need for saving the intermediate "state updated but ack 26 | // pending" state. 27 | // - Treat saving the impression as our WAL 28 | } 29 | 30 | // Priorities: 31 | // 1. Finish up a strawman SQL database to use as a guide / test 32 | // - save impressions 33 | // - trigger processImpression 34 | // 2. Implement the top level processImpressions function 35 | // - update channel store 36 | // - create schema for channels 37 | // - create reducer for updating channel state 38 | 39 | function saveImpression () {} 40 | 41 | function processImpression () {} 42 | 43 | function saveChannel () {} 44 | 45 | function notifyPeer () {} 46 | 47 | function recordACK () {} 48 | 49 | // Other messages 50 | 51 | function provideData () {} 52 | 53 | // In order to prevent replay attacks, even those that may be accidental, we 54 | // need to save on the demand side intermediate state updates without ACKs. The 55 | // reason why is to preserve ordering. If messages are ever replayed to a peer 56 | // in a different order (e.g. in the event of a crash), then we are replaying. 57 | // Either messages on the demand side should be saved in order, or we should 58 | // assign order only once ever after the impression is received. In order to 59 | // not depend on an external system for order (even if we depend on it as a WAL 60 | // for impressions) we should implement ordering ourselves. 61 | // 62 | // So after a crash, we would query the DB for all unaccounted for impressions, 63 | // order them and include them into the channel history, then get all messages 64 | // that have yet to be ACKed, and fire them off to the supply. 65 | // 66 | // Supply needs a way of dealing with duplicate messages (should be 67 | // idempotent). Basically, they should ignore duplicates. 68 | 69 | // So we have one table w/ non blockchain data about impressions / price 70 | // Need a query for that, but just for startup / cron job 71 | // 72 | // And then another table with all the channel data. New saves only after ACK. 73 | // 74 | // I DONT WANT TO USE SQL, because even if it does serve as an effective hack 75 | // for this project, it will 1. slow down development 2. not be user friendly 76 | // 3. I can iterate on it later, easier if I already know. Same as implementing 77 | // merkle trree in JS first. From here forth, SQL is only for fun. 78 | 79 | function deliverImpression (supplyId, impressionId) { 80 | // 81 | } 82 | 83 | function ackReceived (supplyId, impressionId) { 84 | // 85 | } 86 | 87 | // Demand and supply both call "recordImpressionServed", the difference is what 88 | // each do after. Demand needs to message supply. Supply needs to ACK to 89 | // demand. Supply is done after sending the ACK. Demand will wait until it has 90 | // recevied the message from Supply to change the channel status from pending 91 | // to ackReceived. 92 | // 93 | // Demand will keep the most recent ACK from Supply. 94 | // 95 | // How to deal with supply getting message out of order? 96 | // Save the message, notice the gap. Either cron / wait then request that data 97 | -------------------------------------------------------------------------------- /js/servers/admarket.js: -------------------------------------------------------------------------------- 1 | // servers/admarket.js 2 | // 3 | // Receieves streams of beacon data. 4 | 5 | import { createStore } from 'redux' 6 | import { combineReducers } from 'redux-immutable' 7 | import { List } from 'immutable' 8 | import { admarketChannelsReducer } from '../reducers' 9 | import { amImpDB as impressionDB, amChDB as channelDB } from '../storage' 10 | import { makeChannel, makeUpdate, sign } from '../channel' 11 | import config from '../config' 12 | import Promise from 'bluebird' 13 | import Web3 from 'web3' 14 | 15 | const web3 = new Web3() 16 | const sha3 = web3.sha3 17 | 18 | const p = Promise.promisify 19 | 20 | const privKey = new Buffer(config.adMarket.privKey, 'hex') 21 | 22 | const store = createStore(admarketChannelsReducer) 23 | const dispatch = store.dispatch 24 | 25 | const CHANNEL_ID = web3.sha3('foo') 26 | 27 | const channel = { 28 | contractId: '0x12345123451234512345', 29 | channelId: CHANNEL_ID, 30 | demand: '0x11111111111111111111', 31 | supply: '0x22222222222222222222', 32 | impressionId: 'foo', 33 | price: 1, 34 | impressions: 0, 35 | root: web3.sha3('0'), 36 | balance: 0, 37 | state: 0, 38 | expiration: 100, 39 | challengeTimeout: 100, 40 | proposedRoot: 0, 41 | pendingUpdates: List([]) 42 | } 43 | 44 | var express = require('express') 45 | var bodyParser = require('body-parser') 46 | var app = express() 47 | 48 | app.use(bodyParser.json()) 49 | 50 | let IS_OPEN = false 51 | 52 | // This is a hack to initialize the channel in storage before receiving 53 | // impressions 54 | // This will have to change 55 | app.get('/open', async function (req, res) { 56 | IS_OPEN = true 57 | await p(channelDB.remove.bind(channelDB))({}, { multi: true }) 58 | await p(impressionDB.remove.bind(impressionDB))({}, { multi: true }) 59 | await p(channelDB.insert.bind(channelDB))(channel) 60 | dispatch({ type: 'CHANNEL_OPENED', payload: channel }) 61 | // console.log(await p(channelDB.find.bind(channelDB))({ channelId: CHANNEL_ID})) 62 | res.sendStatus(200) 63 | }) 64 | 65 | app.post('/channel_update', async function (req, res) { 66 | const { impression, update } = req.body 67 | 68 | // TODO Before we dispatch, verify the inputs. 69 | 70 | // TODO If impression doesn't exist in DB, save it. (for now just save) 71 | await p(impressionDB.insert.bind(impressionDB))(impression) 72 | 73 | // How can we tell if the impression has already been received? 74 | // It should exist in the DB, and also be in the pendingImpression queue. 75 | // What if there is a race condition? The channel_update is received during 76 | // the processing of the impression event. We could check both conditions 77 | // separately. If it isn't in the database, save it. If it is in the 78 | // pendingImpressions queue, remove it. 79 | // 80 | // There is no reason to fire an impressionServed event if we are receiving 81 | // the channel_update with the impression before the actual impression event. 82 | 83 | dispatch({ type: 'CHANNEL_UPDATE', payload: update }) 84 | 85 | const channelState = store.getState().toJS()[0] 86 | 87 | console.log('\nChannel Update Received\n') 88 | console.log(formatState(channelState)) 89 | 90 | await p(channelDB.update.bind(channelDB))( 91 | { channelId: CHANNEL_ID }, 92 | channelState, 93 | { multi: true } 94 | ) 95 | 96 | res.sendStatus(200) 97 | }) 98 | 99 | app.get('/request_signature', async function (req, res) { 100 | // Just need the impression Id? 101 | const impressions = req.body 102 | 103 | console.log('Signature requested for impressions:\n') 104 | console.log(impressions) 105 | 106 | const savedImpressions = await p(impressionDB.find.bind(impressionDB))({ 107 | impressionId: { $in: impressions.map(({ impressionId }) => impressionId) } 108 | }) 109 | 110 | // needs to return signed impressions, each signed individually 111 | // [ { impressionId, signature } ... ] 112 | if (savedImpressions && savedImpressions.length) { 113 | console.log('Impression found') 114 | 115 | const signedImpressions = savedImpressions.map(impression => { 116 | impression.signature = sign(sha3(impression.impressionId), privKey) 117 | delete impression._id 118 | return impression 119 | }) 120 | 121 | res.json(signedImpressions) 122 | 123 | } else { 124 | console.log('Impression not found') 125 | res.json([]) 126 | } 127 | }) 128 | 129 | app.post('/', async function (req, res) { 130 | // The impression could be received before or after the channel_update. 131 | // Most likely it will be before, in which case we saved the impression and 132 | // add it the the pendingImpressions queue. 133 | // If it arrives after, the impression will have already both been saved and 134 | // the channel updated, so there is no reason to do anything. 135 | // There is a chance the impression was received as part of the channelUpdate 136 | // but out of order, so it is saved but still in the pendingUpdates queue. 137 | 138 | const impression = req.body 139 | 140 | console.log('\nImpression Received:\n') 141 | console.log(impression) 142 | 143 | // TODO If impression doesn't exist in DB, save it. (for now just save) 144 | await p(impressionDB.insert.bind(impressionDB))(impression) 145 | 146 | res.sendStatus(200) 147 | }) 148 | 149 | app.listen(3002, function () { 150 | console.log('listening on 3002') 151 | }) 152 | 153 | function formatState(state) { 154 | return { 155 | price: state.price, 156 | impressionId: state.impressionId, 157 | balance: state.balance, 158 | impressions: state.impressions, 159 | prevRoot: state.prevRoot, 160 | root: state.root, 161 | signature: state.signature 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /js/channel.js: -------------------------------------------------------------------------------- 1 | import leftPad from 'left-pad' 2 | import Web3 from 'web3' 3 | import ethUtils from 'ethereumjs-util' 4 | import { merkleRoot } from 'merkle-tree-solidity' 5 | import { Map } from 'immutable' 6 | 7 | const web3 = new Web3() 8 | const sha3 = ethUtils.sha3 9 | 10 | // TODO 11 | // get private key from config file or unlock account? 12 | // This server is intended to run on its own for a long period. 13 | // private key can be in memory, part of env (typed in to start server) or 14 | // file. 15 | const privKey = Buffer.alloc(32, 'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109', 'hex') 16 | 17 | // TODO separate util functions 18 | // channel specific 19 | // eth specific (needs web3) 20 | // general, about eth (doesn't need web3) 21 | // general, not about eth 22 | 23 | const makeChannel = (channelObj, impressionObj) => { 24 | impressionObj = impressionObj || { 25 | impressionId: 0, 26 | price: 0, 27 | impressions: 0, 28 | balance: 0, 29 | prevRoot: 0 30 | } 31 | return Map({ ...channelObj, ...impressionObj }) 32 | } 33 | 34 | const sign = (msgHash, privKey) => { 35 | if (typeof msgHash === 'string' && msgHash.slice(0, 2) === '0x') { 36 | msgHash = Buffer.alloc(32, msgHash.slice(2), 'hex') 37 | } 38 | const sig = ethUtils.ecsign(msgHash, privKey) 39 | return `0x${sig.r.toString('hex')}${sig.s.toString('hex')}${sig.v.toString(16)}` 40 | } 41 | 42 | const parseChannel = (channel) => { 43 | return Object.assign({}, channel, { 44 | state: +channel.state.toString(), 45 | expiration: +channel.expiration.toString(), 46 | challengeTimeout: +channel.challengeTimeout.toString() 47 | }) 48 | } 49 | 50 | const parseChallenge = (challenge) => { 51 | return Object.assign({}, challenge, { 52 | impressions: +challenge.impressions.toString() 53 | }) 54 | } 55 | 56 | const getFingerprint = (channel) => { 57 | if (typeof channel.toJS === 'function') { 58 | channel = channel.toJS() 59 | } 60 | return solSha3( 61 | channel.contractId, 62 | channel.channelId, 63 | channel.demand, 64 | channel.supply, 65 | channel.root 66 | ) 67 | } 68 | 69 | const hashLeaf = (leaf) => { 70 | if (typeof leaf === 'number') { 71 | return Buffer.alloc(32, solSha3(leaf).slice(2), 'hex') 72 | } 73 | return sha3(leaf) 74 | } 75 | 76 | const getLeaves = (channel, prevRoot) => { 77 | if (typeof channel.toJS === 'function') { 78 | channel = channel.toJS() 79 | } 80 | return [ 81 | channel.impressionId, 82 | channel.price, 83 | channel.impressions, 84 | channel.balance, 85 | prevRoot 86 | ].map(hashLeaf) 87 | } 88 | 89 | const getRoot = (channel, prevRoot) => { 90 | return '0x' + merkleRoot(getLeaves(channel, prevRoot), true).toString('hex') 91 | } 92 | 93 | const verifySignature = (channel, sig, address) => { 94 | const fingerprint = getFingerprint(channel) 95 | return ecrecover(fingerprint, sig) === address 96 | } 97 | 98 | // TODO make a real implementation of solSha3 in JS which captures all 99 | // complexity (expanding arrays, recursion, uint sizes, etc...) 100 | // https://github.com/raineorshine/solidity-sha3/blob/master/src/index.js 101 | // http://ethereum.stackexchange.com/questions/2632/how-does-soliditys-sha3-keccak256-hash-uints 102 | const solSha3 = (...args) => { 103 | args = args.map(arg => { 104 | if (typeof arg === 'string') { 105 | if (arg.substring(0, 2) === '0x') { 106 | return arg.slice(2) 107 | } else { 108 | return web3.toHex(arg).slice(2) 109 | } 110 | } 111 | 112 | if (typeof arg === 'number') { 113 | return leftPad((arg).toString(16), 64, 0) 114 | } 115 | }) 116 | 117 | args = args.join('') 118 | 119 | return web3.sha3(args, { encoding: 'hex' }) 120 | } 121 | 122 | const isValidUpdate = (update) => { 123 | return typeof update.price === 'number' && update.price > 0 && 124 | typeof update.impressionId === 'string' 125 | } 126 | 127 | const makeUpdate = (channel, update, doSign) => { 128 | if (!isValidUpdate(update)) { throw new Error('Invalid Update') } 129 | // Assume impressionId and price are set on update 130 | update.impressions = channel.get('impressions') + 1 131 | update.balance = channel.get('balance') + update.price 132 | update.root = getRoot(update, channel.get('root')) 133 | update.prevRoot = channel.get('root') 134 | if (doSign) { update.signature = sign(getFingerprint(update), privKey) } 135 | return channel.merge(update) 136 | } 137 | 138 | // Different ways a channel gets constructed: 139 | // 1. Built entirely from offchain state, then deployed 140 | // - this is wrong -> there are only deployed channels 141 | // - deployed channels can *also* have offchain state extending onchain 142 | // - deployed channels can *also* need to extend offchain state with onchain 143 | // I have to think through state and storage now. 144 | 145 | const verifyUpdate = (channel, update) => { 146 | return channel.get('contractId') === update.contractId && 147 | channel.get('channelId') === update.channelId && 148 | channel.get('demand') === update.demand && 149 | channel.get('supply') === update.supply && 150 | channel.get('impressions') === (update.impressions - 1) && 151 | channel.get('balance') === (update.balance - update.price) && 152 | update.root === getRoot(update, channel.get('root')) 153 | } 154 | 155 | // converts a 0x[10 0's][address] -> 0x[address] 156 | const parseLogAddress = (logAddress) => { 157 | return '0x' + logAddress.slice(26) 158 | } 159 | 160 | // only use on bigNumbers that I know are actually small 161 | const parseBN = (bigNumber) => { 162 | return +bigNumber.toString() 163 | } 164 | 165 | const ecrecover = (msg, sig) => { 166 | const r = ethUtils.toBuffer(sig.slice(0, 66)) 167 | const s = ethUtils.toBuffer('0x' + sig.slice(66, 130)) 168 | const v = 27 + parseInt(sig.slice(130, 132)) 169 | const m = ethUtils.toBuffer(msg) 170 | const pub = ethUtils.ecrecover(m, v, r, s) 171 | return '0x' + ethUtils.pubToAddress(pub).toString('hex') 172 | } 173 | 174 | export { parseChannel, getFingerprint, getLeaves, getRoot, solSha3, parseLogAddress, 175 | verifySignature, makeUpdate, verifyUpdate, parseBN, makeChannel, sign, ecrecover, 176 | parseChallenge 177 | } 178 | 179 | /* 180 | * Punt on validation - data coming from blockchain is assumed to be valid 181 | * 182 | function isValidState(state) { 183 | return isValidAddress(state.contractId) && 184 | isValidAddress(state.demand) && 185 | isValidAddress(state.supply) && 186 | isValidBytes32(state.channelId) && 187 | isValidBytes32(state.root) 188 | } 189 | 190 | function isValidBytes32(bytes32) { 191 | return Buffer(bytes32.slice(2), 'hex').length === 32 192 | } 193 | 194 | function isValidAddress(address) { 195 | return typeof address === 'string' && address.length === '22' && 196 | address.slice(0, 2) === '0x' && Buffer(address.slice(2), 'hex').length === 20 197 | } 198 | */ 199 | -------------------------------------------------------------------------------- /js/servers/demand.js: -------------------------------------------------------------------------------- 1 | // servers/demand.js 2 | // 3 | // Receives impression beacon events from the browser. 4 | // Receives impression clearing requests from supply. 5 | 6 | import request from 'request' 7 | import { createStore } from 'redux' 8 | import { combineReducers } from 'redux-immutable' 9 | import Promise from 'bluebird' 10 | import Web3 from 'web3' 11 | import { channelsReducer } from '../reducers' 12 | import { impressionDB, channelDB } from '../storage' 13 | import { makeChannel, makeUpdate } from '../channel' 14 | 15 | const web3 = new Web3() 16 | 17 | const p = Promise.promisify 18 | 19 | const store = createStore(channelsReducer) 20 | const dispatch = store.dispatch 21 | 22 | const CHANNEL_ID = web3.sha3('foo') 23 | 24 | const channel = { 25 | contractId: '0x12345123451234512345', 26 | channelId: CHANNEL_ID, 27 | demand: '0x11111111111111111111', 28 | supply: '0x22222222222222222222', 29 | impressionId: 'foo', 30 | price: 1, 31 | impressions: 0, 32 | root: web3.sha3('0'), 33 | balance: 0, 34 | state: 0, 35 | expiration: 100, 36 | challengeTimeout: 100, 37 | proposedRoot: 0 38 | } 39 | 40 | // Only need to track the most recent state 41 | // need to track the signature! 42 | // So I want to find the channel in storage and update it. 43 | 44 | var express = require('express') 45 | var bodyParser = require('body-parser') 46 | var app = express() 47 | 48 | app.use(bodyParser.json()) 49 | 50 | // This is a hack to initialize the channel in storage before receiving 51 | // impressions 52 | // This will have to change 53 | app.get('/open', async function (req, res) { 54 | await p(channelDB.remove.bind(channelDB))({}, { multi: true }) 55 | await p(impressionDB.remove.bind(impressionDB))({}, { multi: true }) 56 | await p(channelDB.insert.bind(channelDB))(channel) 57 | dispatch({ type: 'CHANNEL_OPENED', payload: channel }) 58 | // console.log(await p(channelDB.find.bind(channelDB))({ channelId: CHANNEL_ID})) 59 | res.sendStatus(200) 60 | }) 61 | 62 | app.get('/verify', async function (req, res) { 63 | // implement an endpoint which queries all existing data and verifies it 64 | // in practice, this will be used to validate an impression chain starting 65 | // from some checkpointed state. This will require us to query all data for 66 | // the channel, sort it, and then do the sequence of hashing to see if it 67 | // produces the same root. req.root should be the root we are checking 68 | // against, and req.from should be the impression # to start, and req.end 69 | // should be the impression # to end at. 70 | // 71 | // req: { supplyId, demandId, root, from, to } 72 | // const saved = await p(impressionDB.find.bind(impressionDB))({ supplyId: req.supplyId, demandId: req.demandId}) 73 | 74 | const saved = await p(impressionDB.find.bind(impressionDB))({ supplyId: req.body.supplyId, demandId: req.body.demandId }) 75 | // NOTE - query responses are not ordered 76 | saved.sort((a, b) => { 77 | return a.impressionId - b.impressionId 78 | }) 79 | 80 | const newChannel = makeChannel(channel) 81 | 82 | const final = newChannel.withMutations(channel => { 83 | saved.reduce((channel, impression) => { 84 | return makeUpdate(channel, impression) 85 | }, channel) 86 | }) 87 | 88 | res.sendStatus(200) 89 | }) 90 | 91 | app.post('/request_update', async function (req, res) { 92 | const impression = req.body 93 | 94 | console.log('Channel Update Requested') 95 | 96 | // TODO verify the signature from the admarket... 97 | if (impression.signature) { 98 | console.log('AdMarket signature verified') 99 | 100 | await p(impressionDB.insert.bind(impressionDB))(impression) 101 | 102 | // TODO Before we dispatch, verify the inputs. 103 | // new channel states are signed within the reducer 104 | // TODO add flag to skip sigining? 105 | // supply will need to call roughly the same function but without signing 106 | 107 | dispatch({ type: 'IMPRESSION_SERVED', payload: impression }) 108 | 109 | // console.log(store.getState().get(0)) 110 | const channelState = store.getState().toJS()[0] 111 | 112 | await p(channelDB.update.bind(channelDB))( 113 | { channelId: CHANNEL_ID }, 114 | channelState, 115 | { multi: true } 116 | ) 117 | 118 | // no timeout for impressions=2, 100ms timeout for impressions=1 119 | // const timeout = channelState.impressions == 1 ? 100 : 0 120 | const timeout = 0 121 | 122 | console.log('\nChannel Update Sent\n') 123 | console.log(formatState(channelState)) 124 | 125 | request.post({ url: 'http://localhost:3001/channel_update', body: { impression, update: channelState }, json: true}, function () {}) 126 | request.post({ url: 'http://localhost:3002/channel_update', body: { impression, update: channelState }, json: true}, function () {}) 127 | 128 | res.sendStatus(200) 129 | 130 | } else { 131 | console.log('AdMarket ') 132 | res.json([]) 133 | } 134 | 135 | 136 | }) 137 | 138 | app.post('/', async function (req, res) { 139 | const impression = req.body 140 | 141 | console.log('\nImpression Received:\n') 142 | console.log(impression) 143 | 144 | await p(impressionDB.insert.bind(impressionDB))(impression) 145 | 146 | // TODO Before we dispatch, verify the inputs. 147 | // new channel states are signed within the reducer 148 | // TODO add flag to skip sigining? 149 | // supply will need to call roughly the same function but without signing 150 | 151 | dispatch({ type: 'IMPRESSION_SERVED', payload: impression }) 152 | 153 | // console.log(store.getState().get(0)) 154 | const channelState = store.getState().toJS()[0] 155 | 156 | await p(channelDB.update.bind(channelDB))( 157 | { channelId: CHANNEL_ID }, 158 | channelState, 159 | { multi: true } 160 | ) 161 | 162 | // no timeout for impressions=2, 100ms timeout for impressions=1 163 | // const timeout = channelState.impressions == 1 ? 100 : 0 164 | const timeout = 0 165 | 166 | console.log('\nChannel Update Sent\n') 167 | console.log(formatState(channelState)) 168 | 169 | setTimeout(function () { 170 | if (channelState.impressions == 1) { 171 | request.post({ url: 'http://localhost:3001/channel_update', body: { impression, update: channelState }, json: true}, function () {}) 172 | request.post({ url: 'http://localhost:3002/channel_update', body: { impression, update: channelState }, json: true}, function () {}) 173 | } 174 | }, timeout) 175 | 176 | // const saved = await p(channelDB.find.bind(channelDB))({ channelId: CHANNEL_ID}) 177 | 178 | res.sendStatus(200) 179 | }) 180 | 181 | app.get('/state', function (req, res) { 182 | res.json(store.getState()) 183 | }) 184 | 185 | app.listen(3000, function () { 186 | console.log('listening on 3000') 187 | }) 188 | 189 | function formatState(state) { 190 | return { 191 | price: state.price, 192 | impressionId: state.impressionId, 193 | balance: state.balance, 194 | impressions: state.impressions, 195 | prevRoot: state.prevRoot, 196 | root: state.root, 197 | signature: state.signature 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /js/servers/supply.js: -------------------------------------------------------------------------------- 1 | // servers/supply.js 2 | // 3 | // Receives impression beacon events from the browser. 4 | // Receives impression acknowledgments from demand partners. 5 | 6 | import { createStore } from 'redux' 7 | import { combineReducers } from 'redux-immutable' 8 | import { List } from 'immutable' 9 | import request from 'request' 10 | import { supplyChannelsReducer } from '../reducers' 11 | import { supImpDB as impressionDB, supChDB as channelDB } from '../storage' 12 | import config from '../config' 13 | import { makeChannel, makeUpdate, ecrecover } from '../channel' 14 | import Promise from 'bluebird' 15 | import Web3 from 'web3' 16 | 17 | const web3 = new Web3() 18 | 19 | const p = Promise.promisify 20 | 21 | const store = createStore(supplyChannelsReducer) 22 | const dispatch = store.dispatch 23 | 24 | const CHANNEL_ID = web3.sha3('foo') 25 | 26 | const channel = { 27 | contractId: '0x12345123451234512345', 28 | channelId: CHANNEL_ID, 29 | demand: '0x11111111111111111111', 30 | supply: '0x22222222222222222222', 31 | impressionId: 'foo', 32 | price: 1, 33 | impressions: 0, 34 | root: web3.sha3('0'), 35 | balance: 0, 36 | state: 0, 37 | expiration: 100, 38 | challengeTimeout: 100, 39 | proposedRoot: 0, 40 | pendingImpressions: List([]), 41 | pendingUpdates: List([]), 42 | pendingUpdateRequests: List([]) 43 | } 44 | 45 | var express = require('express') 46 | var bodyParser = require('body-parser') 47 | var app = express() 48 | 49 | app.use(bodyParser.json()) 50 | 51 | let IS_OPEN = false 52 | 53 | // This is a hack to initialize the channel in storage before receiving 54 | // impressions 55 | // This will have to change 56 | app.get('/open', async function (req, res) { 57 | IS_OPEN = true 58 | await p(channelDB.remove.bind(channelDB))({}, { multi: true }) 59 | await p(impressionDB.remove.bind(impressionDB))({}, { multi: true }) 60 | await p(channelDB.insert.bind(channelDB))(channel) 61 | dispatch({ type: 'CHANNEL_OPENED', payload: channel }) 62 | // console.log(await p(channelDB.find.bind(channelDB))({ channelId: CHANNEL_ID})) 63 | res.sendStatus(200) 64 | }) 65 | 66 | app.get('/verify', async function (req, res) { 67 | // implement an endpoint which queries all existing data and verifies it 68 | // in practice, this will be used to validate an impression chain starting 69 | // from some checkpointed state. This will require us to query all data for 70 | // the channel, sort it, and then do the sequence of hashing to see if it 71 | // produces the same root. req.root should be the root we are checking 72 | // against, and req.from should be the impression # to start, and req.end 73 | // should be the impression # to end at. 74 | // 75 | // req: { supplyId, demandId, root, from, to } 76 | // const saved = await p(impressionDB.find.bind(impressionDB))({ supplyId: req.supplyId, demandId: req.demandId}) 77 | 78 | const saved = await p(impressionDB.find.bind(impressionDB))({ supplyId: req.body.supplyId, demandId: req.body.demandId }) 79 | // NOTE - query responses are not ordered 80 | saved.sort((a, b) => { 81 | return a.impressionId - b.impressionId 82 | }) 83 | 84 | const newChannel = makeChannel(channel) 85 | 86 | const final = newChannel.withMutations(channel => { 87 | saved.reduce((channel, impression) => { 88 | return makeUpdate(channel, impression) 89 | }, channel) 90 | }) 91 | 92 | res.sendStatus(200) 93 | }) 94 | 95 | app.post('/channel_update', async function (req, res) { 96 | const { impression, update } = req.body 97 | 98 | // TODO Before we dispatch, verify the inputs. 99 | 100 | // TODO If impression doesn't exist in DB, save it. (for now just save) 101 | await p(impressionDB.insert.bind(impressionDB))(impression) 102 | 103 | // How can we tell if the impression has already been received? 104 | // It should exist in the DB, and also be in the pendingImpression queue. 105 | // What if there is a race condition? The channel_update is received during 106 | // the processing of the impression event. We could check both conditions 107 | // separately. If it isn't in the database, save it. If it is in the 108 | // pendingImpressions queue, remove it. 109 | // 110 | // There is no reason to fire an impressionServed event if we are receiving 111 | // the channel_update with the impression before the actual impression event. 112 | 113 | dispatch({ type: 'CHANNEL_UPDATE', payload: update }) 114 | 115 | const channelState = store.getState().toJS()[0] 116 | 117 | console.log('\nChannel Update Received\n') 118 | console.log(formatState(channelState)) 119 | 120 | await p(channelDB.update.bind(channelDB))( 121 | { channelId: CHANNEL_ID }, 122 | channelState, 123 | { multi: true } 124 | ) 125 | 126 | res.sendStatus(200) 127 | }) 128 | 129 | app.post('/', async function (req, res) { 130 | // The impression could be received before or after the channel_update. 131 | // Most likely it will be before, in which case we saved the impression and 132 | // add it the the pendingImpressions queue. 133 | // If it arrives after, the impression will have already both been saved and 134 | // the channel updated, so there is no reason to do anything. 135 | // There is a chance the impression was received as part of the channelUpdate 136 | // but out of order, so it is saved but still in the pendingUpdates queue. 137 | 138 | const impression = req.body 139 | 140 | console.log('\nImpression Received:\n') 141 | console.log(impression) 142 | 143 | // TODO If impression doesn't exist in DB, save it. (for now just save) 144 | await p(impressionDB.insert.bind(impressionDB))(impression) 145 | 146 | // TODO Before we dispatch, verify the inputs. 147 | 148 | dispatch({ type: 'IMPRESSION_SERVED', 149 | payload: { 150 | demandId: impression.demandId, 151 | supplyId: impression.supplyId, 152 | impressionId: impression.impressionId, 153 | price: impression.price, 154 | time: impression.time 155 | }}) 156 | 157 | // console.log(store.getState().get(0)) 158 | 159 | const channelState = store.getState().toJS()[0] 160 | 161 | // console.log('\nIMPRESSION_SERVED - CHANNEL STATE\n') 162 | // console.log(channelState) 163 | 164 | await p(channelDB.update.bind(channelDB))( 165 | { channelId: CHANNEL_ID }, 166 | channelState, 167 | { multi: true } 168 | ) 169 | 170 | // const saved = await p(channelDB.find.bind(channelDB))({ channelId: CHANNEL_ID}) 171 | // const sig = saved[0].signature 172 | 173 | res.sendStatus(200) 174 | }) 175 | 176 | app.get('/state', function (req, res) { 177 | res.json(store.getState()) 178 | }) 179 | 180 | app.listen(3001, function () { 181 | console.log('listening on 3001') 182 | }) 183 | 184 | function requestSignatures (impressionIds, cb) { 185 | request.get({ url: 'http://localhost:3002/request_signature', body: impressionIds, json: true }, function (err, res, body) { 186 | if (err) { throw err } 187 | const signedImpressions = body 188 | 189 | console.log('Response from AdMarket') 190 | console.log(signedImpressions) 191 | 192 | // filter out invalid impressions, only dispatch the ones that succeeded 193 | // retry the failed impressions? need some limit or timeout. 194 | // if the demand is unresponsive, we close the channel 195 | // if the adMarket is unresponsive, we should do the same 196 | 197 | // TODO verify signature before saving it 198 | // TODO create a keypair for each participant for testing 199 | // TODO save signature to a new database 200 | 201 | if (signedImpressions && signedImpressions.length) { 202 | 203 | const validSignedImpressions = signedImpressions.filter(impression => { 204 | // TODO get the address from the channel 205 | // should this just be part of the reducer? 206 | // 1. pass the bundle of impressionIds 207 | return ecrecover(web3.sha3(impression.impressionId), impression.signature) == config.adMarket.address 208 | }) 209 | 210 | console.log('Signature received from AdMarket:\n') 211 | console.log(signedImpressions) 212 | 213 | dispatch({ type: 'SIGNATURES_RECEIVED', payload: validSignedImpressions }) 214 | 215 | request.post({ url: 'http://localhost:3000/request_update', body: validSignedImpressions[0], json: true }, function (err, res, body) { 216 | console.log('Response from Demand received') 217 | }) 218 | 219 | // Impression was not found 220 | } else if (signedImpressions && signedImpressions.length == 0) { 221 | console.log('Impression not found') 222 | dispatch({ type: 'IMPRESSION_NOT_FOUND', payload: impressionIds }) 223 | cb() 224 | } 225 | }) 226 | } 227 | 228 | // long running process which queries the AdMarket for pendingImpressions 229 | // looping over all pending impressions seems simpler than putting setTimeouts 230 | // for each impressions 231 | function loopPendingImpressions (timeout) { 232 | setTimeout(function () { 233 | if (IS_OPEN) { 234 | const now = new Date() / 1000 235 | const pending = store.getState().get(0).get('pendingImpressions').filter(impression => { 236 | return now - impression.time > 2 237 | }).toJS() 238 | 239 | if (pending.length) { 240 | console.log(pending) 241 | console.log('Requesting signatures from AdMarket') 242 | requestSignatures(pending, function (err) { 243 | if (err) { throw err } 244 | loopPendingImpressions(timeout) 245 | }) 246 | } else { 247 | loopPendingImpressions(timeout) 248 | } 249 | 250 | } else { 251 | loopPendingImpressions(timeout) 252 | } 253 | }, timeout) 254 | } 255 | 256 | loopPendingImpressions(2000) 257 | 258 | function formatState(state) { 259 | return { 260 | price: state.price, 261 | impressionId: state.impressionId, 262 | balance: state.balance, 263 | impressions: state.impressions, 264 | prevRoot: state.prevRoot, 265 | root: state.root, 266 | signature: state.signature 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /js/__tests__/admarket.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import p from 'es6-promisify' 3 | import Web3 from 'web3' 4 | import MerkleTree, { checkProof, merkleRoot } from 'merkle-tree-solidity' 5 | import { sha3 } from 'ethereumjs-util' 6 | import setup from '../setup' 7 | import { parseChannel, getFingerprint, getRoot, solSha3, parseLogAddress, 8 | verifySignature, makeUpdate, verifyUpdate, parseBN } from '../channel' 9 | import { wait } from '../utils' 10 | 11 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000 12 | 13 | const web3 = new Web3() 14 | 15 | // goal - usable node.js middleware library for impression tracking 16 | // ACF will be registrar first and operate adMarket first 17 | // need to test out what auditing looks like 18 | // reference implementation. 2 weeks till completion. 19 | // offchain storage combines with this. 20 | // need to manage state machine between both nodes, interaction with adMarket 21 | 22 | describe('AdMarket', async () => { 23 | 24 | let adMarket, eth, accounts, web3 25 | let snapshotId, filter 26 | 27 | beforeAll(async () => { 28 | let result = await setup({ testRPCProvider: 'http://localhost:8545'}) 29 | adMarket = result.adMarket 30 | eth = result.eth 31 | accounts = result.accounts 32 | web3 = result.web3 33 | }) 34 | 35 | beforeEach(async () => { 36 | let res = await p(web3.currentProvider.sendAsync.bind(web3.currentProvider))({ 37 | jsonrpc: '2.0', 38 | method: 'evm_snapshot', 39 | id: new Date().getTime() 40 | }) 41 | snapshotId = res.result 42 | filter = web3.eth.filter({ address: adMarket.address, fromBlock: 0 }) 43 | }) 44 | 45 | afterEach(async () => { 46 | await p(filter.stopWatching.bind(filter))() 47 | await p(web3.currentProvider.sendAsync.bind(web3.currentProvider))({ 48 | jsonrpc: '2.0', 49 | method: 'evm_revert', 50 | params: [snapshotId], 51 | id: new Date().getTime() 52 | }) 53 | }) 54 | 55 | it('setup', async () => { 56 | // channelCount should start at 0 57 | const channelCount = await adMarket.channelCount() 58 | assert.equal(+channelCount[0].toString(), 0) 59 | }) 60 | 61 | it('registerDemand', async () => { 62 | const demand = accounts[1] 63 | const url = 'foo' 64 | await adMarket.registerDemand(demand, url) 65 | const result = await adMarket.registeredDemand(demand) 66 | assert.equal(result[0], url) 67 | 68 | const logs = await p(filter.get.bind(filter))() 69 | const logAddress = parseLogAddress(logs[0].topics[1]) 70 | assert.equal(logAddress, demand) 71 | }) 72 | 73 | it('registerSupply', async () => { 74 | const supply = accounts[1] 75 | const url = 'foo' 76 | await adMarket.registerSupply(supply, url) 77 | const result = await adMarket.registeredSupply(supply) 78 | assert.equal(result[0], url) 79 | 80 | const logs = await p(filter.get.bind(filter))() 81 | const logAddress = parseLogAddress(logs[0].topics[1]) 82 | assert.equal(logAddress, supply) 83 | }) 84 | 85 | it('deregisterDemand', async () => { 86 | const demand = accounts[1] 87 | const url = 'foo' 88 | await adMarket.registerDemand(demand, url) 89 | await adMarket.deregisterDemand(demand) 90 | const result = await adMarket.registeredDemand(demand) 91 | assert.equal(result[0], '') 92 | 93 | const logs = await p(filter.get.bind(filter))() 94 | const logAddress = parseLogAddress(logs[1].topics[1]) 95 | assert.equal(logAddress, demand) 96 | }) 97 | 98 | it('deregisterSupply', async () => { 99 | const supply = accounts[1] 100 | const url = 'foo' 101 | await adMarket.registerSupply(supply, url) 102 | await adMarket.deregisterSupply(supply) 103 | const result = await adMarket.registeredSupply(supply) 104 | assert.equal(result[0], '') 105 | 106 | const logs = await p(filter.get.bind(filter))() 107 | const logAddress = parseLogAddress(logs[1].topics[1]) 108 | assert.equal(logAddress, supply) 109 | }) 110 | 111 | it('updateDemandUrl', async () => { 112 | const demand = accounts[1] 113 | const url = 'foo' 114 | await adMarket.registerDemand(demand, url) 115 | await adMarket.updateDemandUrl('bar', { from: demand }) 116 | const result = await adMarket.registeredDemand(demand) 117 | assert.equal(result[0], 'bar') 118 | 119 | const logs = await p(filter.get.bind(filter))() 120 | const logAddress = parseLogAddress(logs[1].topics[1]) 121 | assert.equal(logAddress, demand) 122 | }) 123 | 124 | it('updateSupplyUrl', async () => { 125 | const supply = accounts[1] 126 | const url = 'foo' 127 | await adMarket.registerSupply(supply, url) 128 | await adMarket.updateSupplyUrl('bar', { from: supply }) 129 | const result = await adMarket.registeredSupply(supply) 130 | assert.equal(result[0], 'bar') 131 | 132 | const logs = await p(filter.get.bind(filter))() 133 | const logAddress = parseLogAddress(logs[1].topics[1]) 134 | assert.equal(logAddress, supply) 135 | }) 136 | 137 | it('openChannel', async () => { 138 | const demand = accounts[1] 139 | const supply = accounts[2] 140 | const demandUrl = 'foo' 141 | const supplyUrl = 'bar' 142 | const channelId = solSha3(0) 143 | await adMarket.registerDemand(demand, demandUrl) 144 | await adMarket.registerSupply(supply, supplyUrl) 145 | 146 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 147 | const channelTimeout = parseBN((await p(adMarket.channelTimeout)())[0]) 148 | const expiration = blockNumber + channelTimeout + 1 149 | 150 | await adMarket.openChannel(supply, { from: demand }) 151 | 152 | const channel = parseChannel(await adMarket.getChannel(channelId)) 153 | 154 | assert.equal(channel.contractId, adMarket.address) 155 | assert.equal(channel.channelId, channelId) 156 | assert.equal(channel.demand, demand) 157 | assert.equal(channel.supply, supply) 158 | assert.equal(parseInt(channel.root, 16), 0) 159 | assert.equal(channel.state, 0) 160 | assert.equal(channel.expiration, expiration) 161 | assert.equal(channel.challengeTimeout, 0) 162 | assert.equal(parseInt(channel.proposedRoot, 16), 0) 163 | }) 164 | 165 | it('proposeCheckpointChannel', async () => { 166 | const demand = accounts[1] 167 | const supply = accounts[2] 168 | const demandUrl = 'foo' 169 | const supplyUrl = 'bar' 170 | const channelId = solSha3(0) 171 | await adMarket.registerDemand(demand, demandUrl) 172 | await adMarket.registerSupply(supply, supplyUrl) 173 | await adMarket.openChannel(supply, { from: demand }) 174 | const channel = parseChannel(await adMarket.getChannel(channelId)) 175 | 176 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 177 | const challengePeriod = parseBN((await p(adMarket.challengePeriod)())[0]) 178 | const challengeTimeout = blockNumber + challengePeriod + 1 179 | 180 | const proposedRoot = solSha3('wut') 181 | channel.root = proposedRoot 182 | const fingerprint = getFingerprint(channel) 183 | const sig = await p(web3.eth.sign)(demand, fingerprint) 184 | await adMarket.proposeCheckpointChannel( 185 | channelId, proposedRoot, sig, { from: demand } 186 | ) 187 | 188 | const updatedChannel = parseChannel(await adMarket.getChannel(channelId)) 189 | assert.equal(updatedChannel.state, 1) 190 | assert.equal(updatedChannel.challengeTimeout, challengeTimeout) 191 | assert.equal(updatedChannel.proposedRoot, proposedRoot) 192 | }) 193 | 194 | xit('challengeCheckpointChannel', async () => { 195 | const demand = accounts[1] 196 | const supply = accounts[2] 197 | const demandUrl = 'foo' 198 | const supplyUrl = 'bar' 199 | await adMarket.registerDemand(demand, demandUrl) 200 | await adMarket.registerSupply(supply, supplyUrl) 201 | await adMarket.openChannel(supply, { from: demand }) 202 | const channel = parseChannel(await adMarket.getChannel(channelId)) 203 | 204 | // todo make channel 205 | 206 | const input = { 207 | impressionId: web3.sha3('bar'), 208 | impressionPrice: 2 209 | } 210 | }) 211 | 212 | it('verifySignature', async () => { 213 | const channel = { 214 | contractId: '0x12345123451234512345', 215 | channelId: web3.sha3('foo'), 216 | demand: '0x11111111111111111111', 217 | supply: '0x22222222222222222222', 218 | impressionId: 'foo', 219 | impressionPrice: 1, 220 | impressions: 1000, 221 | balance: 1000 222 | } 223 | 224 | channel.root = getRoot(channel, web3.sha3('foo')) 225 | 226 | const fingerprint = getFingerprint(channel) 227 | const sig = await p(web3.eth.sign)(accounts[0], fingerprint) 228 | assert.ok(verifySignature(channel, sig, accounts[0])) 229 | }) 230 | }) 231 | 232 | // TODO test Join edge cases 233 | 234 | /* 235 | it('checkpoint', async () => { 236 | await adMarket.open() 237 | 238 | await adMarket.join(1, { 239 | from: accounts[1], 240 | value: 1000 241 | }) 242 | 243 | let state = parseChannel(await adMarket.getChannel(1)) 244 | state.balances = [1, 999] 245 | state.sequenceNumber += 1 246 | 247 | const data = 'hello world' 248 | 249 | // TODO need a better merkling strategy 250 | await p(trie.put.bind(trie))(0, state.root) 251 | await p(trie.put.bind(trie))(1, data) 252 | 253 | state.root = '0x'+trie.root.toString('hex') 254 | 255 | const fingerprint = getFingerprint(state) 256 | 257 | const sig0 = await p(web3.eth.sign)(accounts[0], fingerprint) 258 | const sig1 = await p(web3.eth.sign)(accounts[1], fingerprint) 259 | 260 | await adMarket.checkpoint( 261 | state.channelId, 262 | state.participants, 263 | state.balances, 264 | state.root, 265 | state.sequenceNumber, 266 | sig0, 267 | sig1 268 | ) 269 | 270 | const saved = parseChannel(await adMarket.getChannel(1)) 271 | 272 | assert.equal(saved.balances[0], state.balances[0]) 273 | assert.equal(saved.balances[1], state.balances[1]) 274 | assert.equal(saved.root, state.root) 275 | assert.equal(saved.sequenceNumber, state.sequenceNumber) 276 | }) 277 | */ 278 | 279 | function makeString (char, length) { 280 | let string = '' 281 | for (let i = 0; i < length; i++) { 282 | string += char 283 | } 284 | return string 285 | } 286 | 287 | function range (max) { 288 | const arr = [] 289 | for (let i = 0; i < max; i++) { 290 | arr.push(i + 1) 291 | } 292 | return arr 293 | } 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdMarket 2 | 3 | ## Tests 4 | 5 | At the moment, only the contract tests have been written. To run the tests: 6 | 7 | First start testrpc in one terminal. 8 | 9 | ```bash 10 | npm run testrpc 11 | ``` 12 | 13 | Then run the tests in a different terminal. 14 | 15 | ```bash 16 | npm run mocha 17 | ``` 18 | 19 | ## Overview 20 | 21 | This repo is a (WIP) state channel system to allow an advertiser and a publisher (or their agents) to synchronize their recorded advertising impressions in real-time. 22 | 23 | ### Background 24 | 25 | For traditional ad contracts with 30 to 60 day settlement cycles, discrepancies in impression reporting between parties are not discovered until the contract is complete. Discrepancies in tracked impressions commonly reach [up to 20%](https://support.google.com/dfp_premium/answer/6160380). Some of these are intrinsic to browsers and networks and come from latency, network connection errors, ad blockers, and differences between ad server spam filtering techniques. The widespread acceptance of discrepancies across the industry, however, is exploited through fraudulent tampering of metrics and misreporting impressions. 26 | 27 | By syncronizing impressions in real-time, discrepancies can be eliminated or at 28 | least discovered much more quickly. 29 | 30 | ### AdChain 31 | 32 | This repo is being developed as part of the AdChain project, a collaboration 33 | between ConsenSys and MetaX. 34 | 35 | Check out [these slides](https://docs.google.com/presentation/d/1U7vi49QalSg2zwaetGK7DqQFhwMmiRGFdpdhtr1p3iU) for a quick overview of AdChain and the AdMarket. 36 | 37 | ### State Channels 38 | 39 | State Channels is a design pattern for building scalable decentralized 40 | applications. This documentation assumes familiarity with state channels. To 41 | review, check out the following links: 42 | 43 | - Martin Koeppelmann (Oct. 2015, blog) - [How offchain trading will work](http://forum.groupgnosis.com/t/how-offchain-trading-will-work/63) 44 | - Robert Mccone (Oct. 2015, blog) - [Ethereum Lightning Network and Beyond](http://www.arcturnus.com/ethereum-lightning-network-and-beyond/) 45 | - Jeff Coleman (Nov. 2015, blog) - [State Channels](http://www.jeffcoleman.ca/state-channels/) (see also: [discussion on /r/ethereum](https://www.reddit.com/r/ethereum/comments/3tcu82/state_channels_an_explanation/)) 46 | - Heiko Hees (Dec. 2015, talk) - [Raiden: Scaling Out With Offchain State 47 | Networks](https://www.youtube.com/watch?v=h791zjvf3uQ) 48 | - Jeff Coleman (Dec. 2015, interview) - [Epicenter Bitcoin: State Networks](https://www.youtube.com/watch?v=v0ZJDsRYnbA) 49 | - Jehan Tremback (Dec. 2015, blog) - [Universal Payment Channels](http://altheamesh.com/blog/universal-payment-channels/) 50 | - Martin Koeppelmann (Jan. 2016, slides) - [Scalability via State Channels](http://de.slideshare.net/MartinKppelmann/state-channels-and-scalibility) 51 | - Vitalik Buterin (Jun. 2016, paper) - [Ethereum: Platform Review (page 30)](http://static1.squarespace.com/static/55f73743e4b051cfcc0b02cf/t/57506f387da24ff6bdecb3c1/1464889147417/Ethereum_Paper.pdf) 52 | - Ameen Soleimani (Sept. 2016, talk) - [An Introduction to State Channels in 53 | Depth](https://www.youtube.com/watch?v=MEL50CVOcH4) 54 | - Jeff Coleman (ongoing, wiki) - [State Channels Wiki](https://github.com/ledgerlabs/state-channels/wiki) 55 | - Jeff Coleman (ongoing, code) - [Toy State Channels](https://github.com/ledgerlabs/toy-state-channels/tree/master/contracts) 56 | - Heiko Hees (ongoing, code) - [Raiden Network](https://github.com/raiden-network/raiden) 57 | - Sergey Ukustov (ongoing, code) - [Machinomy](https://github.com/machinomy/machinomy) 58 | 59 | I especially recommend Jeff Coleman's blog post and the Machinomy documentation 60 | as starting points. 61 | 62 | ### Usage 63 | 64 | The advertiser or their agent (demand) and the publisher or their agent (supply) 65 | will maintain a state channel for the duration of their business relationship, 66 | periodically checkpointing the channel state onchain. All data can be kept private between the parties, even during checkpointing, unless there is a dispute. 67 | 68 | This state channel tracks the impressions between demand and supply and can be thought of as an immutable "impression ledger". In response to browser ad impression events, the demand will send a signed state channel update over HTTP to the supply, acknowledging the impression. Both supply and demand store these channel updates offchain, in traditional databases such as PostgreSQL or MongoDB. 69 | 70 | The AdMarket operator plays a role as a passive observer and a tie-breaker in the event the supply witnesses an impression event the demand fails to acknowledge. 71 | 72 | #### Registration 73 | 74 | Both the demand and supply must be registered with the AdMarket in order to open channels. The AdMarket contract maintains a mapping of Ethereum addresses to url strings for registered members. The url strings point to adservers which will handle state channel messages. 75 | 76 | ``` 77 | mapping (address => string) public registeredDemand; 78 | mapping (address => string) public registeredSupply; 79 | ``` 80 | 81 | Only the owner of the AdMarket contract may register (or deregister) supply and demand 82 | participants. 83 | 84 | To register demand or supply, the AdMarket owner must provide their Ethereum address as well as a url string which points to their adserver which will handle offchain HTTP state channel messages. 85 | 86 | ``` 87 | function registerDemand(address demand, string url) only_owner {...} 88 | function registerSupply(address supply, string url) only_owner {...} 89 | 90 | function deregisterDemand(address demand) only_owner {...} 91 | function deregisterSupply(address supply) only_owner {...} 92 | ``` 93 | 94 | Once registration is complete, the supply and demand may update their own 95 | adserver urls. 96 | 97 | ``` 98 | function updateDemandUrl(string url) only_registered_demand {...} 99 | function updateSupplyUrl(string url) only_registered_supply {...} 100 | ``` 101 | 102 | ##### Future Integration with the AdChain Registry 103 | 104 | In the future, registration functionality will be removed in favor of interfacing directly with the AdChain Registry. 105 | 106 | #### Opening the Channel 107 | 108 | In this system, only the demand can open the channel, which it does by providing the address of a registered supply which it doesn't already have an open channel with. 109 | 110 | ``` 111 | function openChannel(address supply) only_registered_demand {...} 112 | ``` 113 | 114 | At the moment, the channel is only used for accounting purposes and payments are done out-of-band, so opening a channel does not require a monetary deposit. 115 | 116 | #### State Updates 117 | 118 | Once a channel is open, whenever the demand receives an impression event from a user's browser, it will generate a state channel update acknowledging the impression, save it, sign it, and send it to the supply. 119 | 120 | The **signed** portions of state channel message include the following fields: 121 | 122 | - contractId - the Ethereum address of the AdMarket contract 123 | - channelId - the integer id for this channel 124 | - demand - the Ethereum address of the demand 125 | - supply - the Ethereum address of the supply 126 | - root - the merkle root of the most recent channel state 127 | 128 | Where the channel state includes the following fields: 129 | 130 | - balance - the cumulative amount demand owes supply 131 | - impressions - the cumulative number of impressions 132 | - impressionId - the id of the latest impression 133 | - impressionPrice - the price of the latest impression 134 | - prevRoot - the merkle root of the previous channel state 135 | 136 | Because the channel state includes the previous root, the **root** of each channel state acts as a unique identifier not only for that specific state, but for the entire historical record of states leading up to it. 137 | 138 | Upon receiving the state channel message, the supply will verify it and save it to persistant storage. 139 | 140 | 141 | #### Channel Data 142 | 143 | The channel data in the AdMarket contract is set once when the channel is opened and periodically as the channel is checkpointed. Only the `root` is actually updated; the metadata serves to guide the channel through the proper checkpointing flow. 144 | 145 | ``` 146 | struct Channel { 147 | // State Variables (only root changes on each channel update) 148 | address contractId; 149 | bytes32 channelId; 150 | address demand; 151 | address supply; 152 | bytes32 root; 153 | 154 | // Metadata (not included in offchain state updates) 155 | ChannelState state; 156 | uint256 expiration; 157 | uint256 challengeTimeout; 158 | bytes32 proposedRoot; 159 | } 160 | 161 | enum ChannelState { Open, Checkpointing, Closing, Closed } 162 | ``` 163 | 164 | - `expiration` - block number after which the channel expires and can be closed by anyone (set in `openChannel`) 165 | - `challengeTimeout` - block number after which a proposed checkpoint or valid challenge can be finalized (set in `proposeCheckpoint` and `challengeCheckpoint`) 166 | - `proposedRoot` - a placeholder root which is only stored and set after the challenge period is over 167 | 168 | #### Checkpointing the Channel 169 | 170 | Periodically, the demand or supply can checkpoint the channel on the AdMarket contract, and optionally renew the channel. 171 | 172 | ##### Propose Checkpoint 173 | 174 | Checkpointing the channel happens in a few steps. The first step is to propose a checkpoint for the most recent signed state, indicated by its root. As mentioned above, the root acts as a unique fingerprint for the entire historical record of impressions for this channel, and checkpointing it amounts to a globally visible commitment to that record. 175 | 176 | The signature provided must be from the demand and is verified in the contract method. 177 | 178 | If `renew` is set to `true`, the channel will remain open once the checkpoint is completed. 179 | 180 | ``` 181 | function proposeCheckpoint( 182 | bytes32 channelId, 183 | bytes32 proposedRoot, 184 | bytes signature, 185 | bool renew 186 | ) {...} 187 | ``` 188 | 189 | ##### Challenge Checkpoint 190 | 191 | Proposing a checkpoint triggers a challenge period during which either party can challenge the `proposedRoot` by submitting a signed state update -- the `challengeRoot` -- with a higher impression count (the impressions count is sequence number). The index of the impressions in the state array and the corresponding merkle proof are required to verify that the impressions value is included in the `challengeRoot`. 192 | 193 | ``` 194 | function challengeCheckpoint( 195 | bytes32 channelId, 196 | bytes32 challengeRoot, 197 | uint256 impressions, 198 | uint256 index, 199 | bytes merkleProof, 200 | bytes signature 201 | ) {...} 202 | ``` 203 | 204 | ##### Accept Challenge 205 | 206 | If a challenge was issued, the challenge period is reset, providing time to answer the challenge. To accept the challenge, the party must provide an impressions count which can be proven to be included in the original `proposedRoot`, and is higher than the impressions in the challenge. Should this be the case, the checkpointing immediately completes and the original `proposedRoot` is recorded. 207 | 208 | ``` 209 | function acceptChallenge( 210 | bytes32 channelId, 211 | uint256 impressions, 212 | uint256 index, 213 | bytes merkleProof 214 | ) {...} 215 | ``` 216 | 217 | ##### Checkpoint 218 | 219 | If there is no valid challenge, then after the challenge period expires either party can finalize the proposed checkpoint, and `root` will be set to `proposedRoot`. 220 | 221 | Alternatively, if there is a valid challenge which is not accepted within the (reset) challenge period, then calling the `checkpointChannel` method will set `root` to `challengeRoot`. 222 | 223 | ``` 224 | function checkpointChannel(bytes32 channelId) {...} 225 | ``` 226 | 227 | 228 | 229 | #### Closing the Channel 230 | 231 | Closing a channel uses the same methods and follows the same flow as checkpointing, except with `renew` set to `false` from the beginning. 232 | 233 | If checkpointing was initiated with `renew` set to `true`, either party can still decide to close the channel using the `closeChannel` method. This can be done at any time during the checkpointing process, and sets `renew` to `false`. 234 | 235 | ``` 236 | function closeChannel(bytes32 channelId) {...} 237 | ``` 238 | 239 | ## License 240 | 241 | MIT 242 | -------------------------------------------------------------------------------- /js/reducers.js: -------------------------------------------------------------------------------- 1 | import { List, Map } from 'immutable' 2 | import { combineReducers } from 'redux-immutable' 3 | import { createStore } from 'redux' 4 | import { makeChannel, parseChannel, makeUpdate } from './channel' 5 | 6 | import Web3 from 'web3' 7 | 8 | const web3 = new Web3() 9 | 10 | const findChannel = (channels, payload) => { 11 | const result = channels.findEntry((channel) => { 12 | return channel.get('demand') == payload.demandId && 13 | channel.get('supply') == payload.supplyId 14 | }) 15 | return result || [undefined, undefined] 16 | } 17 | 18 | // TODO use a Map instead of a List for the channels? Because I query them by 19 | // supplyId + demandId every time anyways 20 | // I have to handle the edge case of multiple contracts with the same demand 21 | // + supply, meaning I have to create a unique channel ID 22 | // This could be the hash of channel + supply + demand, but then I have to hash 23 | // every time... 24 | 25 | // start - impressions count 26 | // pendingImpressions - immutable array of pending impressions 27 | // returns two values: 28 | // 1. the array of impressions to apply 29 | // 2. the remaining pendingImpressions 30 | function getReadyUpdates (start, pendingUpdates) { 31 | const sorted = pendingUpdates.sort((a, b) => { 32 | return a.impressions - b.impressions 33 | }) 34 | 35 | const stop = sorted.findIndex((update, index) => { 36 | return update.impressions != (index + start + 1) 37 | }) 38 | 39 | return stop == -1 ? [sorted, List([])] : [sorted.slice(0, stop), sorted.slice(stop)] 40 | } 41 | 42 | // IMPRESSION_SERVED should mean the same thing on supply and demand. It is the 43 | // event fired in response to receiving the impression served beacon from the 44 | // browser. 45 | // 46 | // CHANNEL_UPDATE should mean the state channel update event 47 | // 48 | // That said, the Supply and Demand IMPRESSION_SERVED resposnes are still 49 | // different. The Demand and Supply should both save, but the Demand will 50 | // message supply and the Supply will create a timeout waiting for that 51 | // message. 52 | // 53 | // Neither do Supply and Demand do the same thing for CHANNEL_UPDATE, because 54 | // the Supply has timeouts to clear. 55 | // 56 | // It is probably best to factor them by entity. 57 | // 58 | // TODO By default, send the whole impression object from Demand to Supply. 59 | // Later on, optimize for bandwidth by only sending the whole impression event 60 | 61 | // I wasn't going to use gun but they have PANIC... 62 | 63 | export function supplyChannelsReducer (channels = List([]), { type, payload }) { 64 | let index, channel 65 | switch (type) { 66 | case 'IMPRESSION_SERVED': 67 | // So here, we will add the impression to pendingImpressions queue 68 | // - some chance of receiving the channelUpdate on the impression before the 69 | // impression? 70 | // - idempotent updating, store the channelUpdate anyway, trigger when we receive 71 | // impression. 72 | // 73 | // when we restart the process, we should also restart timeouts 74 | // - and if any timeouts are already passed, we should immediately 75 | // trigger those requests (this can be handled in a restart bootstrap 76 | // script) 77 | 78 | [index, channel] = findChannel(channels, payload) 79 | if (channel) { 80 | return channels.update(index, (channel) => { 81 | return channel.update('pendingImpressions', pending => pending.push(payload)) 82 | }) 83 | } 84 | 85 | case 'CHANNEL_UPDATE': 86 | [index, channel] = findChannel(channels, payload) 87 | if (channel) { 88 | // TODO efficiency - combine with other mutations? 89 | // remove impression from pendingImpressions 90 | channel = channel.update('pendingImpressions', pendingImpressions => { 91 | return pendingImpressions.filter(impression => impression.impressionId != payload.impressionId) 92 | }) 93 | 94 | // impression is next in order 95 | if (payload.impressions == channel.get('impressions') + 1) { 96 | // - compute the state transition with the impression 97 | // - sort the pending impressions 98 | // - loop over the pending impressions and apply them until one is out 99 | // of order 100 | // - remove applied impressions from pool 101 | // - also cancel the setTimeout functions 102 | const final = channel.withMutations(channel => { 103 | const [toMerge, pending] = getReadyUpdates(payload.impressions, channel.get('pendingUpdates')) 104 | toMerge.reduce((channel, impression) => { 105 | return makeUpdate(channel, impression) 106 | }, makeUpdate(channel, payload)) 107 | channel.set('pendingUpdates', pending) 108 | }) 109 | return channels.set(index, final) 110 | } else { 111 | // - add it to the list of pending impressions 112 | // - start a setTimeout (10s) and if the impression event isn't received by supply 113 | // - this setTimeout should not cancel until either the demand responds 114 | // with the impression or the arbiter responds that it is invalid. 115 | // - the setTimeout should dispatch which replaces existing timeout with 116 | // the new one 117 | const final = channel.update('pendingUpdates', pending => pending.push(payload)) 118 | return channels.set(index, final) 119 | } 120 | } 121 | return channels 122 | 123 | case 'SIGNATURES_RECEIVED': 124 | // TODO multiple channels - for now just do it for one 125 | // hack - we get to assume all impressions are for the same channel 126 | // future - we have to separate out impressions by channels, loop through 127 | // each channel separately 128 | // 129 | // payload is an array of impressions 130 | // [ { impressionId, signature, price, ... } ... ] 131 | 132 | let impression = payload[0] 133 | // remove the impressions from the pendingImpressions queue 134 | // add the impressions to the pendingUpdateRequests queue 135 | 136 | return channels.deleteIn([0, 'pendingImpressions', 0]).setIn([0, 'pendingUpdateRequests', 0], impression) 137 | 138 | case 'IMPRESSION_NOT_FOUND': 139 | // TOTAL HACK 140 | 141 | // for each id in the impressionIds array, 142 | // remove the impression from the pendingImpressions array 143 | 144 | return channels.deleteIn([0, 'pendingImpressions', 0]) 145 | 146 | case 'CHANNEL_OPENED': 147 | // TODO allow for multiple channels, right now just create a new list 148 | return List([makeChannel(parseChannel(payload))]) 149 | case 'CHANNEL_CHECKPOINT_PROPOSED': 150 | [index, channel] = findChannel(channels, payload) 151 | return channels.setIn([index, 'state'], 1) 152 | case 'CHANNEL_CHECKPOINT_CHALLENGED': 153 | 154 | case 'CHANNEL_CHECKPOINT_CHALLENGE_ACCEPTED': 155 | 156 | case 'CHANNEL_CLOSE_PROPOSED': 157 | 158 | case 'CHANNEL_CLOSE_CHALLENGED': 159 | 160 | case 'CHANNEL_CLOSE_CHALLENGE_ACCEPTED': 161 | 162 | case 'CHANNEL_CLOSED': 163 | 164 | default: 165 | return channels 166 | } 167 | } 168 | 169 | export function admarketChannelsReducer (channels = List([]), { type, payload }) { 170 | let index, channel 171 | switch (type) { 172 | case 'IMPRESSION_SERVED': 173 | // Is there any reason to update state when an impression is received? 174 | // Don't think so. Maybe to track impressions that were never included in 175 | // the channel state, to delete them? We need to make sure to prune 176 | // impressions on all nodes, actually. 177 | return channels 178 | 179 | case 'CHANNEL_UPDATE': 180 | [index, channel] = findChannel(channels, payload) 181 | if (channel) { 182 | // impression is next in order 183 | if (payload.impressions == channel.get('impressions') + 1) { 184 | // - compute the state transition with the impression 185 | // - sort the pending impressions 186 | // - loop over the pending impressions and apply them until one is out 187 | // of order 188 | // - remove applied impressions from pool 189 | // - also cancel the setTimeout functions 190 | const final = channel.withMutations(channel => { 191 | const [toMerge, pending] = getReadyUpdates(payload.impressions, channel.get('pendingUpdates')) 192 | toMerge.reduce((channel, impression) => { 193 | return makeUpdate(channel, impression) 194 | }, makeUpdate(channel, payload)) 195 | channel.set('pendingUpdates', pending) 196 | }) 197 | return channels.set(index, final) 198 | } else { 199 | // - add it to the list of pending impressions 200 | // - start a setTimeout (10s) and if the impression event isn't received by supply 201 | // - this setTimeout should not cancel until either the demand responds 202 | // with the impression or the arbiter responds that it is invalid. 203 | // - the setTimeout should dispatch which replaces existing timeout with 204 | // the new one 205 | const final = channel.update('pendingUpdates', pending => pending.push(payload)) 206 | return channels.set(index, final) 207 | } 208 | } 209 | return channels 210 | 211 | case 'CHANNEL_OPENED': 212 | // TODO allow for multiple channels, right now just create a new list 213 | return List([makeChannel(parseChannel(payload))]) 214 | case 'CHANNEL_CHECKPOINT_PROPOSED': 215 | [index, channel] = findChannel(channels, payload) 216 | return channels.setIn([index, 'state'], 1) 217 | case 'CHANNEL_CHECKPOINT_CHALLENGED': 218 | 219 | case 'CHANNEL_CHECKPOINT_CHALLENGE_ACCEPTED': 220 | 221 | case 'CHANNEL_CLOSE_PROPOSED': 222 | 223 | case 'CHANNEL_CLOSE_CHALLENGED': 224 | 225 | case 'CHANNEL_CLOSE_CHALLENGE_ACCEPTED': 226 | 227 | case 'CHANNEL_CLOSED': 228 | 229 | default: 230 | return channels 231 | } 232 | } 233 | 234 | export function channelsReducer (channels = List([]), { type, payload }) { 235 | let index, channel 236 | switch (type) { 237 | case 'IMPRESSION_SERVED': 238 | [index, channel] = findChannel(channels, payload) 239 | if (channel) { 240 | channels = channels.set(index, makeUpdate(channel, payload, true)) 241 | } 242 | return channels 243 | case 'CHANNEL_OPENED': 244 | // TODO allow for multiple channels, right now just create a new list 245 | return List([makeChannel(parseChannel(payload))]) 246 | case 'CHANNEL_CHECKPOINT_PROPOSED': 247 | [index, channel] = findChannel(channels, payload) 248 | return channels.setIn([index, 'state'], 1) 249 | case 'CHANNEL_CHECKPOINT_CHALLENGED': 250 | 251 | case 'CHANNEL_CHECKPOINT_CHALLENGE_ACCEPTED': 252 | 253 | case 'CHANNEL_CLOSE_PROPOSED': 254 | 255 | case 'CHANNEL_CLOSE_CHALLENGED': 256 | 257 | case 'CHANNEL_CLOSE_CHALLENGE_ACCEPTED': 258 | 259 | case 'CHANNEL_CLOSED': 260 | 261 | default: 262 | return channels 263 | } 264 | } 265 | 266 | // How do I open a channel? I call the API method, either through CLI or Web. 267 | // This triggers the transaction to be sent to Ethereum. Do I store it locally? 268 | // Yes. The state should include that my transaction is pending. 269 | // In fact this is true for all transactions and should be considered a pattern. 270 | // How do we represent pending transactions? I could create a top level reducer 271 | // just for blockchain interaction. This would prevent duplicate calls. 272 | // I would query it before I sent the TX to see if I can (api middleware). 273 | // The store of pending requests can also have a timeout which dispatches to 274 | // clear the TX after too long (could be a problem in case of DDOS on ethereum) 275 | 276 | const init = Map({ Demand: Map({}), Supply: Map({}), Arbiter: Map({}) }) 277 | 278 | export function registryReducer (registry = init, { type, payload: { address, url } }) { 279 | switch (type) { 280 | case 'DEMAND_REGISTERED': 281 | case 'DEMAND_UPDATED': 282 | return registry.setIn(['Demand', address], url) 283 | case 'SUPPLY_REGISTERED': 284 | case 'SUPPLY_UPDATED': // TODO - if supply has a url 285 | return registry.setIn(['Supply', address], url) 286 | case 'ARBITER_REGISTERED': 287 | case 'ARBITER_UPDATED': 288 | return registry.setIn(['Arbiter', address], url) 289 | case 'DEMAND_DEREGISTERED': 290 | return registry.deleteIn(['Demand', address]) 291 | case 'SUPPLY_DEREGISTERED': 292 | return registry.deleteIn(['Supply', address]) 293 | case 'ARBITER_DEREGISTERED': 294 | return registry.deleteIn(['Arbiter', address]) 295 | default: 296 | return registry 297 | } 298 | } 299 | 300 | /* 301 | case 'SUPPLY_REGISTERED': 302 | reducer.Supply.push(payload) 303 | case 'ARBITER_REGISTERED': 304 | reducer.Arbiter.push(action.payload) 305 | case 'DEMAND_DEREGISTERED': 306 | reducer.Demand.push(action.payload) 307 | case 'SUPPLY_DEREGISTERED': 308 | reducer.Demand.push(action.payload) 309 | case 'ARBITER_DEREGISTERED': 310 | reducer.Demand.push(action.payload) 311 | case 'DEMAND_UPDATED': 312 | reducer.Demand.push(action.payload) 313 | case 'SUPPLY_UPDATED': 314 | reducer.Demand.push(action.payload) 315 | return 316 | */ 317 | 318 | export function blocks () {} 319 | -------------------------------------------------------------------------------- /contracts/AdMarket.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.7; 2 | 3 | import "ECVerify.sol"; 4 | 5 | // Registers supply and demand, facilitates discovery, and manages the impression tracking state channels between them 6 | contract AdMarket is ECVerify { 7 | 8 | bytes32 emptyString = hex"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; 9 | 10 | address owner; 11 | string ownerUrl; 12 | 13 | mapping (address => string) public registeredDemand; 14 | // registeredDemand[0xabc...] => toyota.adserver.com 15 | 16 | mapping (address => string) public registeredSupply; 17 | // registeredSupply[0xdef...] => nyt.adserver.com 18 | 19 | mapping (bytes32 => Channel) channels; 20 | // channels[channelId] => channel metadata 21 | 22 | mapping (bytes32 => Challenge) challenges; 23 | // channels[channelId] => channel challenge metadata 24 | 25 | mapping (address => mapping (address => bool)) channelPartners; 26 | // channelPartners[demand][supply] => true/false 27 | 28 | uint256 public channelCount = 0; 29 | uint256 public channelTimeout; // max lifetime of a channel in blocks 30 | uint256 public challengePeriod; // number of blocks to wait for challenges before closing 31 | 32 | enum ChannelState { Open, Checkpointing, Closing, Closed } 33 | 34 | struct Channel { 35 | // State Variables (only root changes on each channel update) 36 | address contractId; 37 | bytes32 channelId; 38 | address demand; 39 | address supply; 40 | bytes32 root; 41 | 42 | // Metadata (not included in offchain state updates) 43 | ChannelState state; 44 | uint256 expiration; // block number after which the channel expires and can be closed by anyone (set in openChannel) 45 | uint256 challengeTimeout; // block number after which the channel can be closed by its participants (set in proposeCheckpointChannel and challengeCheckpointChannel) 46 | bytes32 proposedRoot; // a placeholder root which is only stored and set after the challenge period is over 47 | } 48 | 49 | // Note: Root is the merkle root of the previous root and the current state. The current state includes: 50 | // - balance: demand -> supply 51 | // - impressions (#) (sequence number) 52 | // - impressionId 53 | // - impressionPrice 54 | // In case of a dispute, the data can be made public and verified onchain using merkle proofs. 55 | // For example, to challenge a replay attack, we provide impression count (sequence number) and merkle proof for both the replay 56 | // state and the most recent one. 57 | 58 | struct Challenge { 59 | bytes32 challengeRoot; // the root of the most recent channel state, according to the challenging party 60 | uint256 impressions; // the state with the higher impression count wins 61 | } 62 | 63 | modifier only_owner() { 64 | if (msg.sender != owner) throw; 65 | _; 66 | } 67 | 68 | modifier only_registered_demand() { 69 | if (!isRegisteredDemand(msg.sender)) throw; 70 | _; 71 | } 72 | 73 | modifier only_registered_supply() { 74 | if (!isRegisteredSupply(msg.sender)) throw; 75 | _; 76 | } 77 | 78 | event DemandRegistered(address indexed demand); 79 | event SupplyRegistered(address indexed supply); 80 | event DemandDeregistered(address indexed demand); 81 | event SupplyDeregistered(address indexed supply); 82 | event DemandUrlUpdated(address indexed demand); 83 | event SupplyUrlUpdated(address indexed supply); 84 | // TODO 85 | // event ChannelOpened(); 86 | // event ChannelCheckpointProposed(); 87 | // event ChannelCheckpointChallenged(); 88 | // event ChannelCheckpointChallengeAccepted(); 89 | // event ChannelCheckpointed(); 90 | // event ChannelCloseProposed(); 91 | // event ChannelCloseChallenged(); 92 | // event ChannelCloseChallengeAccepted(); 93 | // event ChannelCloseed(); 94 | 95 | // Debugging 96 | event Error(string message); 97 | event LogBytes32(bytes32 message); 98 | event LogBool(bool message); 99 | 100 | // Constructor 101 | function AdMarket(string _ownerUrl, uint256 _channelTimeout, uint256 _challengePeriod) { 102 | owner = msg.sender; 103 | ownerUrl = _ownerUrl; 104 | channelTimeout = _channelTimeout; 105 | challengePeriod = _challengePeriod; 106 | } 107 | 108 | function updateOwnerUrl(string _ownerUrl) only_owner { 109 | ownerUrl = _ownerUrl; 110 | } 111 | 112 | function registerDemand(address demand, string url) only_owner { 113 | if (isEmptyString(url)) throw; // must at least provide a non-empty string to update later 114 | registeredDemand[demand] = url; 115 | DemandRegistered(demand); 116 | } 117 | 118 | function registerSupply(address supply, string url) only_owner { 119 | if (isEmptyString(url)) throw; // must at least provide a non-empty string to update later 120 | registeredSupply[supply] = url; 121 | SupplyRegistered(supply); 122 | } 123 | 124 | function deregisterDemand(address demand) only_owner { 125 | registeredDemand[demand] = ""; 126 | DemandDeregistered(demand); 127 | } 128 | 129 | function deregisterSupply(address supply) only_owner { 130 | registeredSupply[supply] = ""; 131 | SupplyDeregistered(supply); 132 | } 133 | 134 | // A registered demand can update the url of their server endpoint 135 | function updateDemandUrl(string url) only_registered_demand { 136 | if (isEmptyString(url)) throw; // can't update to empty string, must deregister 137 | registeredDemand[msg.sender] = url; 138 | DemandUrlUpdated(msg.sender); 139 | } 140 | 141 | // A registered supply can update the url of their server endpoint 142 | function updateSupplyUrl(string url) only_registered_supply { 143 | if (isEmptyString(url)) throw; // can't update to empty string, must deregister 144 | registeredSupply[msg.sender] = url; 145 | SupplyUrlUpdated(msg.sender); 146 | } 147 | 148 | // Demand can open a channel with any supply 149 | function openChannel(address supply) only_registered_demand { 150 | address demand = msg.sender; 151 | 152 | // Check that supply is registered 153 | if (!isRegisteredSupply(supply)) throw; 154 | 155 | // Check that we don't already have a channel open with the supply 156 | if (channelPartners[demand][supply]) throw; 157 | 158 | bytes32 channelId = sha3(channelCount++); 159 | uint256 expiration = block.number + channelTimeout; 160 | 161 | channels[channelId] = Channel( 162 | this, // contractId 163 | channelId, 164 | demand, 165 | supply, 166 | 0, // root 167 | ChannelState.Open, 168 | expiration, 169 | 0, // challengeTimeout 170 | 0 // proposed root 171 | ); 172 | 173 | channelPartners[demand][supply] = true; 174 | } 175 | 176 | // Either supply or demand can checkpoint a channel at any time 177 | // We have to have a challenge period because we aren't tracking sequence number (impressions) onchain. 178 | // Checkpointing gives us the ability to have long-lived channels without downtime. 179 | // The channel participants can elect to renew the channel during a checkpoint. 180 | function proposeCheckpoint( 181 | bytes32 channelId, 182 | bytes32 proposedRoot, 183 | bytes signature, 184 | bool renew 185 | ) { 186 | Channel channel = channels[channelId]; 187 | 188 | // Check that msg.sender is either demand or supply 189 | if (!(channel.demand == msg.sender || channel.supply == msg.sender)) throw; 190 | 191 | // Check that the channel is open 192 | if (!(channel.state == ChannelState.Open)) throw; 193 | 194 | bytes32 fingerprint = sha3( 195 | address(this), 196 | channelId, 197 | channel.demand, 198 | channel.supply, 199 | proposedRoot 200 | ); 201 | 202 | // Check the signature on the state 203 | if (!ecverify(fingerprint, signature, channel.demand)) throw; 204 | 205 | // renew the channel, keeping it open at the end of the checkpoint 206 | if (renew) { 207 | channel.state = ChannelState.Checkpointing; 208 | 209 | // close the channel at the end of the checkpoint process 210 | } else { 211 | channel.state = ChannelState.Closing; 212 | } 213 | 214 | channel.challengeTimeout = block.number + challengePeriod; 215 | channel.proposedRoot = proposedRoot; 216 | } 217 | 218 | // Either supply or demand can choose to not renew and instead close a channel 219 | // at any point during the checkpointing process. 220 | // The checkpointing process would continue in the same exact way, but it would 221 | // close upon completion instead of remaining open 222 | function closeChannel(bytes32 channelId) { 223 | Channel channel = channels[channelId]; 224 | 225 | // Check that msg.sender is either demand or supply 226 | if (!(channel.demand == msg.sender || channel.supply == msg.sender)) throw; 227 | 228 | // Check that the channel is checkpointing 229 | if (!(channel.state == ChannelState.Checkpointing)) throw; 230 | 231 | channel.state = ChannelState.Closing; 232 | } 233 | 234 | // Either supply or demand can challenge a checkpointing channel before the challengeTimeout period ends 235 | // They supply a different merkleRoot -- the challenge root -- which has more impressions. 236 | // They also supply the proof for this impression count and the Demand's signature on it. 237 | // This resets the challengeTimeout giving the counterparty a chance to accept this challenge. 238 | // To accept the challenge, the counterparty must prove that the original checkpointed root has more impressions 239 | function challengeCheckpoint( 240 | bytes32 channelId, 241 | bytes32 challengeRoot, 242 | uint256 impressions, 243 | uint256 index, 244 | bytes merkleProof, 245 | bytes signature 246 | ) { 247 | Channel channel = channels[channelId]; 248 | 249 | // Check that msg.sender is either demand or supply 250 | if (!(channel.demand == msg.sender || channel.supply == msg.sender)) throw; 251 | 252 | // Check that the channel is checkpointing or closing 253 | if (channel.state != ChannelState.Checkpointing && channel.state != ChannelState.Closing) throw; 254 | 255 | // Check that the challenge period has not expired 256 | if (channel.challengeTimeout < block.number) throw; 257 | 258 | bytes32 fingerprint = sha3( 259 | address(this), 260 | channelId, 261 | channel.demand, 262 | channel.supply, 263 | challengeRoot 264 | ); 265 | 266 | // Check the signature on the state 267 | if (!ecverify(fingerprint, signature, channel.demand)) throw; 268 | 269 | // Check the merkle proof for the impressions and challengeRoot 270 | if (!(checkProofOrdered(merkleProof, challengeRoot, sha3(impressions), index))) throw; 271 | 272 | challenges[channelId] = Challenge( 273 | challengeRoot, 274 | impressions 275 | ); 276 | 277 | // Extend the challenge timeout, giving the counterparty additional time to respond 278 | channel.challengeTimeout = block.number + challengePeriod; 279 | } 280 | 281 | // Either the demand or supply can accept a checkpoint challenge before the challengeTimeout ends. 282 | // They must provide proof that the impressions in the original checkpoint are greater than 283 | // in the checkpoint challenge. 284 | // If they succeed, then the channel checkpointing process immediately ends, and the channel 285 | // state is finalized with the original state. 286 | // Otherwise, the channel checkpointing process will continue until the challenge period expires 287 | // and the state is finalized by the checkPointChannel function 288 | // If the participants intend to renew, the channel will stay open and its expiration block will reset. 289 | // Otherwise the channel will close. 290 | function acceptChallenge( 291 | bytes32 channelId, 292 | uint256 impressions, 293 | uint256 index, 294 | bytes merkleProof 295 | ) { 296 | Channel channel = channels[channelId]; 297 | 298 | // Check that msg.sender is either demand or supply 299 | if (!(channel.demand == msg.sender || channel.supply == msg.sender)) throw; 300 | 301 | // Check that the channel is checkpointing or closing 302 | if (channel.state != ChannelState.Checkpointing && channel.state != ChannelState.Closing) throw; 303 | 304 | // Check that the challenge period is not over 305 | if (channel.challengeTimeout < block.number) throw; 306 | 307 | Challenge challenge = challenges[channelId]; 308 | 309 | // Check that a challenge was presented 310 | if (challenge.impressions <= 0) throw; 311 | 312 | // Check that impressions is larger than in the challenge 313 | if (challenge.impressions > impressions) throw; 314 | 315 | // Check the merkle proof for the impressions and proposedRoot 316 | if (!(checkProofOrdered(merkleProof, channel.proposedRoot, sha3(impressions), index))) throw; 317 | 318 | // renew channel 319 | if (channel.state == ChannelState.Checkpointing) { 320 | channel.expiration = block.number + channelTimeout; 321 | channel.state = ChannelState.Open; 322 | 323 | // close channel 324 | } else { 325 | channel.state = ChannelState.Closed; 326 | channelPartners[channel.demand][channel.supply] = false; 327 | } 328 | 329 | // even if the channel is closed, we want to record the final state root 330 | channel.root = channel.proposedRoot; 331 | channel.proposedRoot = 0; 332 | channel.challengeTimeout = 0; 333 | delete challenges[channelId]; 334 | } 335 | 336 | // Either the demand or supply can finalize the checkpoint after the challengeTimeout ends. 337 | // If a valid challenge was presented and not accepted, it wins and becomes the final state. 338 | // If no challenge was presented, the originally proposed state root is accepted. 339 | // If the participants intend to renew, the channel will stay open and its expiration block will reset. 340 | // Otherwise the channel will close. 341 | function checkpointChannel(bytes32 channelId) { 342 | Channel channel = channels[channelId]; 343 | 344 | // Check that msg.sender is either demand or supply 345 | if (!(channel.demand == msg.sender || channel.supply == msg.sender)) throw; 346 | 347 | // Check that the channel is checkpointing or closing 348 | if (channel.state != ChannelState.Checkpointing && channel.state != ChannelState.Closing) throw; 349 | 350 | // Check that the challenge period is over 351 | if (channel.challengeTimeout > block.number) throw; 352 | 353 | Challenge challenge = challenges[channelId]; 354 | 355 | // If there was an unanswered challenge, it wins. Otherwise the proposedRoot is accepted. 356 | // note: challenge.impressions can only be > 0 if there was an unanswered challenge. 357 | // if the challenge was successfully answered, challenge.impressions would have been deleted 358 | if (challenge.impressions > 0) { 359 | channel.root = challenge.challengeRoot; 360 | } else { 361 | channel.root = channel.proposedRoot; 362 | } 363 | 364 | // renew channel 365 | if (channel.state == ChannelState.Checkpointing) { 366 | channel.expiration = block.number + channelTimeout; 367 | channel.state = ChannelState.Open; 368 | 369 | // close channel 370 | } else { 371 | channel.state = ChannelState.Closed; 372 | channelPartners[channel.demand][channel.supply] = false; 373 | } 374 | 375 | channel.proposedRoot = 0; 376 | channel.challengeTimeout = 0; 377 | delete challenges[channelId]; 378 | } 379 | 380 | // TODO 381 | function closeExpiredChannel() {} 382 | 383 | function isEmptyString(string s) constant returns (bool) { 384 | return sha3(s) == emptyString; 385 | } 386 | 387 | function isRegisteredDemand(address demand) returns (bool) { 388 | return !isEmptyString(registeredDemand[demand]); 389 | } 390 | 391 | function isRegisteredSupply(address supply) returns (bool) { 392 | return !isEmptyString(registeredSupply[supply]); 393 | } 394 | 395 | // ------- 396 | // Getters 397 | // ------- 398 | 399 | function getChannel(bytes32 id) constant returns ( 400 | address contractId, 401 | bytes32 channelId, 402 | address demand, 403 | address supply, 404 | bytes32 root, 405 | ChannelState state, 406 | uint256 expiration, 407 | uint256 challengeTimeout, 408 | bytes32 proposedRoot 409 | ) { 410 | Channel channel = channels[id]; 411 | return ( 412 | channel.contractId, 413 | channel.channelId, 414 | channel.demand, 415 | channel.supply, 416 | channel.root, 417 | channel.state, 418 | channel.expiration, 419 | channel.challengeTimeout, 420 | channel.proposedRoot 421 | ); 422 | } 423 | 424 | function getChallenge(bytes32 id) constant returns ( 425 | bytes32 challengeRoot, 426 | uint256 impressions 427 | ) { 428 | Challenge challenge = challenges[id]; 429 | return ( 430 | challenge.challengeRoot, 431 | challenge.impressions 432 | ); 433 | } 434 | 435 | // TODO deploy as a library 436 | function checkProofOrdered( 437 | bytes proof, bytes32 root, bytes32 hash, uint256 index 438 | ) constant returns (bool) { 439 | // use the index to determine the node ordering 440 | // index ranges 1 to n 441 | 442 | bytes32 el; 443 | bytes32 h = hash; 444 | uint256 remaining; 445 | 446 | for (uint256 j = 32; j <= proof.length; j += 32) { 447 | assembly { 448 | el := mload(add(proof, j)) 449 | } 450 | 451 | // calculate remaining elements in proof 452 | remaining = (proof.length - j + 32) / 32; 453 | 454 | // we don't assume that the tree is padded to a power of 2 455 | // if the index is odd then the proof will start with a hash at a higher 456 | // layer, so we have to adjust the index to be the index at that layer 457 | while (remaining > 0 && index % 2 == 1 && index > 2 ** remaining) { 458 | index = uint(index) / 2 + 1; 459 | } 460 | 461 | if (index % 2 == 0) { 462 | h = sha3(el, h); 463 | index = index / 2; 464 | } else { 465 | h = sha3(h, el); 466 | index = uint(index) / 2 + 1; 467 | } 468 | } 469 | 470 | return h == root; 471 | } 472 | } 473 | 474 | -------------------------------------------------------------------------------- /tests/contracts/admarket.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import p from 'es6-promisify' 3 | import Web3 from 'web3' 4 | import MerkleTree, { checkProofOrdered, merkleRoot, getProof } from 'merkle-tree-solidity' 5 | import { sha3 } from 'ethereumjs-util' 6 | import setup from '../../js/setup' 7 | import { makeChannel, parseChannel, getFingerprint, getLeaves, getRoot, solSha3, 8 | parseLogAddress, verifySignature, makeUpdate, verifyUpdate, parseBN, parseChallenge 9 | } from '../../js/channel' 10 | import { wait } from '../../js/utils' 11 | 12 | const web3 = new Web3() 13 | 14 | describe('AdMarket', () => { 15 | 16 | let adMarket, eth, accounts, web3 17 | let filter 18 | let snapshots = [] 19 | 20 | let CHANNEL_TIMEOUT = 20 21 | let CHALLENGE_PERIOD = 10 22 | 23 | const takeSnapshot = () => { 24 | return new Promise(async (accept) => { 25 | let res = await p(web3.currentProvider.sendAsync.bind(web3.currentProvider))({ 26 | jsonrpc: '2.0', 27 | method: 'evm_snapshot', 28 | id: new Date().getTime() 29 | }) 30 | accept(res.result) 31 | }) 32 | } 33 | 34 | const revertSnapshot = (snapshotId) => { 35 | return new Promise(async (accept) => { 36 | await p(web3.currentProvider.sendAsync.bind(web3.currentProvider))({ 37 | jsonrpc: '2.0', 38 | method: 'evm_revert', 39 | params: [snapshotId], 40 | id: new Date().getTime() 41 | }) 42 | accept() 43 | }) 44 | } 45 | 46 | const mineBlock = () => { 47 | return new Promise(async (accept) => { 48 | await p(web3.currentProvider.sendAsync.bind(web3.currentProvider))({ 49 | jsonrpc: "2.0", 50 | method: "evm_mine", 51 | id: new Date().getTime() 52 | }) 53 | accept() 54 | }) 55 | } 56 | 57 | const mineBlocks = (count) => { 58 | return new Promise(async (accept) => { 59 | let i = 0 60 | while (i < count) { 61 | await mineBlock() 62 | i++ 63 | } 64 | accept() 65 | }) 66 | } 67 | 68 | before(async () => { 69 | let result = await setup({ 70 | testRPCProvider: 'http://localhost:8545', 71 | channelTimeout: CHANNEL_TIMEOUT, 72 | challengePeriod: CHALLENGE_PERIOD 73 | }) 74 | adMarket = result.adMarket 75 | eth = result.eth 76 | accounts = result.accounts 77 | web3 = result.web3 78 | }) 79 | 80 | describe('[with contract deployed]', () => { 81 | 82 | before(async () => { 83 | snapshots.push(await takeSnapshot()) 84 | }) 85 | 86 | beforeEach(async () => { 87 | snapshots.push(await takeSnapshot()) 88 | filter = web3.eth.filter({ address: adMarket.address, fromBlock: 0 }) 89 | }) 90 | 91 | afterEach(async () => { 92 | await p(filter.stopWatching.bind(filter))() 93 | await revertSnapshot(snapshots.pop()) 94 | }) 95 | 96 | after(async () => { 97 | await revertSnapshot(snapshots.pop()) 98 | }) 99 | 100 | it('setup', async () => { 101 | // channelCount should start at 0 102 | const channelCount = await adMarket.channelCount() 103 | assert.equal(+channelCount[0].toString(), 0) 104 | }) 105 | 106 | it('registerDemand', async () => { 107 | const demand = accounts[1] 108 | const url = 'foo' 109 | await adMarket.registerDemand(demand, url) 110 | const result = await adMarket.registeredDemand(demand) 111 | assert.equal(result[0], url) 112 | 113 | const logs = await p(filter.get.bind(filter))() 114 | const logAddress = parseLogAddress(logs[0].topics[1]) 115 | assert.equal(logAddress, demand) 116 | }) 117 | 118 | it('registerSupply', async () => { 119 | const supply = accounts[1] 120 | const url = 'foo' 121 | await adMarket.registerSupply(supply, url) 122 | const result = await adMarket.registeredSupply(supply) 123 | assert.equal(result[0], url) 124 | 125 | const logs = await p(filter.get.bind(filter))() 126 | const logAddress = parseLogAddress(logs[0].topics[1]) 127 | assert.equal(logAddress, supply) 128 | }) 129 | 130 | it('deregisterDemand', async () => { 131 | const demand = accounts[1] 132 | const url = 'foo' 133 | await adMarket.registerDemand(demand, url) 134 | await adMarket.deregisterDemand(demand) 135 | const result = await adMarket.registeredDemand(demand) 136 | assert.equal(result[0], '') 137 | 138 | const logs = await p(filter.get.bind(filter))() 139 | const logAddress = parseLogAddress(logs[1].topics[1]) 140 | assert.equal(logAddress, demand) 141 | }) 142 | 143 | it('deregisterSupply', async () => { 144 | const supply = accounts[1] 145 | const url = 'foo' 146 | await adMarket.registerSupply(supply, url) 147 | await adMarket.deregisterSupply(supply) 148 | const result = await adMarket.registeredSupply(supply) 149 | assert.equal(result[0], '') 150 | 151 | const logs = await p(filter.get.bind(filter))() 152 | const logAddress = parseLogAddress(logs[1].topics[1]) 153 | assert.equal(logAddress, supply) 154 | }) 155 | 156 | it('updateDemandUrl', async () => { 157 | const demand = accounts[1] 158 | const url = 'foo' 159 | await adMarket.registerDemand(demand, url) 160 | await adMarket.updateDemandUrl('bar', { from: demand }) 161 | const result = await adMarket.registeredDemand(demand) 162 | assert.equal(result[0], 'bar') 163 | 164 | const logs = await p(filter.get.bind(filter))() 165 | const logAddress = parseLogAddress(logs[1].topics[1]) 166 | assert.equal(logAddress, demand) 167 | }) 168 | 169 | it('updateSupplyUrl', async () => { 170 | const supply = accounts[1] 171 | const url = 'foo' 172 | await adMarket.registerSupply(supply, url) 173 | await adMarket.updateSupplyUrl('bar', { from: supply }) 174 | const result = await adMarket.registeredSupply(supply) 175 | assert.equal(result[0], 'bar') 176 | 177 | const logs = await p(filter.get.bind(filter))() 178 | const logAddress = parseLogAddress(logs[1].topics[1]) 179 | assert.equal(logAddress, supply) 180 | }) 181 | 182 | it('openChannel', async () => { 183 | const demand = accounts[1] 184 | const supply = accounts[2] 185 | const demandUrl = 'foo' 186 | const supplyUrl = 'bar' 187 | const channelId = solSha3(0) 188 | await adMarket.registerDemand(demand, demandUrl) 189 | await adMarket.registerSupply(supply, supplyUrl) 190 | 191 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 192 | const channelTimeout = parseBN((await p(adMarket.channelTimeout)())[0]) 193 | const expiration = blockNumber + channelTimeout + 1 194 | 195 | await adMarket.openChannel(supply, { from: demand }) 196 | 197 | const channel = parseChannel(await adMarket.getChannel(channelId)) 198 | 199 | assert.equal(channel.contractId, adMarket.address) 200 | assert.equal(channel.channelId, channelId) 201 | assert.equal(channel.demand, demand) 202 | assert.equal(channel.supply, supply) 203 | assert.equal(parseInt(channel.root, 16), 0) 204 | assert.equal(channel.state, 0) 205 | assert.equal(channel.expiration, expiration) 206 | assert.equal(channel.challengeTimeout, 0) 207 | assert.equal(parseInt(channel.proposedRoot, 16), 0) 208 | }) 209 | }) 210 | 211 | describe('[with channel open]', () => { 212 | let demand, supply, demandUrl, supplyUrl, channelId, channel 213 | 214 | before(async () => { 215 | snapshots.push(await takeSnapshot()) 216 | demand = accounts[1] 217 | supply = accounts[2] 218 | demandUrl = 'foo' 219 | supplyUrl = 'bar' 220 | channelId = solSha3(0) 221 | await adMarket.registerDemand(demand, demandUrl) 222 | await adMarket.registerSupply(supply, supplyUrl) 223 | await adMarket.openChannel(supply, { from: demand }) 224 | }) 225 | 226 | beforeEach(async () => { 227 | snapshots.push(await takeSnapshot()) 228 | channel = parseChannel(await adMarket.getChannel(channelId)) 229 | filter = web3.eth.filter({ address: adMarket.address, fromBlock: 0 }) 230 | }) 231 | 232 | afterEach(async () => { 233 | await p(filter.stopWatching.bind(filter))() 234 | await revertSnapshot(snapshots.pop()) 235 | }) 236 | 237 | after(async () => { 238 | await revertSnapshot(snapshots.pop()) 239 | }) 240 | 241 | it('proposeCheckpoint -- renew', async () => { 242 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 243 | const challengePeriod = parseBN((await p(adMarket.challengePeriod)())[0]) 244 | const challengeTimeout = blockNumber + challengePeriod + 1 245 | 246 | const proposedRoot = solSha3('wut') 247 | channel.root = proposedRoot 248 | const fingerprint = getFingerprint(channel) 249 | const sig = await p(web3.eth.sign)(demand, fingerprint) 250 | await adMarket.proposeCheckpoint( 251 | channelId, proposedRoot, sig, true, { from: demand } 252 | ) 253 | 254 | const updatedChannel = parseChannel(await adMarket.getChannel(channelId)) 255 | assert.equal(updatedChannel.state, 1) 256 | assert.equal(updatedChannel.challengeTimeout, challengeTimeout) 257 | assert.equal(updatedChannel.proposedRoot, proposedRoot) 258 | }) 259 | 260 | it('proposeCheckpoint -- close', async () => { 261 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 262 | const challengePeriod = parseBN((await p(adMarket.challengePeriod)())[0]) 263 | const challengeTimeout = blockNumber + challengePeriod + 1 264 | 265 | const proposedRoot = solSha3('wut') 266 | channel.root = proposedRoot 267 | const fingerprint = getFingerprint(channel) 268 | const sig = await p(web3.eth.sign)(demand, fingerprint) 269 | await adMarket.proposeCheckpoint( 270 | channelId, proposedRoot, sig, false, { from: demand } 271 | ) 272 | 273 | const updatedChannel = parseChannel(await adMarket.getChannel(channelId)) 274 | assert.equal(updatedChannel.state, 2) 275 | assert.equal(updatedChannel.challengeTimeout, challengeTimeout) 276 | assert.equal(updatedChannel.proposedRoot, proposedRoot) 277 | }) 278 | 279 | it('checkpointChannel -- renew', async () => { 280 | const proposedRoot = solSha3('wut') 281 | channel.root = proposedRoot 282 | const fingerprint = getFingerprint(channel) 283 | const sig = await p(web3.eth.sign)(demand, fingerprint) 284 | await adMarket.proposeCheckpoint( 285 | channelId, proposedRoot, sig, true, { from: demand } 286 | ) 287 | 288 | await mineBlocks(CHALLENGE_PERIOD) 289 | 290 | await adMarket.checkpointChannel(channelId, { from: demand }) 291 | 292 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 293 | const expiration = blockNumber + CHANNEL_TIMEOUT 294 | 295 | const updatedChannel = parseChannel(await adMarket.getChannel(channelId)) 296 | 297 | assert.equal(updatedChannel.state, 0) 298 | assert.equal(updatedChannel.challengeTimeout, 0) 299 | assert.equal(updatedChannel.expiration, expiration) 300 | assert.equal(updatedChannel.root, proposedRoot) 301 | assert.equal(updatedChannel.proposedRoot, 0) 302 | }) 303 | 304 | it('checkpointChannel -- close', async () => { 305 | const proposedRoot = solSha3('wut') 306 | channel.root = proposedRoot 307 | const fingerprint = getFingerprint(channel) 308 | const sig = await p(web3.eth.sign)(demand, fingerprint) 309 | await adMarket.proposeCheckpoint( 310 | channelId, proposedRoot, sig, false, { from: demand } 311 | ) 312 | 313 | await mineBlocks(CHALLENGE_PERIOD) 314 | 315 | await adMarket.checkpointChannel(channelId, { from: demand }) 316 | 317 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 318 | const expiration = blockNumber + CHANNEL_TIMEOUT 319 | 320 | const updatedChannel = parseChannel(await adMarket.getChannel(channelId)) 321 | 322 | assert.equal(updatedChannel.state, 3) 323 | assert.equal(updatedChannel.challengeTimeout, 0) 324 | assert.equal(updatedChannel.expiration, channel.expiration) 325 | assert.equal(updatedChannel.root, proposedRoot) 326 | assert.equal(updatedChannel.proposedRoot, 0) 327 | }) 328 | 329 | it('challengeCheckpoint', async () => { 330 | const update = { 331 | impressionId: web3.sha3('bar'), 332 | price: 2 333 | } 334 | 335 | const updatedChannel = makeUpdate(makeChannel(channel), update) 336 | const proposedRoot = updatedChannel.get('root') 337 | const fingerprint = getFingerprint(updatedChannel) 338 | const sig = await p(web3.eth.sign)(demand, fingerprint) 339 | await adMarket.proposeCheckpoint( 340 | channelId, proposedRoot, sig, true, { from: demand } 341 | ) 342 | 343 | const proposedCheckpointChannel = parseChannel(await adMarket.getChannel(channelId)) 344 | assert.equal(proposedCheckpointChannel.proposedRoot, proposedRoot) 345 | 346 | const update2 = { 347 | impressionId: web3.sha3('bar'), 348 | price: 2 349 | } 350 | 351 | const updatedChannel2 = makeUpdate(updatedChannel, update2) 352 | const fingerprint2 = getFingerprint(updatedChannel2) 353 | const sig2 = await p(web3.eth.sign)(demand, fingerprint2) 354 | 355 | const root = updatedChannel2.get('root') 356 | const leaves = getLeaves(updatedChannel2, updatedChannel2.get('prevRoot')) 357 | const impressionsLeaf = leaves[2] 358 | const tree = new MerkleTree(leaves, true) 359 | const index = 3 360 | const proof = tree.getProofOrdered(impressionsLeaf, index, true) 361 | 362 | await adMarket.challengeCheckpoint( 363 | channelId, root, 2, 3, proof, sig2, { from: supply } 364 | ) 365 | 366 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 367 | const challengeTimeout = blockNumber + CHALLENGE_PERIOD 368 | 369 | const challenge = parseChallenge(await adMarket.getChallenge(channelId)) 370 | const challengedChannel = parseChannel(await adMarket.getChannel(channelId)) 371 | assert.equal(challenge.challengeRoot, root) 372 | assert.equal(challenge.impressions, 2) 373 | assert.equal(challengedChannel.challengeTimeout, challengeTimeout) 374 | }) 375 | 376 | it('acceptChallenge', async () => { 377 | // proposeCheckpoint with 2 impressions 378 | // challengeCheckpoint with 1 impression 379 | // acceptChallenge with 2 impressions 380 | 381 | const index = 3 // index of impressions # in the leaves array 382 | 383 | // create update1, use for challengeCheckpoint 384 | const update1 = { 385 | impressionId: web3.sha3('bar'), 386 | price: 2 387 | } 388 | const updatedChannel1 = makeUpdate(makeChannel(channel), update1) 389 | const root1 = updatedChannel1.get('root') 390 | const fingerprint1 = getFingerprint(updatedChannel1) 391 | const sig1 = await p(web3.eth.sign)(demand, fingerprint1) 392 | 393 | // proposeCheckpoint with update2 394 | const update2 = { 395 | impressionId: web3.sha3('bar'), 396 | price: 3 397 | } 398 | const updatedChannel2 = makeUpdate(updatedChannel1, update2) 399 | const root2 = updatedChannel2.get('root') 400 | const fingerprint2 = getFingerprint(updatedChannel2) 401 | const sig2 = await p(web3.eth.sign)(demand, fingerprint2) 402 | 403 | await adMarket.proposeCheckpoint( 404 | channelId, root2, sig2, true, { from: supply } 405 | ) 406 | 407 | // challengeCheckpoint with update1 408 | const leaves1 = getLeaves(updatedChannel1, updatedChannel1.get('prevRoot')) 409 | const impressionsLeaf1 = leaves1[2] 410 | const tree1 = new MerkleTree(leaves1, true) 411 | const proof1 = tree1.getProofOrdered(impressionsLeaf1, index, true) 412 | 413 | await adMarket.challengeCheckpoint( 414 | channelId, root1, 1, index, proof1, sig1, { from: demand } 415 | ) 416 | 417 | // acceptChallenge with update2 418 | const leaves2 = getLeaves(updatedChannel2, updatedChannel2.get('prevRoot')) 419 | const impressionsLeaf2 = leaves2[2] 420 | const tree2 = new MerkleTree(leaves2, true) 421 | const proof2 = tree2.getProofOrdered(impressionsLeaf2, index, true) 422 | 423 | await adMarket.acceptChallenge( 424 | channelId, 2, index, proof2, { from: supply } 425 | ) 426 | 427 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 428 | const expiration = blockNumber + CHANNEL_TIMEOUT 429 | 430 | const finalChannel = parseChannel(await adMarket.getChannel(channelId)) 431 | const finalChallenge = parseChallenge(await adMarket.getChallenge(channelId)) 432 | 433 | assert.equal(finalChannel.state, 0) 434 | assert.equal(finalChannel.expiration, expiration) 435 | assert.equal(finalChannel.root, root2) 436 | assert.equal(finalChannel.proposedRoot, 0) 437 | assert.equal(finalChannel.challengeTimeout, 0) 438 | assert.equal(finalChallenge.challengeRoot, 0) 439 | assert.equal(finalChallenge.impressions, 0) 440 | }) 441 | 442 | it('checkpointChannel -- after a valid challenge', async () => { 443 | // TODO get rid of the unhandled promise rejection warning 444 | // proposeCheckpoint with 1 impressions 445 | // challengeCheckpoint with 2 impression 446 | // acceptChallengeCheckpoint with 1 impression (fails) 447 | // checkpointChannel with the challenge unanswered 448 | 449 | const index = 3 // index of impressions # in the leaves array 450 | 451 | // proposeCheckpoint with update1 452 | const update1 = { 453 | impressionId: web3.sha3('bar'), 454 | price: 2 455 | } 456 | const updatedChannel1 = makeUpdate(makeChannel(channel), update1) 457 | const root1 = updatedChannel1.get('root') 458 | const fingerprint1 = getFingerprint(updatedChannel1) 459 | const sig1 = await p(web3.eth.sign)(demand, fingerprint1) 460 | 461 | await adMarket.proposeCheckpoint( 462 | channelId, root1, sig1, true, { from: demand } 463 | ) 464 | 465 | // challengeCheckpoint with update2 466 | const update2 = { 467 | impressionId: web3.sha3('bar'), 468 | price: 3 469 | } 470 | const updatedChannel2 = makeUpdate(updatedChannel1, update2) 471 | const root2 = updatedChannel2.get('root') 472 | const fingerprint2 = getFingerprint(updatedChannel2) 473 | const sig2 = await p(web3.eth.sign)(demand, fingerprint2) 474 | 475 | const leaves2 = getLeaves(updatedChannel2, updatedChannel2.get('prevRoot')) 476 | const impressionsLeaf2 = leaves2[2] 477 | const tree2 = new MerkleTree(leaves2, true) 478 | const proof2 = tree2.getProofOrdered(impressionsLeaf2, index, true) 479 | 480 | await adMarket.challengeCheckpoint( 481 | channelId, root2, 2, index, proof2, sig2, { from: supply } 482 | ) 483 | 484 | // acceptChallenge with update1 (fails) 485 | const leaves1 = getLeaves(updatedChannel1, updatedChannel1.get('prevRoot')) 486 | const impressionsLeaf1 = leaves1[2] 487 | const tree1 = new MerkleTree(leaves1, true) 488 | const proof1 = tree1.getProofOrdered(impressionsLeaf1, index, true) 489 | 490 | try { 491 | await adMarket.acceptChallenge( 492 | channelId, 2, index, proof1, { from: demand } 493 | ) 494 | } catch (err) { 495 | assert.equal(err.value.message, 'VM Exception while processing transaction: invalid opcode') 496 | assert.equal(err.value.code, -32000) 497 | } 498 | 499 | await mineBlocks(CHALLENGE_PERIOD) 500 | 501 | // checkpointChannel after challenge period expires 502 | await adMarket.checkpointChannel(channelId, { from: supply }) 503 | 504 | const blockNumber = await p(web3.eth.getBlockNumber.bind(web3.eth))() 505 | const expiration = blockNumber + CHANNEL_TIMEOUT 506 | 507 | const finalChannel = parseChannel(await adMarket.getChannel(channelId)) 508 | const finalChallenge = parseChallenge(await adMarket.getChallenge(channelId)) 509 | 510 | assert.equal(finalChannel.state, 0) 511 | assert.equal(finalChannel.challengeTimeout, 0) 512 | assert.equal(finalChannel.expiration, expiration) 513 | assert.equal(finalChannel.root, root2) 514 | assert.equal(finalChannel.proposedRoot, 0) 515 | assert.equal(finalChallenge.challengeRoot, 0) 516 | assert.equal(finalChallenge.impressions, 0) 517 | }) 518 | }) 519 | }) 520 | --------------------------------------------------------------------------------