├── .gitignore ├── client ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── icons │ │ ├── mix.png │ │ ├── churn.png │ │ ├── escrow.png │ │ └── joint-account.png │ ├── manifest.json │ └── index.html ├── src │ ├── config.js │ ├── setupTests.js │ ├── index.js │ ├── App.test.js │ ├── index.css │ ├── model │ │ ├── Cryptography │ │ │ ├── NanoPoWGenerator.js │ │ │ ├── CryptoUtils.js │ │ │ ├── AccountFinder.js │ │ │ ├── NanoAmountConverter.js │ │ │ ├── BlockBuilder.js │ │ │ └── BlockSigner.js │ │ ├── Client │ │ │ ├── BaseClient.js │ │ │ └── SignatureDataCodec.js │ │ ├── EventTypes │ │ │ ├── JointAccountEventTypes.js │ │ │ └── MixEventTypes.js │ │ ├── WebSocketBuilder.js │ │ ├── Phases │ │ │ ├── PhaseTracker.js │ │ │ ├── MixBuildTransactionPathsPhase.js │ │ │ ├── SignTransactionPhaseFactory.js │ │ │ ├── BasePhase.js │ │ │ ├── MixBuildAccountTreePhase.js │ │ │ ├── MixAnnouncePubKeysPhase.js │ │ │ ├── MixCreateLeafSendBlocksPhase.js │ │ │ ├── MixSignTransactionsPhase.js │ │ │ ├── SignTransaction │ │ │ │ ├── BaseSigningPhase.js │ │ │ │ ├── SignTransactionAnnounceRCommitmentPhase.js │ │ │ │ ├── SignTransactionAnnounceRPointPhase.js │ │ │ │ └── SignTransactionAnnounceSignatureContributionPhase.js │ │ │ ├── MixPhaseFactory.js │ │ │ ├── MixAnnounceLeafSendBlocksPhase.js │ │ │ └── MixAnnounceOutputsPhase.js │ │ ├── MixLogic │ │ │ ├── AccountNode.js │ │ │ ├── SignatureComponentStore.js │ │ │ └── AccountTree.js │ │ ├── SessionClient.js │ │ ├── Factory.js │ │ └── NanoNode │ │ │ └── NanoNodeClient.js │ ├── components │ │ ├── Session │ │ │ ├── AccountDetails.js │ │ │ ├── QRCodeImg.js │ │ │ ├── SessionActionCard.js │ │ │ ├── InviteModal.js │ │ │ ├── ChooseSessionAction.js │ │ │ └── UseJointAccount.js │ │ └── pages │ │ │ ├── Session.js │ │ │ └── Home.js │ ├── App.js │ ├── tests │ │ ├── Mocks │ │ │ ├── MockStandardClass.js │ │ │ └── MockSessionClient.js │ │ ├── Cryptography │ │ │ ├── NanoPoWGenerator.test.js │ │ │ ├── AccountFinder.test.js │ │ │ ├── NanoAmountConverter.test.js │ │ │ ├── BlockSigner.test.js │ │ │ └── BlockBuilder.test.js │ │ ├── Client │ │ │ ├── MixSessionClient.test.js │ │ │ └── SignatureDataCodec.test.js │ │ ├── Phases │ │ │ └── MixBuildAccountTreePhase.test.js │ │ └── MixLogic │ │ │ └── AccountTree.test.js │ ├── App.css │ ├── logo.svg │ └── serviceWorker.js ├── .gitignore ├── package.json └── README.md ├── ideas.txt ├── experiments ├── package.json ├── derive-private-key.js ├── sample-data.txt ├── package-lock.json └── testblocksigning.js ├── src ├── Session.js ├── RandomStringGenerator.js └── SessionMananger.js ├── package.json ├── LICENSE ├── GettingStarted.md ├── server.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | experiments/node_modules/ 3 | client/node_modules/ 4 | .idea/ 5 | 6 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unyieldinggrace/nanofusion/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/icons/mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unyieldinggrace/nanofusion/HEAD/client/public/icons/mix.png -------------------------------------------------------------------------------- /client/public/icons/churn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unyieldinggrace/nanofusion/HEAD/client/public/icons/churn.png -------------------------------------------------------------------------------- /client/public/icons/escrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unyieldinggrace/nanofusion/HEAD/client/public/icons/escrow.png -------------------------------------------------------------------------------- /client/public/icons/joint-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unyieldinggrace/nanofusion/HEAD/client/public/icons/joint-account.png -------------------------------------------------------------------------------- /client/src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // need to use port 5000 directly, Chrome doesn't like proxying websocket connections through the dev server 3 | baseURL: 'http://localhost:5000', 4 | baseWebSocketURL: 'ws://localhost:5000', 5 | nanoNodeAPIURL: 'http://nanofusion.casa:7076' 6 | }; -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | 5 | import './index.css'; 6 | import App from './App'; 7 | 8 | render(( 9 | 10 | 11 | 12 | ), document.getElementById('root')); 13 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /ideas.txt: -------------------------------------------------------------------------------- 1 | # DEX with Nano as Base Asset (ala Bisq). 2 | 3 | I investigated Bisq as a way to buy Monero. However, having to pay BTC tx fees makes it a bit less attractive. 4 | I'm fairly certain that a similar system could be built using NanoFusion escrow principles. 5 | 6 | See here for more details: https://bisq.wiki/Dispute_resolution 7 | -------------------------------------------------------------------------------- /experiments/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aggsigjs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "testblocksigning.js", 6 | "scripts": {}, 7 | "author": "", 8 | "license": "MIT", 9 | "dependencies": { 10 | "bn.js": "^5.1.1", 11 | "elliptic": "^6.5.3", 12 | "nanocurrency": "^2.4.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/Session.js: -------------------------------------------------------------------------------- 1 | const RandomStringGenerator = require('./RandomStringGenerator'); 2 | 3 | class Session { 4 | constructor(type) { 5 | this.type = type; 6 | this.ID = RandomStringGenerator.prototype.generateRandomString(); 7 | this.clients = []; 8 | this.nextClientID = 0; 9 | } 10 | 11 | getNextClientID() { 12 | this.nextClientID++; 13 | return this.nextClientID; 14 | } 15 | } 16 | 17 | module.exports = Session; 18 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/model/Cryptography/NanoPoWGenerator.js: -------------------------------------------------------------------------------- 1 | class NanoPoWGenerator { 2 | 3 | // TODO: replace this with an implementation that uses WebGL shaders in the browser to distribute the load among 4 | // mix clients. See: https://github.com/numtel/nano-webgl-pow 5 | async GenerateWork(frontierBlockHashOrAccountPublicKey) { 6 | throw new Error('Not yet implemented.'); 7 | } 8 | 9 | } 10 | 11 | export default NanoPoWGenerator; 12 | -------------------------------------------------------------------------------- /src/RandomStringGenerator.js: -------------------------------------------------------------------------------- 1 | class RandomStringGenerator { 2 | generateRandomString(length) { 3 | length = length ? length : 8; 4 | let charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 5 | let result = ""; 6 | 7 | for (var i = 0; i < length; ++i) { 8 | result += charSet.charAt(Math.round(Math.random() * charSet.length)); 9 | } 10 | 11 | return result; 12 | } 13 | } 14 | 15 | module.exports = RandomStringGenerator; 16 | -------------------------------------------------------------------------------- /client/src/model/Client/BaseClient.js: -------------------------------------------------------------------------------- 1 | import JointAccountEventTypes from "../EventTypes/JointAccountEventTypes"; 2 | 3 | class BaseClient { 4 | constructor(sessionClient) { 5 | this.sessionClient = sessionClient; 6 | this.onStateUpdatedCallback = null; 7 | } 8 | 9 | OnStateUpdated(callback) { 10 | this.onStateUpdatedCallback = callback; 11 | } 12 | 13 | notifyStateChange(state) { 14 | if (this.onStateUpdatedCallback) { 15 | this.onStateUpdatedCallback(state); 16 | } 17 | } 18 | 19 | } 20 | 21 | export default BaseClient; 22 | -------------------------------------------------------------------------------- /client/src/model/EventTypes/JointAccountEventTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | PeerDisconnected: 'PeerDisconnected', 3 | ReadyToUseJointAccount: 'ReadyToUseJointAccount', 4 | RequestForPublicKey: 'RequestForPublicKey', 5 | RequestForRCommitment: 'RequestForRCommitment', 6 | ProvideRCommitment: 'ProvideRCommitment', 7 | RequestForRPoint: 'RequestForRPoint', 8 | ProvideRPoint: 'ProvideRPoint', 9 | RequestForSignatureContribution: 'RequestForSignatureContribution', 10 | ProvideSignatureContribution: 'ProvideSignatureContribution', 11 | ProposeJointAccountTransaction: 'ProposeJointAccountTransaction' 12 | }; -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/src/components/Session/AccountDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class AccountDetails extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | NanoAddress: null, 8 | Amount: null 9 | }; 10 | 11 | this.onNanoAddressChanged = this.onNanoAddressChanged.bind(this); 12 | this.onAmountChanged = this.onAmountChanged.bind(this); 13 | } 14 | 15 | render() { 16 | return ( 17 | <> 18 | Current Balance: 0
19 | Pending Balance: 0.01 20 | Details of block to receive that pending block... 21 | 22 | ); 23 | } 24 | } 25 | export default AccountDetails; 26 | -------------------------------------------------------------------------------- /client/src/components/Session/QRCodeImg.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import * as QR from 'qrcode-generator'; 3 | 4 | class QRCodeImg extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {}; 8 | } 9 | 10 | render() { 11 | if (!this.props.NanoAddress) { 12 | return null; 13 | } 14 | 15 | let typeNumber = 4; 16 | let errorCorrectionLevel = 'L'; 17 | let qr = QR(typeNumber, errorCorrectionLevel); 18 | qr.addData(this.props.NanoAddress); 19 | qr.make(); 20 | 21 | return ( 22 | QR Code 23 | ); 24 | } 25 | } 26 | export default QRCodeImg; 27 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 3 | import './bootstrap/dist/css/bootstrap.min.css'; 4 | import './App.css'; 5 | import Home from './components/pages/Home'; 6 | import Session from './components/pages/Session'; 7 | 8 | class App extends Component { 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanofusion", 3 | "version": "0.0.3", 4 | "description": "Create joint-accounts and mix funds using the Nano crytocurrency.", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "ava", 8 | "start": "node server.js", 9 | "install-start": "npm install && npm start", 10 | "debug": "node --inspect-brk server.js" 11 | }, 12 | "keywords": [ 13 | "Nano", 14 | "Privacy", 15 | "Mix", 16 | "Joint", 17 | "Account", 18 | "Cryptocurrency" 19 | ], 20 | "author": "Nick Watts", 21 | "license": "MIT", 22 | "dependencies": { 23 | "axios": "^0.19.2", 24 | "cors": "^2.8.5", 25 | "express": "^4.17.1", 26 | "express-ws": "^4.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/src/model/Cryptography/CryptoUtils.js: -------------------------------------------------------------------------------- 1 | class CryptoUtils { 2 | 3 | ByteArrayToHex(byteArray) { 4 | if (!byteArray) { 5 | return ''; 6 | } 7 | 8 | let hexStr = ''; 9 | for (let i = 0; i < byteArray.length; i++) { 10 | let hex = (byteArray[i] & 0xff).toString(16); 11 | hex = hex.length === 1 ? `0${hex}` : hex; 12 | hexStr += hex; 13 | } 14 | 15 | return hexStr.toUpperCase(); 16 | } 17 | 18 | HexToByteArray(hexString) { 19 | if (!hexString) { 20 | return new Uint8Array(); 21 | } 22 | 23 | const a = []; 24 | for (let i = 0; i < hexString.length; i += 2) { 25 | a.push(parseInt(hexString.substr(i, 2), 16)); 26 | } 27 | 28 | return new Uint8Array(a); 29 | } 30 | 31 | } 32 | 33 | export default CryptoUtils; 34 | -------------------------------------------------------------------------------- /client/src/model/EventTypes/MixEventTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | PeerDisconnected: 'PeerDisconnected', 3 | RequestPubKeys: 'RequestPubKeys', 4 | AnnouncePubKey: 'AnnouncePubKey', 5 | RequestLeafSendBlocks: 'RequestLeafSendBlocks', 6 | AnnounceLeafSendBlock: 'AnnounceLeafSendBlock', 7 | RequestOutputs: 'RequestOutputs', 8 | AnnounceOutput: 'AnnounceOutput', 9 | AnnounceAccountTreeDigest: 'AnnounceAccountTreeDigest', 10 | RequestAccountTreeDigest: 'AnnounceAccountTreeDigest', 11 | 12 | RequestRCommitments: 'RequestRCommitments', 13 | AnnounceRCommitment: 'AnnounceRCommitment', 14 | RequestRPoints: 'RequestRPoints', 15 | AnnounceRPoint: 'AnnounceRPoint', 16 | RequestSignatureContributions: 'RequestSignatureContributions', 17 | AnnounceSignatureContribution: 'AnnounceSignatureContribution' 18 | }; -------------------------------------------------------------------------------- /client/src/tests/Mocks/MockStandardClass.js: -------------------------------------------------------------------------------- 1 | class MockStandardClass { 2 | constructor() { 3 | this.methodCallLog = []; 4 | } 5 | 6 | LogMethodCall(name, args) { 7 | this.methodCallLog.push({'Name': name, 'Arguments': args}); 8 | } 9 | 10 | GetMethodCallOccurred(methodCall) { 11 | let result = false; 12 | 13 | this.methodCallLog.forEach((loggedMethodCall) => { 14 | if (methodCall['Name'] !== loggedMethodCall['Name']) { 15 | return true; 16 | } 17 | 18 | if (!methodCall['Arguments']) { 19 | result = true; 20 | return false; 21 | } 22 | 23 | if (JSON.stringify(methodCall) === JSON.stringify(loggedMethodCall)) { 24 | result = true; 25 | return false; 26 | } 27 | }); 28 | 29 | // console.log(this.methodCallLog); 30 | return result; 31 | } 32 | } 33 | 34 | export default MockStandardClass; 35 | -------------------------------------------------------------------------------- /client/src/components/Session/SessionActionCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Col, Card } from 'react-bootstrap'; 3 | 4 | class SessionActionCard extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {}; 8 | 9 | this.onCardClickedInternal = this.onCardClickedInternal.bind(this); 10 | } 11 | 12 | onCardClickedInternal() { 13 | if (this.props.onCardClicked) { 14 | this.props.onCardClicked.call(); 15 | } 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 | 22 | 23 | 24 | {this.props.CardTitle} 25 | 26 | {this.props.children} 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | export default SessionActionCard; 35 | -------------------------------------------------------------------------------- /client/src/model/Cryptography/AccountFinder.js: -------------------------------------------------------------------------------- 1 | import * as NanoCurrency from 'nanocurrency'; 2 | 3 | class AccountFinder { 4 | constructor() { 5 | this.MAX_UNUSED_ACCOUNTS_TO_CHECK = 100; 6 | } 7 | 8 | GetPrivateKeyForAccount(accountSeed, nanoAddress) { 9 | let numUnusedAccount = 0; 10 | let addressIndex = 0; 11 | 12 | let privateKey; 13 | let publicKey; 14 | let address; 15 | 16 | while (true) { 17 | try { 18 | privateKey = NanoCurrency.deriveSecretKey(accountSeed, addressIndex); 19 | publicKey = NanoCurrency.derivePublicKey(privateKey); 20 | address = NanoCurrency.deriveAddress(publicKey, {useNanoPrefix: true}); 21 | } catch (error) { 22 | return null; 23 | } 24 | 25 | if (address === nanoAddress) { 26 | return privateKey; 27 | } 28 | 29 | addressIndex++; 30 | numUnusedAccount++; 31 | 32 | if (numUnusedAccount > this.MAX_UNUSED_ACCOUNTS_TO_CHECK) { 33 | return null; 34 | } 35 | } 36 | } 37 | 38 | } 39 | 40 | export default AccountFinder; 41 | -------------------------------------------------------------------------------- /experiments/derive-private-key.js: -------------------------------------------------------------------------------- 1 | const NanoCurrency = require('nanocurrency'); 2 | 3 | console.log(NanoCurrency.deriveSecretKey('42B6E48F67470FBA286B4A2D3D13CA85A3A24BBFB70DFAD329D089D5A294BBED', 0)); 4 | console.log(NanoCurrency.deriveSecretKey('42B6E48F67470FBA286B4A2D3D13CA85A3A24BBFB70DFAD329D089D5A294BBED', 1)); 5 | console.log(NanoCurrency.deriveSecretKey('42B6E48F67470FBA286B4A2D3D13CA85A3A24BBFB70DFAD329D089D5A294BBED', 2)); 6 | console.log(NanoCurrency.deriveSecretKey('42B6E48F67470FBA286B4A2D3D13CA85A3A24BBFB70DFAD329D089D5A294BBED', 3)); 7 | 8 | console.log(NanoCurrency.deriveSecretKey('194B244C86E7E8F653BCC610D353958FE8ED61D4B7BA2FB6B12FEBD38EE4E3A5', 0)); 9 | console.log(NanoCurrency.deriveSecretKey('194B244C86E7E8F653BCC610D353958FE8ED61D4B7BA2FB6B12FEBD38EE4E3A5', 1)); 10 | console.log(NanoCurrency.deriveSecretKey('194B244C86E7E8F653BCC610D353958FE8ED61D4B7BA2FB6B12FEBD38EE4E3A5', 2)); 11 | console.log(NanoCurrency.deriveSecretKey('194B244C86E7E8F653BCC610D353958FE8ED61D4B7BA2FB6B12FEBD38EE4E3A5', 3)); 12 | -------------------------------------------------------------------------------- /client/src/model/WebSocketBuilder.js: -------------------------------------------------------------------------------- 1 | import config from "../config"; 2 | 3 | class WebSocketBuilder { 4 | buildWebSocket(relativeURLPath, sessionID, onMessageCallback) { 5 | let socket = new WebSocket(config.baseWebSocketURL + relativeURLPath); 6 | 7 | socket.onopen = function(e) { 8 | console.log("[open] Connection established"); 9 | console.log("Sending join-session request to server..."); 10 | this.send(JSON.stringify({ 11 | 'MessageType': 'JoinSession', 12 | 'SessionID': sessionID 13 | })); 14 | }; 15 | 16 | socket.onmessage = onMessageCallback; 17 | 18 | socket.onclose = function(event) { 19 | if (event.wasClean) { 20 | console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); 21 | } else { 22 | // e.g. server process killed or network down 23 | // event.code is usually 1006 in this case 24 | console.log('[close] Connection died'); 25 | } 26 | }; 27 | 28 | socket.onerror = function(error) { 29 | console.log(`[error] ${error.message}`); 30 | }; 31 | 32 | return socket; 33 | } 34 | } 35 | 36 | export default WebSocketBuilder; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nick Watts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/model/Phases/PhaseTracker.js: -------------------------------------------------------------------------------- 1 | class PhaseTracker { 2 | constructor() { 3 | this.phases = []; 4 | this.stateUpdateEmittedCallback = null; 5 | this.latestState = null; 6 | } 7 | 8 | AddPhase(phase) { 9 | this.phases.push(phase); 10 | } 11 | 12 | ExecutePhases(state) { 13 | this.phases.forEach((phase) => { 14 | if (phase.IsReady() && !phase.IsComplete()) { 15 | phase.SetPhaseCompletedCallback(this.onPhaseCompleted.bind(this)); 16 | phase.SetEmitStateUpdateCallback(this.onStateUpdateEmitted.bind(this)); 17 | phase.Execute(state); 18 | } 19 | }); 20 | } 21 | 22 | SetStateUpdateEmittedCallback(callback) { 23 | this.stateUpdateEmittedCallback = callback; 24 | } 25 | 26 | NotifyOfUpdatedState(state) { 27 | this.latestState = state; 28 | 29 | this.phases.forEach((phase) => { 30 | phase.NotifyOfUpdatedState(state); 31 | }); 32 | } 33 | 34 | onPhaseCompleted() { 35 | this.ExecutePhases(this.latestState); 36 | } 37 | 38 | onStateUpdateEmitted(newState) { 39 | if (this.stateUpdateEmittedCallback) { 40 | this.stateUpdateEmittedCallback(newState); 41 | } 42 | } 43 | } 44 | 45 | export default PhaseTracker; 46 | -------------------------------------------------------------------------------- /client/src/tests/Cryptography/NanoPoWGenerator.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as NanoCurrency from "nanocurrency"; 3 | import NanoPoWGenerator from "../../model/Cryptography/NanoPoWGenerator"; 4 | 5 | test('When GenerateWorkForBlockHash is called, then correct work is returned.', async t => { 6 | let nanoPoWGenerator = getTestObjects(); 7 | let recipientAddressPrivateKey = '2211ABAE11F9721C550FCEDFC5034CF84CB51327E1545099023098E820D0DB66'; 8 | let recipientAddressPublicKey = NanoCurrency.derivePublicKey(recipientAddressPrivateKey); 9 | 10 | // this test is commented out for the sake of speed, but uncomment 11 | // it if you have any difficulties with PoW generation. It's mainly here to document the API. 12 | 13 | // console.log(recipientAddressPublicKey); 14 | // let work = await nanoPoWGenerator.GenerateWork(recipientAddressPublicKey); 15 | 16 | // let expectedWork = '51b0c735c30e85f0'; 17 | // t.is(expectedWork, work); 18 | 19 | let workValidated = NanoCurrency.validateWork({ 20 | blockHash: recipientAddressPublicKey, 21 | work: '60dc8a2964ad65f7' // pre-generated 22 | }); 23 | 24 | t.true(workValidated); 25 | }); 26 | 27 | let getTestObjects = () => { 28 | return new NanoPoWGenerator(); 29 | } 30 | -------------------------------------------------------------------------------- /client/src/model/Phases/MixBuildTransactionPathsPhase.js: -------------------------------------------------------------------------------- 1 | import BasePhase from "./BasePhase"; 2 | 3 | class MixBuildTransactionPathsPhase extends BasePhase { 4 | constructor(blockBuilder) { 5 | super(); 6 | this.Name = 'Build Transaction Paths'; 7 | this.blockBuilder = blockBuilder; 8 | 9 | this.accountTree = null; 10 | this.outputAccounts = null; 11 | } 12 | 13 | executeInternal(state) { 14 | console.log('Mix Phase: Building transaction paths.'); 15 | this.accountTree = state.AccountTree; 16 | this.outputAccounts = state.MyOutputAccounts.concat(state.ForeignOutputAccounts); 17 | this.outputAccounts.sort((a, b) => { 18 | return a.NanoAddress.localeCompare(b.NanoAddress); 19 | }); 20 | 21 | this.accountTree.SetOutputAccounts(this.outputAccounts); 22 | 23 | console.log('Tree Dump:'); 24 | console.log(this.accountTree.GetTreeDump()); 25 | console.log(this.accountTree.Digest()); 26 | 27 | this.emitStateUpdate({ 28 | AccountTree: this.accountTree 29 | }); 30 | } 31 | 32 | async NotifyOfUpdatedState(state) { 33 | if (!!state.AccountTree && !!state.AccountTree.Digest()) { 34 | this.markPhaseCompleted(); 35 | } 36 | } 37 | 38 | } 39 | 40 | export default MixBuildTransactionPathsPhase; 41 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | .ClickableCard { 41 | cursor: pointer; 42 | margin-bottom: 1rem; 43 | } 44 | 45 | .TextWrap { 46 | text-wrap: normal; 47 | } 48 | 49 | .AccountTreeRow, .ProgressRow, .InputInfoRow { 50 | margin-bottom: 1em 51 | } 52 | 53 | table tbody.MixInputsTableBody td { 54 | padding: 0.1rem; 55 | } 56 | 57 | textarea.form-control { 58 | font-size: 0.8rem; 59 | } 60 | 61 | .AccountTreeRow table td { 62 | word-break: break-word; 63 | } 64 | 65 | .AccountTreeRow table td .NanoCrawlerLink { 66 | font-size: 0.8rem; 67 | } 68 | 69 | .MixBinaryTreeTable { 70 | margin-bottom: 0; 71 | border-bottom: 1rem solid #ccc; 72 | } -------------------------------------------------------------------------------- /client/src/components/Session/InviteModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Modal, Button } from 'react-bootstrap'; 3 | 4 | class InviteModal extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {}; 8 | 9 | this.onAcceptButtonClicked = this.onAcceptButtonClicked.bind(this); 10 | this.onCloseButtonClicked = this.onCloseButtonClicked.bind(this); 11 | } 12 | 13 | onAcceptButtonClicked() { 14 | if (this.props.onAccepted) { 15 | this.props.onAccepted.call(); 16 | } 17 | } 18 | 19 | onCloseButtonClicked() { 20 | if (this.props.onClosed) { 21 | this.props.onClosed.call(); 22 | } 23 | } 24 | 25 | render() { 26 | return ( 27 | 28 | 29 | 30 | {this.props.Title} 31 | 32 | 33 | 34 |

{this.props.children}

35 |
36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 | ); 44 | } 45 | } 46 | export default InviteModal; 47 | -------------------------------------------------------------------------------- /client/src/model/MixLogic/AccountNode.js: -------------------------------------------------------------------------------- 1 | class AccountNode { 2 | constructor(componentPublicKeysHex, nanoAddress) { 3 | this.componentPublicKeysHex = componentPublicKeysHex; 4 | this.NanoAddress = nanoAddress; 5 | 6 | this.AccountNodeLeft = null; 7 | this.AccountNodeRight = null; 8 | 9 | this.MixAmountRaw = '0'; 10 | 11 | this.IncomingLeafSendBlocks = []; 12 | 13 | this.TransactionPaths = { 14 | Success: [], 15 | RefundLeft: [], 16 | RefundRight: [], 17 | RefundBoth: [], 18 | }; 19 | } 20 | 21 | GetComponentPublicKeysHex() { 22 | return this.componentPublicKeysHex; 23 | } 24 | 25 | AddIncomingLeafSendBlock(sendBlock, amountRaw) { 26 | this.IncomingLeafSendBlocks.push({ 27 | Block: sendBlock, 28 | AmountRaw: amountRaw 29 | }); 30 | } 31 | 32 | SetMixAmountRaw(mixAmountRaw) { 33 | console.log('Setting mix raw amount: '+mixAmountRaw); 34 | this.MixAmountRaw = mixAmountRaw; 35 | } 36 | 37 | IsLeafNode() { 38 | return (this.AccountNodeLeft === null && this.AccountNodeRight === null); 39 | } 40 | 41 | GetSuccessPathSendBlock(destinationNanoAddress) { 42 | let resultBlock = null; 43 | this.TransactionPaths.Success.forEach((blockInfo) => { 44 | if (blockInfo.block.link_as_account === destinationNanoAddress) { 45 | resultBlock = blockInfo; 46 | } 47 | }); 48 | 49 | return resultBlock; 50 | } 51 | } 52 | 53 | export default AccountNode; 54 | -------------------------------------------------------------------------------- /experiments/sample-data.txt: -------------------------------------------------------------------------------- 1 | Seed 1 For Mix Demo: 42B6E48F67470FBA286B4A2D3D13CA85A3A24BBFB70DFAD329D089D5A294BBED 2 | First 4 Addresses (inputs): 3 | 0,nano_16fz4nztc4wp6ataz9x7fa4xgp1hg49a4ig46xqymmpduqwupj3imy5hhq6c 4 | 1,nano_3rgr1bzxxuup939mf4qb4o6oe85johiymq4oriodowuzdn31tqh91reg77fi 5 | 2,nano_1bhjcifu6mpz69a6rx45mc86nibirer8poawh8dq79gnj358maj5z3ae3ipy 6 | 3,nano_14odeip7msw3hfy75dosmfaotzfiqzaxty4gdekk9z7471i8zm6937upwrw6 7 | 8 | Second 4 Addresses (outputs): 9 | 4,nano_1g1tutsoskbpfz7qhymfpmgteeg7o4n38j3z6j81y9gwg8jx3kcsnx7krhd5 10 | 5,nano_1ude767onchizwt13eduwndmcaqu8mbqzckze8mqrfpxtqg9hthcyi81ayyt 11 | 6,nano_1djh7q9ax7br86bob4sysk8jxrxs13ypbhcmjbymsth8yjiabzq36ra4sf5f 12 | 7,nano_39x954678drj1r6addyej3e6dxt9tud1ck7wmcfc1ch95ydqbdcj68qx6dxk 13 | 14 | 15 | Seed 2 For Mix Demo: 194B244C86E7E8F653BCC610D353958FE8ED61D4B7BA2FB6B12FEBD38EE4E3A5 16 | First 4 Addresses: 17 | 0,nano_3z1scpktndkphq9h3pewktgwxxxjh9ptqcg9midf4fk9wd8wibtfgir1iuoz 18 | 1,nano_3dzwbyrwn7dqqxes98ygwngbrrpumqt1pfybeax3roib5xbt1xocfpi6ifgx 19 | 2,nano_1uw69xsdsux5d78tpkeh7ciy7p1461k1ccsikzrgffbq4nz5depcxpuo1axd 20 | 3,nano_3g99w7s6cor1k181nbha8i7to4wjcn611dxc6a5yek45bu86c8doninotfnm 21 | 4,nano_11bibi4za8b15gmrzz877qhcpfadcifka5pbkt46rrdownfse57rkf3r17qi 22 | 5,nano_37n73r4ss11r8kdfy637qjtef3d9pqdc7da8uwh66m1xgjfns6fud7rryf67 23 | 6,nano_3duzhtkdo4pmrmd8zp4hqb9t36m1d784y138b31y99pxau67nuoyzkuryamo 24 | 7,nano_1egyrf7t133yayinqsg89j9zrgkqu9yr7qwnejnh6tbgewqyqxff46ammcbj 25 | 26 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.2", 10 | "bignumber.js": "^9.0.0", 11 | "bn.js": "^5.1.1", 12 | "jsdom": "^16.2.2", 13 | "nanocurrency": "^2.4.0", 14 | "qrcode-generator": "^1.4.4", 15 | "react": "^16.13.1", 16 | "react-bootstrap": "^1.0.1", 17 | "react-dom": "^16.13.1", 18 | "react-router-dom": "^5.1.2", 19 | "react-scripts": "3.4.1", 20 | "react-youtube": "^7.11.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "ava --verbose", 26 | "eject": "react-scripts eject" 27 | }, 28 | "proxy": "http://localhost:5000", 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "ava": "^3.8.2", 46 | "deepequal": "0.0.1", 47 | "esm": "^3.2.25", 48 | "variable-diff": "^2.0.1" 49 | }, 50 | "ava": { 51 | "require": [ 52 | "esm" 53 | ], 54 | "files": [ 55 | "src/tests/**/*.test.js" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/src/model/Cryptography/NanoAmountConverter.js: -------------------------------------------------------------------------------- 1 | import BigNumber from "bignumber.js"; 2 | 3 | class NanoAmountConverter { 4 | 5 | ConvertNanoAmountToRawAmount(nanoAmount) { 6 | let bigNumNanoAmount = new BigNumber(nanoAmount); 7 | let exponent = new BigNumber(30); 8 | let conversionFactor = new BigNumber(10).pow(exponent); 9 | return bigNumNanoAmount.times(conversionFactor).toString(10); 10 | } 11 | 12 | ConvertRawAmountToNanoAmount(rawAmount) { 13 | let bigNumRawAmount = new BigNumber(rawAmount); 14 | let exponent = new BigNumber(30); 15 | let conversionFactor = new BigNumber(10).pow(exponent); 16 | return bigNumRawAmount.dividedBy(conversionFactor).toString(10); 17 | } 18 | 19 | AddRawAmounts(rawAmount1, rawAmount2) { 20 | let bigNumRawAmount1 = new BigNumber(rawAmount1); 21 | let bigNumRawAmount2 = new BigNumber(rawAmount2); 22 | 23 | return bigNumRawAmount1.plus(bigNumRawAmount2).toString(10); 24 | } 25 | 26 | SubtractSendAmount(currentBalanceInRaw, amountToSendInRaw) { 27 | let bigNumCurrentBalance = new BigNumber(currentBalanceInRaw); 28 | let bigNumAmountToSend = new BigNumber(amountToSendInRaw); 29 | 30 | return bigNumCurrentBalance.minus(bigNumAmountToSend).toString(10); 31 | } 32 | 33 | GetTransactionAmount(preTransactionBalance, postTransactionBalance) { 34 | let preTxBigNum = new BigNumber(preTransactionBalance); 35 | let postTxBigNum = new BigNumber(postTransactionBalance); 36 | 37 | let difference = postTxBigNum.minus(preTxBigNum); 38 | return this.ConvertRawAmountToNanoAmount(difference.toString(10)); 39 | } 40 | 41 | } 42 | 43 | export default NanoAmountConverter; 44 | -------------------------------------------------------------------------------- /client/src/model/Client/SignatureDataCodec.js: -------------------------------------------------------------------------------- 1 | import * as BN from 'bn.js'; 2 | 3 | class SignatureDataCodec { 4 | constructor(cryptoUtils, ec) { 5 | this.cryptoUtils = cryptoUtils; 6 | this.ec = ec; 7 | } 8 | 9 | EncodePublicKey(publicKey) { 10 | return this.EncodeEllipticCurvePoint(publicKey); 11 | } 12 | 13 | DecodePublicKey(publicKeyHex) { 14 | return this.DecodeEllipticCurvePoint(publicKeyHex); 15 | } 16 | 17 | EncodeRCommitment(RCommitment) { 18 | return this.EncodeBigNum(RCommitment); 19 | } 20 | 21 | DecodeRCommitment(RCommitmentHex) { 22 | return this.DecodeBigNum(RCommitmentHex); 23 | } 24 | 25 | EncodeRPoint(RPoint) { 26 | return this.EncodeEllipticCurvePoint(RPoint); 27 | } 28 | 29 | DecodeRPoint(RPointHex) { 30 | return this.DecodeEllipticCurvePoint(RPointHex); 31 | } 32 | 33 | EncodeSignatureContribution(signatureContribution) { 34 | return this.EncodeBigNum(signatureContribution); 35 | } 36 | 37 | DecodeSignatureContribution(signatureContributionHex) { 38 | return this.DecodeBigNum(signatureContributionHex); 39 | } 40 | 41 | EncodeEllipticCurvePoint(pubKeyPoint) { 42 | return this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(pubKeyPoint)); 43 | } 44 | 45 | DecodeEllipticCurvePoint(pubKeyHex) { 46 | let byteArray = this.cryptoUtils.HexToByteArray(pubKeyHex); 47 | let jsArray = Array.from(byteArray); 48 | 49 | return this.ec.decodePoint(jsArray); 50 | } 51 | 52 | EncodeBigNum(bigNum) { 53 | return bigNum.toString(16).toUpperCase(); 54 | } 55 | 56 | DecodeBigNum(bigNumHex) { 57 | return new BN.BN(bigNumHex, 16); 58 | } 59 | 60 | } 61 | 62 | export default SignatureDataCodec; 63 | -------------------------------------------------------------------------------- /client/src/model/Phases/SignTransactionPhaseFactory.js: -------------------------------------------------------------------------------- 1 | import PhaseTracker from "./PhaseTracker"; 2 | import SignTransactionAnnounceRCommitmentPhase from "./SignTransaction/SignTransactionAnnounceRCommitmentPhase"; 3 | import SignTransactionAnnounceRPointPhase from "./SignTransaction/SignTransactionAnnounceRPointPhase"; 4 | import SignTransactionAnnounceSignatureContributionPhase 5 | from "./SignTransaction/SignTransactionAnnounceSignatureContributionPhase"; 6 | 7 | class SignTransactionPhaseFactory { 8 | constructor(sessionClient, signatureDataCodec, blockSigner) { 9 | this.sessionClient = sessionClient; 10 | this.signatureDataCodec = signatureDataCodec; 11 | this.blockSigner = blockSigner; 12 | } 13 | 14 | BuildPhaseTracker(messageToSign) { 15 | let phaseTracker = new PhaseTracker(); 16 | let announceRCommitmentPhase = new SignTransactionAnnounceRCommitmentPhase(this.sessionClient, this.signatureDataCodec, this.blockSigner, messageToSign); 17 | 18 | let announceRPointPhase = new SignTransactionAnnounceRPointPhase(this.sessionClient, this.signatureDataCodec, this.blockSigner, messageToSign); 19 | announceRPointPhase.SetPrerequisitePhases([announceRCommitmentPhase]); 20 | 21 | let announceSignatureContributionPhase = new SignTransactionAnnounceSignatureContributionPhase(this.sessionClient, this.signatureDataCodec, this.blockSigner, messageToSign); 22 | announceSignatureContributionPhase.SetPrerequisitePhases([announceRPointPhase]); 23 | 24 | phaseTracker.AddPhase(announceRCommitmentPhase); 25 | phaseTracker.AddPhase(announceRPointPhase); 26 | phaseTracker.AddPhase(announceSignatureContributionPhase); 27 | 28 | return phaseTracker; 29 | } 30 | } 31 | 32 | export default SignTransactionPhaseFactory; 33 | -------------------------------------------------------------------------------- /client/src/model/Phases/BasePhase.js: -------------------------------------------------------------------------------- 1 | class BasePhase { 2 | constructor() { 3 | this.prerequisitePhases = []; 4 | this.phaseCompletedCallback = null; 5 | this.emitStateUpdateCallback = null; 6 | 7 | this.PhaseStatus = { 8 | READY: 0, 9 | RUNNING: 1, 10 | COMPLETED: 2 11 | }; 12 | 13 | this.currentStatus = this.PhaseStatus.READY; 14 | } 15 | 16 | SetPrerequisitePhases(phases) { 17 | this.prerequisitePhases = phases; 18 | } 19 | 20 | SetPhaseCompletedCallback(callback) { 21 | this.phaseCompletedCallback = callback; 22 | } 23 | 24 | SetEmitStateUpdateCallback(callback) { 25 | this.emitStateUpdateCallback = callback; 26 | } 27 | 28 | async Execute(state) { 29 | this.currentStatus = this.PhaseStatus.RUNNING; 30 | this.executeInternal(state); 31 | } 32 | 33 | executeInternal(state) { 34 | this.markPhaseCompleted(); 35 | } 36 | 37 | async NotifyOfUpdatedState(state) { 38 | } 39 | 40 | emitStateUpdate(newState) { 41 | if (this.emitStateUpdateCallback) { 42 | this.emitStateUpdateCallback(newState); 43 | } 44 | } 45 | 46 | IsReady() { 47 | if (this.currentStatus !== this.PhaseStatus.READY) { 48 | return false; 49 | } 50 | 51 | let result = true; 52 | 53 | this.prerequisitePhases.forEach((phase) => { 54 | if (!phase.IsComplete()) { 55 | result = false; 56 | return false; 57 | } 58 | }); 59 | 60 | return result; 61 | } 62 | 63 | IsRunning() { 64 | return (this.currentStatus === this.PhaseStatus.RUNNING) 65 | } 66 | 67 | IsComplete() { 68 | return (this.currentStatus === this.PhaseStatus.COMPLETED); 69 | } 70 | 71 | markPhaseCompleted() { 72 | this.currentStatus = this.PhaseStatus.COMPLETED; 73 | 74 | if (this.phaseCompletedCallback) { 75 | this.phaseCompletedCallback(); 76 | } 77 | } 78 | } 79 | 80 | export default BasePhase; 81 | -------------------------------------------------------------------------------- /client/src/tests/Mocks/MockSessionClient.js: -------------------------------------------------------------------------------- 1 | import deepEqual from 'deepequal'; 2 | import variableDiff from 'variable-diff'; 3 | 4 | class MockSessionClient { 5 | constructor() { 6 | this.sessionID = null; 7 | this.callbacks = {}; 8 | this.nextCallbackID = 0; 9 | this.sendEventLog = []; 10 | } 11 | 12 | ConnectToSession(SessionID) { 13 | this.sessionID = SessionID; 14 | } 15 | 16 | Disconnect() { 17 | this.sessionID = null; 18 | } 19 | 20 | SubscribeToEvent(event, callback) { 21 | if (!this.callbacks[event]) { 22 | this.callbacks[event] = {}; 23 | } 24 | 25 | let callbackID = this.nextCallbackID; 26 | this.nextCallbackID++; 27 | 28 | this.callbacks[event][callbackID] = callback; 29 | return callbackID; 30 | } 31 | 32 | UnsubscribeFromAllEvents() { 33 | this.callbacks = {}; 34 | } 35 | 36 | SendEvent(eventType, data) { 37 | this.sendEventLog.push({ 38 | EventType: eventType, 39 | Data: data 40 | }); 41 | } 42 | 43 | EmitMockEvent(event, data) { 44 | if (!this.callbacks[event]) { 45 | console.log("No callbacks registered for event: "+event); 46 | return; 47 | } 48 | 49 | let self = this; 50 | let keys = Object.keys(this.callbacks[event]); 51 | keys.forEach(function (key) { 52 | self.callbacks[event][key](data); 53 | }); 54 | } 55 | 56 | GetEventSent(event) { 57 | let result = false; 58 | 59 | // console.log('Expected Event:'); 60 | // console.log(event); 61 | // console.log('Logged Events:'); 62 | 63 | this.sendEventLog.forEach((loggedEvent) => { 64 | if (deepEqual(event, loggedEvent)) { 65 | result = true; 66 | return false; 67 | } 68 | 69 | // console.log(variableDiff(event, loggedEvent)); 70 | // console.log(loggedEvent); 71 | }); 72 | 73 | return result; 74 | } 75 | 76 | } 77 | 78 | export default MockSessionClient; 79 | -------------------------------------------------------------------------------- /client/src/tests/Client/MixSessionClient.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import SignatureDataCodec from "../../model/Client/SignatureDataCodec"; 3 | import * as BN from 'bn.js'; 4 | import Factory from "../../model/Factory"; 5 | 6 | test('When public key point is encoded, then decoded, result matches original point.', async t => { 7 | let signatureDataCodec = getTestObjects(); 8 | let privateKey = 'D0965AD27E3E096F10F0B1775C8DD38E44F5C53A042C07D778E4C2229D296442'; 9 | 10 | let ec = getEC(); 11 | let keyPair = ec.keyFromSecret(privateKey); 12 | let publicKey = ec.decodePoint(keyPair.pubBytes()); 13 | 14 | let pubKeyHex = signatureDataCodec.EncodeEllipticCurvePoint(publicKey); 15 | let pubKeyPoint = signatureDataCodec.DecodeEllipticCurvePoint(pubKeyHex); 16 | 17 | t.deepEqual(publicKey, pubKeyPoint); 18 | t.is(pubKeyHex, signatureDataCodec.EncodeEllipticCurvePoint(pubKeyPoint)); 19 | }); 20 | 21 | test('When BigNum is encoded, then decoded, result matches original BigNum.', async t => { 22 | let signatureDataCodec = getTestObjects(); 23 | let originalSig = new BN.BN('deadbeef', 16); 24 | let encodedSig = signatureDataCodec.EncodeBigNum(originalSig); 25 | let decodedSig = signatureDataCodec.DecodeBigNum(encodedSig); 26 | 27 | t.true(originalSig.eq(decodedSig)); 28 | }); 29 | 30 | test('When BigNum is encoded, result is uppercased.', async t => { 31 | let signatureDataCodec = getTestObjects(); 32 | let originalSig = new BN.BN('deadbeef', 16); 33 | let encodedSig = signatureDataCodec.EncodeBigNum(originalSig); 34 | 35 | t.is('DEADBEEF', encodedSig); 36 | }); 37 | 38 | let getTestObjects = () => { 39 | let factory = new Factory('test'); 40 | 41 | return new SignatureDataCodec(factory.GetCryptoUtils(), factory.GetEllipticCurveProcessor()); 42 | } 43 | 44 | let getEC = () => { 45 | let factory = new Factory('test'); 46 | return factory.GetEllipticCurveProcessor(); 47 | } 48 | -------------------------------------------------------------------------------- /client/src/tests/Client/SignatureDataCodec.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import SignatureDataCodec from "../../model/Client/SignatureDataCodec"; 3 | import * as BN from 'bn.js'; 4 | import Factory from "../../model/Factory"; 5 | 6 | test('When public key point is encoded, then decoded, result matches original point.', async t => { 7 | let signatureDataCodec = getTestObjects(); 8 | let privateKey = 'D0965AD27E3E096F10F0B1775C8DD38E44F5C53A042C07D778E4C2229D296442'; 9 | 10 | let ec = getEC(); 11 | let keyPair = ec.keyFromSecret(privateKey); 12 | let publicKey = ec.decodePoint(keyPair.pubBytes()); 13 | 14 | let pubKeyHex = signatureDataCodec.EncodeEllipticCurvePoint(publicKey); 15 | let pubKeyPoint = signatureDataCodec.DecodeEllipticCurvePoint(pubKeyHex); 16 | 17 | t.deepEqual(publicKey, pubKeyPoint); 18 | t.is(pubKeyHex, signatureDataCodec.EncodeEllipticCurvePoint(pubKeyPoint)); 19 | }); 20 | 21 | test('When BigNum is encoded, then decoded, result matches original BigNum.', async t => { 22 | let signatureDataCodec = getTestObjects(); 23 | let originalSig = new BN.BN('deadbeef', 16); 24 | let encodedSig = signatureDataCodec.EncodeBigNum(originalSig); 25 | let decodedSig = signatureDataCodec.DecodeBigNum(encodedSig); 26 | 27 | t.true(originalSig.eq(decodedSig)); 28 | }); 29 | 30 | test('When BigNum is encoded, result is uppercased.', async t => { 31 | let signatureDataCodec = getTestObjects(); 32 | let originalSig = new BN.BN('deadbeef', 16); 33 | let encodedSig = signatureDataCodec.EncodeBigNum(originalSig); 34 | 35 | t.is('DEADBEEF', encodedSig); 36 | }); 37 | 38 | let getTestObjects = () => { 39 | let factory = new Factory('test'); 40 | 41 | return new SignatureDataCodec(factory.GetCryptoUtils(), factory.GetEllipticCurveProcessor()); 42 | } 43 | 44 | let getEC = () => { 45 | let factory = new Factory('test'); 46 | return factory.GetEllipticCurveProcessor(); 47 | } 48 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | NanoFusion 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/src/model/Phases/MixBuildAccountTreePhase.js: -------------------------------------------------------------------------------- 1 | import BasePhase from "./BasePhase"; 2 | import AccountTree from "../MixLogic/AccountTree"; 3 | 4 | class MixBuildAccountTreePhase extends BasePhase { 5 | constructor(signatureDataCodec, blockSigner, blockBuilder) { 6 | super(); 7 | this.Name = 'Build Account Tree'; 8 | this.signatureDataCodec = signatureDataCodec; 9 | this.blockSigner = blockSigner; 10 | this.blockBuilder = blockBuilder; 11 | 12 | this.foreignPubKeys = []; 13 | this.myPubKeys = []; 14 | } 15 | 16 | executeInternal(state) { 17 | console.log('Mix Phase: Building account tree.'); 18 | this.myPubKeys = state.MyPubKeys; 19 | this.foreignPubKeys = state.ForeignPubKeys; 20 | 21 | let accountTree = this.buildAccountTree(); 22 | this.emitStateUpdate({ 23 | AccountTree: accountTree 24 | }); 25 | } 26 | 27 | async NotifyOfUpdatedState(state) { 28 | if (!!state.AccountTree) { 29 | this.markPhaseCompleted(); 30 | } 31 | } 32 | 33 | buildAccountTree() { 34 | let sortedUniquePubKeys = this.getSortedUniquePubKeys(); 35 | let accountTree = new AccountTree(this.signatureDataCodec, this.blockSigner, this.blockBuilder); 36 | accountTree.SetInputPubKeysHex(sortedUniquePubKeys); 37 | return accountTree; 38 | } 39 | 40 | getSortedUniquePubKeys() { 41 | let myPubKeysHex = this.myPubKeys.map((pubKey) => { 42 | return this.signatureDataCodec.EncodePublicKey(pubKey); 43 | }); 44 | 45 | let foreignPubKeysHex = this.foreignPubKeys.map((pubKey) => { 46 | return this.signatureDataCodec.EncodePublicKey(pubKey); 47 | }); 48 | 49 | return this.normaliseArray(myPubKeysHex.concat(foreignPubKeysHex)); 50 | } 51 | 52 | normaliseArray(array) { 53 | let unique = array.filter((element, index) => { 54 | return (array.indexOf(element) === index); 55 | }); 56 | 57 | unique.sort(); 58 | return unique; 59 | } 60 | } 61 | 62 | export default MixBuildAccountTreePhase; 63 | -------------------------------------------------------------------------------- /client/src/tests/Cryptography/AccountFinder.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import AccountFinder from "../../model/Cryptography/AccountFinder"; 3 | import * as NanoCurrency from "nanocurrency"; 4 | 5 | test('Check seed is valid.', async t => { 6 | let accountSeed = '6EA72F4895A8CCDA0B47E92187A152BCC621C148C720DE143ADC20E96AD7B49D'; 7 | t.true(NanoCurrency.checkSeed(accountSeed)); 8 | }); 9 | 10 | test('When NanoCurrency throws an error converting to private key, then return null.', async t => { 11 | let accountSeed = 'asdf'; 12 | t.false(NanoCurrency.checkSeed(accountSeed)); 13 | 14 | let accountSigner = getTestObjects(); 15 | 16 | let nanoAddress = 'nano_1hfcqh3gu34s5b6wo6tsc1k88doqaqbq5r34jkt96cq67frynu94wjn6dtbe'; // account index 2 17 | let privateKey = accountSigner.GetPrivateKeyForAccount(accountSeed, nanoAddress); 18 | 19 | t.is(privateKey, null); 20 | }); 21 | 22 | test('When searching for account private key, then correct private key is returned.', async t => { 23 | let accountSigner = getTestObjects(); 24 | 25 | let accountSeed = '6EA72F4895A8CCDA0B47E92187A152BCC621C148C720DE143ADC20E96AD7B49D'; 26 | let nanoAddress = 'nano_1hfcqh3gu34s5b6wo6tsc1k88doqaqbq5r34jkt96cq67frynu94wjn6dtbe'; // account index 2 27 | 28 | let privateKey = accountSigner.GetPrivateKeyForAccount(accountSeed, nanoAddress); 29 | 30 | t.is(privateKey, 'FD2D1A566DCFC0B0AB0CDDD8ACBD420078CAC7BEEE6048DA446F78FD82369C85'); 31 | }); 32 | 33 | test('When searching for account private key, when key is not found after 100 cycles, then return null.', async t => { 34 | let accountSigner = getTestObjects(); 35 | 36 | let accountSeed = '6EA72F4895A8CCDA0B47E92187A152BCC621C148C720DE143ADC20E96AD7B49D'; 37 | let nanoAddress = 'nano_14crrbgo8ti6h3wtcqs5bf8y5sb4f9sugao684pbxcp7rk4zzpkwb5i5tqd7'; // account index 101 38 | 39 | let privateKey = accountSigner.GetPrivateKeyForAccount(accountSeed, nanoAddress); 40 | 41 | t.is(privateKey, null); 42 | }); 43 | 44 | let getTestObjects = () => { 45 | return new AccountFinder(); 46 | } 47 | -------------------------------------------------------------------------------- /src/SessionMananger.js: -------------------------------------------------------------------------------- 1 | const Session = require('./Session'); 2 | 3 | class SessionMananger { 4 | constructor() { 5 | this.sessions = {}; 6 | } 7 | 8 | createSession(type) { 9 | let session = new Session(type); 10 | this.sessions[session.ID] = session; 11 | 12 | return session; 13 | } 14 | 15 | joinSession(sessionID, wsClient) { 16 | let session = this.getSession(sessionID); 17 | 18 | session.clients.push(wsClient); 19 | let clientID = sessionID+'.'+session.getNextClientID(); 20 | wsClient.ClientID = clientID; 21 | wsClient.SessionID = sessionID; 22 | 23 | return clientID; 24 | } 25 | 26 | removeClient(wsClient) { 27 | let session = this.getSession(wsClient.SessionID); 28 | let clientIndex = null; 29 | let searchIndex = 0; 30 | session.clients.forEach((client) => { 31 | if (client.ClientID === wsClient.ClientID) { 32 | clientIndex = searchIndex; 33 | return false; 34 | } 35 | 36 | searchIndex++; 37 | }); 38 | 39 | if (clientIndex === null) { 40 | throw new Error('Client with ID '+wsClient.ClientID+' not found.'); 41 | } 42 | 43 | session.clients.splice(clientIndex, 1); 44 | if (session.clients.length === 0) { 45 | console.log('All clients disconnected, ending session...'); 46 | this.endSession(wsClient.SessionID); 47 | } 48 | } 49 | 50 | messageAllOtherClients(sessionID, messageBody, wsClient) { 51 | let session = this.getSession(sessionID); 52 | let senderClientID = wsClient.ClientID; 53 | 54 | let sendCount = 0; 55 | session.clients.forEach(function (client) { 56 | if (client.ClientID !== senderClientID) { 57 | client.send(messageBody); 58 | sendCount++; 59 | } 60 | }); 61 | 62 | return sendCount; 63 | } 64 | 65 | getSession(sessionID) { 66 | let session = this.sessions[sessionID]; 67 | if (!session) { 68 | throw new Error('Session ID not found.'); 69 | } 70 | 71 | return session; 72 | } 73 | 74 | endSession(sessionID) { 75 | delete this.sessions[sessionID]; 76 | } 77 | } 78 | 79 | module.exports = SessionMananger; 80 | -------------------------------------------------------------------------------- /client/src/model/Phases/MixAnnouncePubKeysPhase.js: -------------------------------------------------------------------------------- 1 | import BasePhase from "./BasePhase"; 2 | import MixEventTypes from "../EventTypes/MixEventTypes"; 3 | 4 | class MixAnnouncePubKeysPhase extends BasePhase { 5 | constructor(sessionClient, signatureDataCodec) { 6 | super(); 7 | this.Name = 'Announce Pub Keys'; 8 | this.sessionClient = sessionClient; 9 | this.signatureDataCodec = signatureDataCodec; 10 | 11 | this.sessionClient.SubscribeToEvent(MixEventTypes.AnnouncePubKey, this.onPeerAnnouncesPubKey.bind(this)); 12 | this.sessionClient.SubscribeToEvent(MixEventTypes.RequestPubKeys, this.onPeerRequestsPubKeys.bind(this)); 13 | this.foreignPubKeys = []; 14 | this.myPubKeys = []; 15 | } 16 | 17 | executeInternal(state) { 18 | console.log('Mix Phase: Announcing public keys.'); 19 | this.myPubKeys = state.MyPubKeys; 20 | 21 | this.sessionClient.SendEvent(MixEventTypes.RequestPubKeys, {}); 22 | this.broadcastMyPubKeys(); 23 | } 24 | 25 | async NotifyOfUpdatedState(state) { 26 | if (state.PubKeyListFinalised) { 27 | this.markPhaseCompleted(); 28 | } 29 | } 30 | 31 | onPeerAnnouncesPubKey(data) { 32 | let alreadyKnown = false; 33 | this.foreignPubKeys.forEach((foreignPubKey) => { 34 | let foreignPubKeyHex = this.signatureDataCodec.EncodePublicKey(foreignPubKey); 35 | if (data.Data.PubKey === foreignPubKeyHex) { 36 | alreadyKnown = true; 37 | return false; 38 | } 39 | }); 40 | 41 | if (!alreadyKnown) { 42 | this.foreignPubKeys.push(this.signatureDataCodec.DecodePublicKey(data.Data.PubKey)); 43 | } 44 | 45 | this.emitStateUpdate({ 46 | ForeignPubKeys: this.foreignPubKeys 47 | }); 48 | } 49 | 50 | onPeerRequestsPubKeys() { 51 | this.broadcastMyPubKeys(); 52 | } 53 | 54 | broadcastMyPubKeys() { 55 | this.myPubKeys.forEach((pubKeyPoint) => { 56 | // console.log('Broadcasting PubKey: '+this.signatureDataCodec.EncodePublicKey(pubKeyPoint)); 57 | 58 | this.sessionClient.SendEvent(MixEventTypes.AnnouncePubKey, { 59 | PubKey: this.signatureDataCodec.EncodePublicKey(pubKeyPoint) 60 | }); 61 | }); 62 | } 63 | } 64 | 65 | export default MixAnnouncePubKeysPhase; 66 | -------------------------------------------------------------------------------- /client/src/model/SessionClient.js: -------------------------------------------------------------------------------- 1 | class SessionClient { 2 | constructor(webSocketBuilder) { 3 | this.webSocketBuilder = webSocketBuilder; 4 | this.socket = null; 5 | 6 | this.nextCallbackID = 0; 7 | this.callbacks = {}; 8 | } 9 | 10 | ConnectToSession(SessionID) { 11 | this.socket = this.webSocketBuilder.buildWebSocket('/api/joinSession', SessionID, this.onSocketMessage.bind(this)); 12 | return this.socket.ClientID; 13 | } 14 | 15 | Disconnect() { 16 | if (this.socket) { 17 | return this.socket.close(); 18 | } 19 | } 20 | 21 | SubscribeToEvent(event, callback) { 22 | if (!this.callbacks[event]) { 23 | this.callbacks[event] = {}; 24 | } 25 | 26 | let callbackID = this.nextCallbackID; 27 | this.nextCallbackID++; 28 | 29 | this.callbacks[event][callbackID] = callback; 30 | return callbackID; 31 | } 32 | 33 | UnsubscribeFromAllEvents() { 34 | this.callbacks = {}; 35 | } 36 | 37 | SendEvent(eventType, data) { 38 | // console.log(eventType); 39 | // console.log(data); 40 | 41 | this.socket.send(JSON.stringify({ 42 | 'MessageType': 'MessageOtherParticipants', 43 | 'MessageBody': JSON.stringify({ 44 | EventType: eventType, 45 | Data: data 46 | }) 47 | })); 48 | } 49 | 50 | /**********************************************************************************************************************/ 51 | // Internal State Functions 52 | onSocketMessage(event) { 53 | let message = JSON.parse(event.data); 54 | // console.log('[message] Data received from server:'); 55 | // console.log(message); 56 | 57 | if (message.JoinSessionResponse) { 58 | this.socket.ClientID = message.ClientID; 59 | } 60 | 61 | if (message.EventType) { 62 | this.emitEvent(message.EventType, message); 63 | } 64 | } 65 | 66 | emitEvent(event, data) { 67 | if (!this.callbacks[event]) { 68 | console.log("No callbacks registered for event: "+event); 69 | return; 70 | } 71 | 72 | let self = this; 73 | let keys = Object.keys(this.callbacks[event]); 74 | keys.forEach(function (key) { 75 | self.callbacks[event][key](data); 76 | }); 77 | } 78 | 79 | } 80 | 81 | export default SessionClient; 82 | -------------------------------------------------------------------------------- /client/src/tests/Cryptography/NanoAmountConverter.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import NanoAmountConverter from "../../model/Cryptography/NanoAmountConverter"; 3 | 4 | test('When nano is converted to raw, then result is correct.', async t => { 5 | let NanoAmountConverter = getTestObjects(); 6 | let testAmount = 1; 7 | 8 | let result = NanoAmountConverter.ConvertNanoAmountToRawAmount(testAmount); 9 | 10 | t.is('1000000000000000000000000000000', result); 11 | }); 12 | 13 | test('When raw is converted to nano, then result is correct.', async t => { 14 | let NanoAmountConverter = getTestObjects(); 15 | let testAmount = '100000000000000000000000000000'; 16 | 17 | let result = NanoAmountConverter.ConvertRawAmountToNanoAmount(testAmount); 18 | 19 | t.is('0.1', result); 20 | }); 21 | 22 | test('When raw amounts are added together, then result is correct.', async t => { 23 | let NanoAmountConverter = getTestObjects(); 24 | let testAmount1 = '1000000000000000000000000000000'; 25 | let testAmount2 = '500000000000000000000000000000'; 26 | 27 | let result = NanoAmountConverter.AddRawAmounts(testAmount1, testAmount2); 28 | 29 | t.is('1500000000000000000000000000000', result); 30 | }); 31 | 32 | test('When raw amount to send is subtracted from current balance, then result is correct.', async t => { 33 | let NanoAmountConverter = getTestObjects(); 34 | let currentBalance = '1000000000000000000000000000000'; 35 | let amountToSend = '400000000000000000000000000000'; 36 | 37 | let result = NanoAmountConverter.SubtractSendAmount(currentBalance, amountToSend); 38 | 39 | t.is('600000000000000000000000000000', result); 40 | }); 41 | 42 | test('When GetTransactionAmount is called, then difference between pre-tx and post-tx balance is returned in whole Nano.', async t => { 43 | let NanoAmountConverter = getTestObjects(); 44 | let testAmount1 = '1000000000000000000000000000000'; 45 | let testAmount2 = '800000000000000000000000000000'; 46 | 47 | let result = NanoAmountConverter.GetTransactionAmount(testAmount1, testAmount2); 48 | 49 | t.is('-0.2', result); 50 | }); 51 | 52 | let getTestObjects = () => { 53 | return new NanoAmountConverter(); 54 | } 55 | -------------------------------------------------------------------------------- /client/src/components/pages/Session.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Container, Row, Col } from 'react-bootstrap'; 3 | import { Redirect } from 'react-router-dom'; 4 | import ChooseSessionAction from '../Session/ChooseSessionAction'; 5 | import Factory from "../../model/Factory"; 6 | 7 | class Session extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | SessionID: this.props.location.state.SessionID, 12 | RedirectToHome: false, 13 | ActionComponent: 'ChooseSessionAction' 14 | }; 15 | 16 | let factory = new Factory(); 17 | this.SessionClient = factory.GetSessionClient(); 18 | this.JointAccountClient = factory.GetJointAccountClient(); 19 | this.MixPhaseFactory = factory.GetMixPhaseFactory(); 20 | this.BlockSigner = factory.GetBlockSigner(); 21 | } 22 | 23 | componentDidMount() { 24 | let sessionID = this.state.SessionID; 25 | this.SessionClient.ConnectToSession(sessionID); 26 | } 27 | 28 | componentWillUnmount() { 29 | this.SessionClient.Disconnect(); 30 | } 31 | 32 | render() { 33 | if (this.state.RedirectToHome) { 34 | return ( 35 | 36 | ); 37 | } 38 | 39 | let actionComponent; 40 | switch (this.state.ActionComponent) { 41 | case 'ChooseSessionAction': 42 | actionComponent = ( 43 | 49 | ); 50 | break; 51 | default: 52 | break; 53 | } 54 | 55 | return ( 56 |
57 | Session View (Session ID: {this.state.SessionID}) 58 | 59 | {actionComponent} 60 | 61 | 62 | 63 | 64 | Icons made by Freepik from www.flaticon.com 65 | 66 | 67 | 68 | {/**/} 69 | {/**/} 70 |
71 | ); 72 | } 73 | } 74 | export default Session; 75 | -------------------------------------------------------------------------------- /client/src/tests/Phases/MixBuildAccountTreePhase.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import MixBuildAccountTreePhase from "../../model/Phases/MixBuildAccountTreePhase"; 3 | import Factory from "../../model/Factory"; 4 | import MockStandardClass from "../Mocks/MockStandardClass"; 5 | import AccountTree from "../../model/MixLogic/AccountTree"; 6 | 7 | let testAggregatedNanoAddress = 'nano_1cxndmsxfdwy8s18rxxcgcubps4wfa13qrkj7f6ffaxdmb5ntscshi1bhd31'; 8 | 9 | test('When phase is executed, then AccountTree is emitted.', async t => { 10 | let phase = getTestObjects(); 11 | 12 | let receivedAccountTree = null; 13 | phase.SetEmitStateUpdateCallback((state) => { 14 | receivedAccountTree = state.AccountTree; 15 | }); 16 | 17 | phase.Execute({ 18 | MyPubKeys: [ 19 | signatureDataCodec.DecodePublicKey('21F80BDC5AB6C926CA8794D83FFA381F33C08101AAF642817B39A4AB8105E7E2'), 20 | signatureDataCodec.DecodePublicKey('A103E2D5474DF8A1BA0039EEB4C4C14847C7F5E8C86D080E7F9AEBE6FDD3E101'), 21 | signatureDataCodec.DecodePublicKey('BB8A385E9816394AB78804CF6279F46F77B8B7D5DF52AEFADC938BD83D829C55'), 22 | signatureDataCodec.DecodePublicKey('9A43BD42D6A795DF5C379AC6EAA30AEF0C04B100C0D01A5722D32A35FE5F2753') 23 | ], 24 | ForeignPubKeys:[ 25 | signatureDataCodec.DecodePublicKey('AAAC435821F1DBA79ABD4FC2B10E77DC900C4B0F58D3A23FCAC868A7531A6B6D') 26 | ] 27 | }); 28 | 29 | t.true(!!receivedAccountTree); 30 | }); 31 | 32 | test('When phase is notified of AccountTree in state, then mark completed.', async t => { 33 | let phase = getTestObjects(); 34 | 35 | let receivedAccountTree = null; 36 | phase.SetEmitStateUpdateCallback((state) => { 37 | receivedAccountTree = state.AccountTree; 38 | }); 39 | 40 | phase.NotifyOfUpdatedState({ 41 | AccountTree: new AccountTree(null, null, null) 42 | }); 43 | 44 | t.true(phase.IsComplete()); 45 | }); 46 | 47 | let signatureDataCodec = null; 48 | 49 | let getTestObjects = () => { 50 | let factory = new Factory('test'); 51 | signatureDataCodec = factory.GetSignatureDataCodec(); 52 | 53 | let mockBlockSigner = new MockStandardClass(); 54 | mockBlockSigner.GetNanoAddressForAggregatedPublicKey = ((pubKeys) => { 55 | return testAggregatedNanoAddress; 56 | }); 57 | 58 | return new MixBuildAccountTreePhase(signatureDataCodec, mockBlockSigner); 59 | } 60 | -------------------------------------------------------------------------------- /client/src/model/Cryptography/BlockBuilder.js: -------------------------------------------------------------------------------- 1 | import * as NanoCurrency from 'nanocurrency'; 2 | 3 | class BlockBuilder { 4 | constructor() { 5 | this.DefaultRepNodeAddress = 'nano_3arg3asgtigae3xckabaaewkx3bzsh7nwz7jkmjos79ihyaxwphhm6qgjps4'; // Nano Foundation #1 6 | this.tempSecretKey = '0000000000000000000000000000000000000000000000000000000000000002'; 7 | this.previousBlockHashForOpenBlock = '0000000000000000000000000000000000000000000000000000000000000000'; 8 | } 9 | 10 | GetUnsignedSendBlock(sendingNanoAddress, previousBlockHash, repNodeAddress, newBalanceAmountInRaw, destinationNanoAddress) { 11 | return this.getUnsignedBlock(sendingNanoAddress, previousBlockHash, repNodeAddress, newBalanceAmountInRaw, destinationNanoAddress); 12 | } 13 | 14 | GetUnsignedReceiveBlock(receivingNanoAddress, previousBlockHash, repNodeAddress, newBalanceAmountInRaw, pendingBlockHash) { 15 | return this.getUnsignedBlock(receivingNanoAddress, previousBlockHash, repNodeAddress, newBalanceAmountInRaw, pendingBlockHash); 16 | } 17 | 18 | getUnsignedBlock(nanoAddress, previousBlockHash, repNodeAddress, newBalanceAmountInRaw, pendingBlockHash) { 19 | repNodeAddress = (repNodeAddress ? repNodeAddress : this.DefaultRepNodeAddress) 20 | 21 | let hash = this.getBlockHash(nanoAddress, previousBlockHash, repNodeAddress, newBalanceAmountInRaw, pendingBlockHash); 22 | 23 | let block = NanoCurrency.createBlock(this.tempSecretKey, { 24 | work: null, 25 | previous: previousBlockHash, 26 | representative: repNodeAddress, 27 | balance: newBalanceAmountInRaw, 28 | link: pendingBlockHash, 29 | }); 30 | 31 | block.hash = hash; 32 | block.block.account = nanoAddress; 33 | block.block.signature = null; 34 | 35 | return block; 36 | } 37 | 38 | getBlockHash(nanoAddress, previousBlockHash, representativeAddress, balanceInRaw, linkBlockHash) { 39 | previousBlockHash = (previousBlockHash === null) 40 | ? this.previousBlockHashForOpenBlock 41 | : previousBlockHash; 42 | 43 | console.log('Getting block hash with balance: '+balanceInRaw); 44 | 45 | return NanoCurrency.hashBlock({ 46 | account: nanoAddress, 47 | previous: previousBlockHash, 48 | representative: representativeAddress, 49 | balance: balanceInRaw, 50 | link: linkBlockHash 51 | }); 52 | } 53 | 54 | } 55 | 56 | export default BlockBuilder; 57 | -------------------------------------------------------------------------------- /client/src/model/MixLogic/SignatureComponentStore.js: -------------------------------------------------------------------------------- 1 | class SignatureComponentStore { 2 | constructor() { 3 | this.data = { 4 | RCommitments: {}, 5 | RPoints: {}, 6 | SignatureContributions: {}, 7 | JointSignaturesForHashes: {} 8 | }; 9 | } 10 | 11 | AddRCommitment(message, pubKeyHex, RCommitment) { 12 | this.ensureDataStructuresAreDefined(message); 13 | this.data.RCommitments[message][pubKeyHex] = RCommitment; 14 | } 15 | 16 | GetRCommitment(message, pubKeyHex) { 17 | if (!this.data.RCommitments[message]) { 18 | return null; 19 | } 20 | 21 | return this.data.RCommitments[message][pubKeyHex]; 22 | } 23 | 24 | GetAllRCommitments(message) { 25 | return this.data.RCommitments[message]; 26 | } 27 | 28 | AddRPoint(message, pubKeyHex, RPoint) { 29 | this.ensureDataStructuresAreDefined(message); 30 | this.data.RPoints[message][pubKeyHex] = RPoint; 31 | } 32 | 33 | GetRPoint(message, pubKeyHex) { 34 | if (!this.data.RPoints[message]) { 35 | return null; 36 | } 37 | 38 | return this.data.RPoints[message][pubKeyHex]; 39 | } 40 | 41 | GetAllRPoints(message) { 42 | return this.data.RPoints[message]; 43 | } 44 | 45 | AddSignatureContribution(message, pubKeyHex, signatureContribution) { 46 | this.ensureDataStructuresAreDefined(message); 47 | this.data.SignatureContributions[message][pubKeyHex] = signatureContribution; 48 | } 49 | 50 | GetSignatureContribution(message, pubKeyHex) { 51 | if (!this.data.SignatureContributions[message]) { 52 | return null; 53 | } 54 | 55 | return this.data.SignatureContributions[message][pubKeyHex]; 56 | } 57 | 58 | GetAllSignatureContributions(message) { 59 | return this.data.SignatureContributions[message]; 60 | } 61 | 62 | AddJointSignatureForHash(message, jointSignature) { 63 | this.data.JointSignaturesForHashes[message] = jointSignature; 64 | } 65 | 66 | GetJointSignatureForHash(message) { 67 | return this.data.JointSignaturesForHashes[message]; 68 | } 69 | 70 | GetAllJointSignaturesForHashes() { 71 | return this.data.JointSignaturesForHashes; 72 | } 73 | 74 | ensureDataStructuresAreDefined(messageToSign) { 75 | if (!this.data.RCommitments[messageToSign]) { 76 | this.data.RCommitments[messageToSign] = {}; 77 | this.data.RPoints[messageToSign] = {}; 78 | this.data.SignatureContributions[messageToSign] = {}; 79 | } 80 | } 81 | } 82 | 83 | export default SignatureComponentStore; 84 | -------------------------------------------------------------------------------- /GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | NanoFusion is currently in an alpha release state. You are welcome to try it, but be 4 | aware that it definitely has some bugs and rough edges sticking out. 5 | 6 | One of those rough edges is that I haven't bothered doing any production-deployment 7 | stuff yet. If you want to run it, for now you have to install nodejs and run a 8 | development server. Don't worry, it's not as intimidating as it sounds. 9 | 10 | Assuming you have cloned the repository and have node installed, you can start the server component by doing: 11 | ``` 12 | npm install 13 | npm start 14 | ``` 15 | 16 | In a separate terminal window, start the react dev server for the client component: 17 | ``` 18 | cd ./client 19 | npm install 20 | npm start 21 | ``` 22 | 23 | This will open up a browser window to `http://localhost:3000`. The server component 24 | runs on port 5000, but cors is enabled, so they can talk to each other just fine. 25 | 26 | If you want to actually submit blocks to the network, you will need to configure the 27 | Nano node HTTP endpoint. In this repository, it is pointing to my test node at 28 | `http://nanofusion.casa:7076`. When I first put this up, I left that server 29 | running pretty wide open so others could test with it. Not good practice, but 30 | we all know it's easier to test when things run wide open, and I wanted 31 | any interested devs to have a good experience. I do NOT guarantee that this 32 | node will always be available for testing, so you may have to run your own. If it gets 33 | abused at any point (e.g. for excess work generation), then I will have to 34 | take it down. 35 | 36 | To work for an end-to-end test, the node has to accept actions like `work_generate` 37 | that are blocked by default, so you will need to make sure that the node you use 38 | has those available. 39 | 40 | To set up the node endpoint, edit `./client/src/config.js` and set the value of `nanoNodeAPIURL` 41 | to your node's address and port. 42 | 43 | The eventual goal (especially in mix sessions) is to distribute the work-generation 44 | load across all of the participants in the mix session, using WebGL in the browser. 45 | Generating proof-of-work using WebGL has already been done here: [https://numtel.github.io/nano-webgl-pow](https://numtel.github.io/nano-webgl-pow/). 46 | But it will take some effort to integrate it into the multi-party signing process. 47 | -------------------------------------------------------------------------------- /client/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/src/model/Phases/MixCreateLeafSendBlocksPhase.js: -------------------------------------------------------------------------------- 1 | import BasePhase from "./BasePhase"; 2 | import * as NanoCurrency from 'nanocurrency'; 3 | import NanoAmountConverter from "../Cryptography/NanoAmountConverter"; 4 | 5 | class MixCreateLeafSendBlocksPhase extends BasePhase { 6 | constructor(signatureDataCodec, blockBuilder, blockSigner, nanoNodeClient) { 7 | super(); 8 | this.Name = 'Create Leaf-Send Blocks'; 9 | this.signatureDataCodec = signatureDataCodec; 10 | this.blockBuilder = blockBuilder; 11 | this.blockSigner = blockSigner; 12 | this.nanoNodeClient = nanoNodeClient; 13 | 14 | this.myLeafSendBlocks = []; 15 | this.leafSendBlockAmounts = {}; 16 | } 17 | 18 | async executeInternal(state) { 19 | console.log('Mix Phase: Create leaf send blocks.'); 20 | this.myPrivateKeys = state.MyPrivateKeys; 21 | this.accountTree = state.AccountTree; 22 | 23 | let newState = await this.buildLeafSendBlocks(); 24 | 25 | this.emitStateUpdate({ 26 | MyLeafSendBlocks: this.myLeafSendBlocks, 27 | LeafSendBlockAmounts: this.leafSendBlockAmounts 28 | }); 29 | } 30 | 31 | async NotifyOfUpdatedState(state) { 32 | if (state.MyLeafSendBlocks.length) { 33 | this.markPhaseCompleted(); 34 | } 35 | } 36 | 37 | async buildLeafSendBlocks() { 38 | let blockPromises = []; 39 | 40 | this.myPrivateKeys.forEach((privateKey) => { 41 | blockPromises.push(this.buildLeafSendBlock(privateKey)); 42 | }); 43 | 44 | let blocks = await Promise.all(blockPromises); 45 | 46 | blocks.forEach((block) => { 47 | this.myLeafSendBlocks.push(block); 48 | }); 49 | } 50 | 51 | async buildLeafSendBlock(privateKey) { 52 | let publicKey = this.blockSigner.GetPublicKeyFromPrivate(privateKey); 53 | let publicKeyHex = this.signatureDataCodec.EncodePublicKey(publicKey); 54 | let nanoPublicKey = NanoCurrency.derivePublicKey(privateKey); 55 | let nanoAddress = NanoCurrency.deriveAddress(nanoPublicKey, {useNanoPrefix: true}); 56 | 57 | let accountInfo = await this.nanoNodeClient.GetAccountInfo(nanoAddress); 58 | // console.log(accountInfo); 59 | // console.log('Nano Address for Key: ' + nanoAddress); 60 | 61 | let receivingAccountNode = this.accountTree.GetLeafAccountNodeForPublicKeyHex(publicKeyHex); 62 | 63 | let block = this.blockBuilder.GetUnsignedSendBlock( 64 | nanoAddress, 65 | this.getAccountInfoProperty(accountInfo, 'frontier'), 66 | this.getAccountInfoProperty(accountInfo, 'representative'), 67 | '0', 68 | receivingAccountNode.NanoAddress 69 | ); 70 | 71 | receivingAccountNode.AddIncomingLeafSendBlock(block, accountInfo.balance); 72 | 73 | this.leafSendBlockAmounts[block.hash] = accountInfo.balance; 74 | 75 | return block; 76 | } 77 | 78 | getAccountInfoProperty(accountInfo, property) { 79 | if (accountInfo.error === 'Account not found') { 80 | return null; 81 | } 82 | 83 | return accountInfo[property]; 84 | } 85 | 86 | } 87 | 88 | export default MixCreateLeafSendBlocksPhase; 89 | -------------------------------------------------------------------------------- /client/src/model/Phases/MixSignTransactionsPhase.js: -------------------------------------------------------------------------------- 1 | import BasePhase from "./BasePhase"; 2 | 3 | class MixSignTransactionsPhase extends BasePhase { 4 | constructor(signTransactionPhaseFactory, signatureDataCodec) { 5 | super(); 6 | this.Name = 'Signing Transactions'; 7 | this.signTransactionPhaseFactory = signTransactionPhaseFactory; 8 | this.signatureDataCodec = signatureDataCodec; 9 | this.latestState = null; 10 | this.transactionPhaseTrackers = []; 11 | } 12 | 13 | executeInternal(state) { 14 | this.latestState = state; 15 | console.log('Mix Phase: Signing transactions.'); 16 | let transactionsToInitiate = this.getAllTransactionsInTree(this.latestState.AccountTree.MixNode); 17 | 18 | transactionsToInitiate.forEach((transaction) => { 19 | let phaseTracker = this.signTransactionPhaseFactory.BuildPhaseTracker(transaction.hash); 20 | phaseTracker.SetStateUpdateEmittedCallback(this.onSignTransactionPhaseTrackerEmittedState.bind(this)); 21 | 22 | this.transactionPhaseTrackers.push(phaseTracker); 23 | 24 | phaseTracker.ExecutePhases(this.latestState); 25 | }); 26 | 27 | this.emitStateUpdate({ 28 | TransactionsToSign: this.transactionPhaseTrackers.length 29 | }); 30 | } 31 | 32 | async NotifyOfUpdatedState(state) { 33 | this.latestState = state; 34 | this.transactionPhaseTrackers.forEach((phaseTracker) => { 35 | phaseTracker.NotifyOfUpdatedState(this.latestState); 36 | }); 37 | 38 | if (!this.IsRunning()) { 39 | return; 40 | } 41 | 42 | if (this.transactionPhaseTrackers.length === 0) { 43 | return; 44 | } 45 | 46 | if (this.transactionPhaseTrackers.length === Object.keys(this.latestState.SignatureComponentStore.GetAllJointSignaturesForHashes()).length) { 47 | this.markPhaseCompleted(); 48 | } 49 | } 50 | 51 | onSignTransactionPhaseTrackerEmittedState(state) { 52 | this.emitStateUpdate(state); 53 | } 54 | 55 | getAllTransactionsInTree(accountNode) { 56 | if (!accountNode) { 57 | return []; 58 | } 59 | 60 | let leftTransactions = this.getAllTransactionsInTree(accountNode.AccountNodeLeft); 61 | let rightTransactions = this.getAllTransactionsInTree(accountNode.AccountNodeRight); 62 | 63 | // let pubKeysForNode = accountNode.GetComponentPublicKeysHex(); 64 | // pubKeysForNode.sort((a, b) => { 65 | // return a.localeCompare(b); 66 | // }); 67 | 68 | // let myPubKeysHex = this.latestState.MyPubKeys.map((pubKey) => { 69 | // return this.signatureDataCodec.EncodePublicKey(pubKey); 70 | // }); 71 | 72 | // if (myPubKeysHex.indexOf(pubKeysForNode[0]) === -1) { 73 | // return leftTransactions.concat(rightTransactions); 74 | // } 75 | 76 | let selfTransactions = []; 77 | Object.keys(accountNode.TransactionPaths).forEach((key) => { 78 | accountNode.TransactionPaths[key].forEach((transaction) => { 79 | selfTransactions.push(transaction); 80 | }); 81 | }); 82 | 83 | return leftTransactions.concat(rightTransactions).concat(selfTransactions); 84 | } 85 | 86 | } 87 | 88 | export default MixSignTransactionsPhase; 89 | -------------------------------------------------------------------------------- /client/src/model/Phases/SignTransaction/BaseSigningPhase.js: -------------------------------------------------------------------------------- 1 | import BasePhase from "../BasePhase"; 2 | 3 | class BaseSigningPhase extends BasePhase { 4 | constructor() { 5 | super(); 6 | 7 | this.KNOWN_TRANSACTIONS = [ 8 | 'E43CA492CC7420D2168665AC571230D8E2BC533454B5DF7E006A05D05C87ED95', 9 | '3126BB04534205B57A2E378D5632098C310302AAA6344003D3CAF8B699ABFD73', 10 | 'DADAD4DFA602BCA50F5DB4D00A106B2D6DDED1BBAB05E3CFF52FCC098F94CF90', 11 | '4DEFC318CA11BF7E4DB7BA6CCBC8084DD898577A0E49D672CCA2385AD67AF554', 12 | '232CFD54A087699AD183E79C077162585B27072EAFE79DD98FE40F57F3431142', 13 | '5918C306EACEAF7316169FC52884A66ED4C6AF13C21DDD1B18CFDAB949B9511F', 14 | '391053D049B56B1D0909CDE7AE072C0446762B8864463D4AC7C6C9564A0E5DAD', 15 | '47192E68ED45CCC6C7F163F27BC7F9DCD917959E5064D8CA1F10FBE215990F76', 16 | '5333248EF728DAD9CF27A06AF19EB65801B2735BAA78C888B05CCE6784EE6E0A', 17 | 'A7F55257AAC815ADC3F950521B11D96DE5EBC797DC407A5BEF3A8521DD00A384', 18 | 'CD568D665FF186D87C5C30AB4D49C4CF9CA335AFA215360B2EBBAF33F75B0687', 19 | '177F467058C43837CC0AF0FBC3CAA6C6C0EE4D727605F8A3A029B772125F3F20' 20 | ]; 21 | } 22 | 23 | getAnnouncementIsForCorrectMessage(data) { 24 | return (data.Data.MessageToSign === this.messageToSign); 25 | } 26 | 27 | checkIncomingMessageIsValid(data, signedValueKey) { 28 | this.checkPubKeyExists(data.Data.PubKey); 29 | this.checkIncomingMessageSignature(data.Data[signedValueKey], data.Data.Signature, data.Data.PubKey); 30 | } 31 | 32 | checkPubKeyExists(pubKeyHex) { 33 | let pubKeysInHex = this.foreignPubKeys.map((pubKeyPoint) => this.signatureDataCodec.EncodePublicKey(pubKeyPoint)); 34 | if (pubKeysInHex.indexOf(pubKeyHex) === -1) { 35 | throw new Error("Public key "+pubKeyHex+" not found in set of foreign public keys."); 36 | } 37 | } 38 | 39 | checkIncomingMessageSignature(data, signature, pubKeyHex) { 40 | if (!this.blockSigner.VerifyMessageSingle(data, signature, pubKeyHex)) { 41 | throw new Error("Incoming message failed signature verification. PubKey: "+pubKeyHex); 42 | } 43 | } 44 | 45 | getRequiredForeignPubKeysHexForTransaction(messageToSign) { 46 | let requiredPubKeysHex = this.latestState.AccountTree.GetPubKeysHexForTransactionHash(messageToSign); 47 | return requiredPubKeysHex.filter((pubKeyHex) => { 48 | let result = true; 49 | this.myPubKeys.forEach((myPubKey) => { 50 | if (this.signatureDataCodec.EncodePublicKey(myPubKey) === pubKeyHex) { 51 | result = false; 52 | return false; 53 | } 54 | }); 55 | 56 | return result; 57 | }); 58 | } 59 | 60 | // ensureDataStructuresAreDefined(messageToSign) { 61 | // if (!this.foreignRCommitments[messageToSign]) { 62 | // this.foreignRCommitments[messageToSign] = {}; 63 | // } 64 | // 65 | // if (!this.foreignRPoints[messageToSign]) { 66 | // this.foreignRPoints[messageToSign] = {}; 67 | // } 68 | // 69 | // if (!this.foreignSignatureContributions[messageToSign]) { 70 | // this.foreignSignatureContributions[messageToSign] = {}; 71 | // } 72 | // } 73 | 74 | } 75 | 76 | export default BaseSigningPhase; 77 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /client/src/tests/MixLogic/AccountTree.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as NanoCurrency from "nanocurrency"; 3 | import Factory from "../../model/Factory"; 4 | import AccountTree from "../../model/MixLogic/AccountTree"; 5 | import MockStandardClass from "../Mocks/MockStandardClass"; 6 | 7 | let testAggregatedNanoAddress = 'nano_1cxndmsxfdwy8s18rxxcgcubps4wfa13qrkj7f6ffaxdmb5ntscshi1bhd31'; 8 | 9 | test('When pubkeys are set, then correct leaf account node for pubkey is found.', async t => { 10 | let accountTree = getTestObjects(); 11 | 12 | accountTree.SetInputPubKeysHex([ 13 | '21F80BDC5AB6C926CA8794D83FFA381F33C08101AAF642817B39A4AB8105E7E2', 14 | 'A103E2D5474DF8A1BA0039EEB4C4C14847C7F5E8C86D080E7F9AEBE6FDD3E101', 15 | 'BB8A385E9816394AB78804CF6279F46F77B8B7D5DF52AEFADC938BD83D829C55', 16 | '9A43BD42D6A795DF5C379AC6EAA30AEF0C04B100C0D01A5722D32A35FE5F2753', 17 | 'AAAC435821F1DBA79ABD4FC2B10E77DC900C4B0F58D3A23FCAC868A7531A6B6D' 18 | ]); 19 | 20 | let actualNanoAddress = accountTree 21 | .GetLeafAccountNodeForPublicKeyHex('21F80BDC5AB6C926CA8794D83FFA381F33C08101AAF642817B39A4AB8105E7E2') 22 | .NanoAddress; 23 | 24 | t.is(testAggregatedNanoAddress, actualNanoAddress); 25 | }); 26 | 27 | test('When pubkeys are set, and outputs are set, then correct set of nodes is built.', async t => { 28 | let accountTree = getTestObjects(); 29 | 30 | let inputPubKeys = [ 31 | '21F80BDC5AB6C926CA8794D83FFA381F33C08101AAF642817B39A4AB8105E7E2', 32 | 'A103E2D5474DF8A1BA0039EEB4C4C14847C7F5E8C86D080E7F9AEBE6FDD3E101', 33 | 'BB8A385E9816394AB78804CF6279F46F77B8B7D5DF52AEFADC938BD83D829C55', 34 | '9A43BD42D6A795DF5C379AC6EAA30AEF0C04B100C0D01A5722D32A35FE5F2753', 35 | 'AAAC435821F1DBA79ABD4FC2B10E77DC900C4B0F58D3A23FCAC868A7531A6B6D' 36 | ]; 37 | 38 | accountTree.SetInputPubKeysHex(inputPubKeys); 39 | 40 | inputPubKeys.forEach((pubKeyHex) => { 41 | let accountNode = accountTree.GetLeafAccountNodeForPublicKeyHex(pubKeyHex); 42 | accountNode.AddIncomingLeafSendBlock({}, '20'); 43 | }); 44 | 45 | accountTree.SetOutputAccounts([ 46 | { 47 | NanoAddress: 'nano_1g1tutsoskbpfz7qhymfpmgteeg7o4n38j3z6j81y9gwg8jx3kcsnx7krhd5', 48 | Amount: 0.4 49 | }, 50 | { 51 | NanoAddress: 'nano_1ude767onchizwt13eduwndmcaqu8mbqzckze8mqrfpxtqg9hthcyi81ayyt', 52 | Amount: 0.3 53 | }, 54 | { 55 | NanoAddress: 'nano_11bibi4za8b15gmrzz877qhcpfadcifka5pbkt46rrdownfse57rkf3r17qi', 56 | Amount: 0.1 57 | } 58 | ]); 59 | 60 | console.log('Tree Dump:'); 61 | console.log(accountTree.GetTreeDump()); 62 | 63 | t.is(accountTree.Digest(), 'asdf'); 64 | }); 65 | 66 | let signatureDataCodec = null; 67 | 68 | let getTestObjects = () => { 69 | let factory = new Factory('test'); 70 | signatureDataCodec = factory.GetSignatureDataCodec(); 71 | 72 | let mockBlockSigner = new MockStandardClass(); 73 | mockBlockSigner.GetNanoAddressForAggregatedPublicKey = ((pubKeys) => { 74 | return testAggregatedNanoAddress; 75 | }); 76 | 77 | let mockBlockBuilder = new MockStandardClass(); 78 | mockBlockBuilder.GetUnsignedSendBlock = (() => { 79 | return { 80 | hash: 'asdf' 81 | }; 82 | }); 83 | 84 | return new AccountTree(signatureDataCodec, mockBlockSigner, mockBlockBuilder); 85 | } 86 | -------------------------------------------------------------------------------- /client/src/model/Phases/MixPhaseFactory.js: -------------------------------------------------------------------------------- 1 | import PhaseTracker from "./PhaseTracker"; 2 | import MixAnnouncePubKeysPhase from "./MixAnnouncePubKeysPhase"; 3 | import MixAnnounceOutputsPhase from "./MixAnnounceOutputsPhase"; 4 | import MixAnnounceLeafSendBlocksPhase from "./MixAnnounceLeafSendBlocksPhase"; 5 | import MixBuildAccountTreePhase from "./MixBuildAccountTreePhase"; 6 | import MixCreateLeafSendBlocksPhase from "./MixCreateLeafSendBlocksPhase"; 7 | import MixBuildTransactionPathsPhase from "./MixBuildTransactionPathsPhase"; 8 | import MixSignTransactionsPhase from "./MixSignTransactionsPhase"; 9 | 10 | class MixPhaseFactory { 11 | constructor(sessionClient, signatureDataCodec, blockBuilder, blockSigner, nanoNodeClient, signTransactionPhaseFactory) { 12 | this.sessionClient = sessionClient; 13 | this.signatureDataCodec = signatureDataCodec; 14 | this.blockBuilder = blockBuilder; 15 | this.blockSigner = blockSigner; 16 | this.nanoNodeClient = nanoNodeClient; 17 | this.signTransactionPhaseFactory = signTransactionPhaseFactory; 18 | } 19 | 20 | BuildPhaseTracker() { 21 | let phaseTracker = new PhaseTracker(); 22 | let announcePubKeysPhase = new MixAnnouncePubKeysPhase(this.sessionClient, this.signatureDataCodec); 23 | 24 | let buildAccountTreePhase = new MixBuildAccountTreePhase(this.signatureDataCodec, this.blockSigner, this.blockBuilder); 25 | buildAccountTreePhase.SetPrerequisitePhases([announcePubKeysPhase]); 26 | 27 | let createLeafSendBlocksPhase = new MixCreateLeafSendBlocksPhase(this.signatureDataCodec, this.blockBuilder, this.blockSigner, this.nanoNodeClient); 28 | createLeafSendBlocksPhase.SetPrerequisitePhases([buildAccountTreePhase]); 29 | 30 | let announceLeafSendBlocksPhase = new MixAnnounceLeafSendBlocksPhase(this.sessionClient, this.signatureDataCodec, this.blockBuilder); 31 | announceLeafSendBlocksPhase.SetPrerequisitePhases([createLeafSendBlocksPhase]); 32 | 33 | let announceOutputsPhase = new MixAnnounceOutputsPhase(this.sessionClient); 34 | announceOutputsPhase.SetPrerequisitePhases([announceLeafSendBlocksPhase]); 35 | 36 | let buildTransactionPathsPhase = new MixBuildTransactionPathsPhase(this.blockBuilder); 37 | buildTransactionPathsPhase.SetPrerequisitePhases([announceOutputsPhase]); 38 | 39 | // let buildRefundPathsPhase = new MixBuildRefundPathsPhase(this.blockBuilder); 40 | // buildRefundPathsPhase.SetPrerequisitePhases([buildTransactionPathsPhase]); 41 | 42 | let signTransactionsPhase = new MixSignTransactionsPhase(this.signTransactionPhaseFactory, this.signatureDataCodec); 43 | // signTransactionsPhase.SetPrerequisitePhases([buildRefundPathsPhase]); 44 | signTransactionsPhase.SetPrerequisitePhases([buildTransactionPathsPhase]); 45 | 46 | // let publishTransactionsPhase = new MixPublishTransactionsPhase(this.nanoNodeClient) 47 | // publishTransactionsPhase.SetPrerequisitePhases([signTransactionsPhase]); 48 | 49 | phaseTracker.AddPhase(announcePubKeysPhase); 50 | phaseTracker.AddPhase(buildAccountTreePhase); 51 | phaseTracker.AddPhase(createLeafSendBlocksPhase); 52 | phaseTracker.AddPhase(announceLeafSendBlocksPhase); 53 | phaseTracker.AddPhase(announceOutputsPhase); 54 | phaseTracker.AddPhase(buildTransactionPathsPhase); 55 | // phaseTracker.AddPhase(buildRefundPathsPhase); 56 | phaseTracker.AddPhase(signTransactionsPhase); 57 | // phaseTracker.AddPhase(publishTransactionsPhase); 58 | 59 | return phaseTracker; 60 | } 61 | } 62 | 63 | export default MixPhaseFactory; 64 | -------------------------------------------------------------------------------- /client/src/model/Phases/MixAnnounceLeafSendBlocksPhase.js: -------------------------------------------------------------------------------- 1 | import BasePhase from "./BasePhase"; 2 | import MixEventTypes from "../EventTypes/MixEventTypes"; 3 | 4 | class MixAnnounceLeafSendBlocksPhase extends BasePhase { 5 | constructor(sessionClient, signatureDataCodec, blockBuilder) { 6 | super(); 7 | this.Name = 'Announce Leaf Send Blocks'; 8 | this.sessionClient = sessionClient; 9 | this.signatureDataCodec = signatureDataCodec; 10 | this.blockBuilder = blockBuilder; 11 | 12 | this.sessionClient.SubscribeToEvent(MixEventTypes.AnnounceLeafSendBlock, this.onPeerAnnouncesLeafSendBlock.bind(this)); 13 | this.sessionClient.SubscribeToEvent(MixEventTypes.RequestLeafSendBlocks, this.onPeerRequestsLeafSendBlocks.bind(this)); 14 | this.myPubKeys = []; 15 | this.foreignPubKeys = []; 16 | this.myLeafSendBlocks = []; 17 | this.foreignLeafSendBlocks = []; 18 | this.leafSendBlockAmounts = {}; 19 | 20 | this.latestState = {}; 21 | } 22 | 23 | executeInternal(state) { 24 | console.log('Mix Phase: Announcing leaf send blocks.'); 25 | this.latestState = state; 26 | 27 | this.myPubKeys = state.MyPubKeys; 28 | this.foreignPubKeys = state.ForeignPubKeys; 29 | this.myLeafSendBlocks = state.MyLeafSendBlocks; 30 | this.leafSendBlockAmounts = state.LeafSendBlockAmounts; 31 | 32 | this.sessionClient.SendEvent(MixEventTypes.RequestLeafSendBlocks, {}); 33 | this.broadcastMyLeafSendBlocks(); 34 | } 35 | 36 | async NotifyOfUpdatedState(state) { 37 | this.latestState = state; 38 | 39 | if (this.getNumSendBlocksMatchesNumPubKeys()) { 40 | this.markPhaseCompleted(); 41 | } 42 | } 43 | 44 | onPeerAnnouncesLeafSendBlock(data) { 45 | if (!this.IsRunning()) { 46 | return; 47 | } 48 | 49 | let alreadyKnown = false; 50 | this.foreignLeafSendBlocks.forEach((foreignLeafSendBlock) => { 51 | let serialisedLocal = JSON.stringify(foreignLeafSendBlock); 52 | let serialisedForeign = JSON.stringify(data.Data.SendBlock); 53 | if (serialisedLocal === serialisedForeign) { 54 | alreadyKnown = true; 55 | return false; 56 | } 57 | }); 58 | 59 | if (!alreadyKnown) { 60 | this.foreignLeafSendBlocks.push(data.Data.SendBlock); 61 | this.addIncomingSendLeafBlockToAccountNode(data); 62 | } 63 | 64 | this.leafSendBlockAmounts[data.Data.SendBlock.hash] = data.Data.Balance; 65 | 66 | this.emitStateUpdate({ 67 | ForeignLeafSendBlocks: this.foreignLeafSendBlocks, 68 | LeafSendBlockAmounts: this.leafSendBlockAmounts 69 | }); 70 | } 71 | 72 | addIncomingSendLeafBlockToAccountNode(data) { 73 | let accountTree = this.latestState.AccountTree; 74 | 75 | let receivingAccountNode = null; 76 | accountTree.LeafNodes.forEach((leafAccountNode) => { 77 | if (leafAccountNode.NanoAddress === data.Data.SendBlock.block.link_as_account) { 78 | receivingAccountNode = leafAccountNode; 79 | return false; 80 | } 81 | }); 82 | 83 | receivingAccountNode.AddIncomingLeafSendBlock(data.Data.SendBlock, data.Data.Balance); 84 | } 85 | 86 | onPeerRequestsLeafSendBlocks() { 87 | this.broadcastMyLeafSendBlocks(); 88 | } 89 | 90 | broadcastMyLeafSendBlocks() { 91 | this.myLeafSendBlocks.forEach((leafSendBlock) => { 92 | this.sessionClient.SendEvent(MixEventTypes.AnnounceLeafSendBlock, { 93 | SendBlock: leafSendBlock, 94 | Balance: this.latestState.LeafSendBlockAmounts[leafSendBlock.hash] 95 | }); 96 | }); 97 | } 98 | 99 | getNumSendBlocksMatchesNumPubKeys() { 100 | let numSendBlocks = this.latestState.MyLeafSendBlocks.length + this.latestState.ForeignLeafSendBlocks.length; 101 | let numPubKeys = this.latestState.MyPubKeys.length + this.latestState.ForeignPubKeys.length; 102 | return (numSendBlocks === numPubKeys); 103 | } 104 | } 105 | 106 | export default MixAnnounceLeafSendBlocksPhase; 107 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const path = require('path'); 4 | const axios = require('axios'); 5 | 6 | let app = express(); 7 | const expressWs = require('express-ws')(app); 8 | app = expressWs.app; 9 | 10 | const SessionManager = require('./src/SessionMananger'); 11 | 12 | let NANO_API_ENDPOINT = 'http://nanofusion.casa:7076/api/v2'; // development 13 | // let NANO_API_ENDPOINT = 'http://nano-node:7076/api/v2'; // production 14 | let ALLOWED_ACTIONS = [ 15 | 'account_info', 16 | // 'account_balance', 17 | 'pending', 18 | 'blocks_info', 19 | 'work_generate', 20 | 'process' 21 | ]; 22 | 23 | // Serve the static files from the React app 24 | app.use(express.static(path.join(__dirname, 'client/build'))); 25 | 26 | // Parse JSON post requests 27 | app.use(express.json()); 28 | 29 | let sessionManager = new SessionManager(); 30 | 31 | let corsOptions = { 32 | origin: 'http://localhost:3000' 33 | }; 34 | 35 | // create a new fusion/joint-account session 36 | app.get('/api/createSession', cors(corsOptions), (req, res) => { 37 | let newSession = sessionManager.createSession(req.type); 38 | 39 | console.log("Created new session with ID: " + newSession.ID); 40 | 41 | res.json({ 42 | 'SessionID': newSession.ID 43 | }); 44 | }); 45 | 46 | app.post('/api/v2', cors(corsOptions), async (req, res) => { 47 | if (ALLOWED_ACTIONS.indexOf(req.body.action) === -1) { 48 | res.json({ 49 | Status: 'Error', 50 | Message: 'To prevent abuse, the "'+req.body.action+'" action is disabled for this API.' 51 | }); 52 | 53 | return; 54 | } 55 | 56 | axios.post(NANO_API_ENDPOINT, req.body) 57 | .then((response) => { 58 | if (response.status === 200) { 59 | res.json(response.data); 60 | } else { 61 | res.json({ 62 | 'Status': response.status, 63 | 'Message': JSON.stringify(response) 64 | }) 65 | } 66 | }).catch((error) => { 67 | console.log(error); 68 | res.json({ 69 | 'Error': 500, 70 | 'Message': 'Could not process request, received error from Nano node: '+error 71 | }); 72 | }); 73 | }); 74 | 75 | app.ws('/api/joinSession', function (ws, req) { 76 | ws.on('message', function (msgStr) { 77 | let msg = JSON.parse(msgStr); 78 | console.log(msg); 79 | 80 | switch (msg.MessageType) { 81 | case 'JoinSession': 82 | let clientID = null; 83 | try { 84 | clientID = sessionManager.joinSession(msg.SessionID, ws); 85 | } catch (error) { 86 | ws.send(JSON.stringify({ 87 | "Response": "Could not connect to session: " + msg.SessionID 88 | })); 89 | 90 | ws.close(); 91 | break; 92 | } 93 | 94 | ws.send(JSON.stringify({ 95 | "JoinSessionResponse": true, 96 | "Response": "Successfully joined session.", 97 | "ClientID": clientID 98 | })); 99 | break; 100 | case 'MessageOtherParticipants': 101 | let sendCount = sessionManager.messageAllOtherClients(ws.SessionID, msg.MessageBody, ws); 102 | ws.send(JSON.stringify({ 103 | "Response": "Successfully sent message. Recipients: "+sendCount 104 | })); 105 | break; 106 | default: 107 | ws.send("Received: " + msg); 108 | break; 109 | } 110 | }); 111 | 112 | ws.on('close', function () { 113 | console.log('Dropping client: '+ws.ClientID); 114 | try { 115 | sessionManager.removeClient(ws); 116 | } catch (error) { 117 | console.log("Could not drop client: "+error); 118 | } 119 | }); 120 | }); 121 | 122 | // Handles any requests that don't match the ones above 123 | app.get('*', cors(corsOptions), (req,res) => { 124 | res.sendFile(path.join(__dirname+'/client/build/index.html')); 125 | }); 126 | 127 | const port = process.env.PORT || 5000; 128 | app.listen(port); 129 | 130 | console.log('App is listening on port ' + port); 131 | -------------------------------------------------------------------------------- /client/src/model/Phases/MixAnnounceOutputsPhase.js: -------------------------------------------------------------------------------- 1 | import BasePhase from "./BasePhase"; 2 | import MixEventTypes from "../EventTypes/MixEventTypes"; 3 | import NanoAmountConverter from "../Cryptography/NanoAmountConverter"; 4 | 5 | class MixAnnounceOutputsPhase extends BasePhase { 6 | constructor(sessionClient) { 7 | super(); 8 | this.Name = 'Announce Outputs'; 9 | this.sessionClient = sessionClient; 10 | this.sessionClient.SubscribeToEvent(MixEventTypes.AnnounceOutput, this.onPeerAnnouncesOutput.bind(this)); 11 | this.sessionClient.SubscribeToEvent(MixEventTypes.RequestOutputs, this.onPeerRequestsOutputs.bind(this)); 12 | this.myOutputAccounts = null; 13 | this.foreignOutputAccounts = []; 14 | 15 | this.latestState = {}; 16 | } 17 | 18 | executeInternal(state) { 19 | console.log('Mix Phase: Announcing outputs.'); 20 | this.latestState = state; 21 | 22 | this.myOutputAccounts = state.MyOutputAccounts; 23 | 24 | this.sessionClient.SendEvent(MixEventTypes.RequestOutputs, {}); 25 | this.broadcastMyOutputAccounts(); 26 | } 27 | 28 | onPeerAnnouncesOutput(data) { 29 | let alreadyKnown = false; 30 | this.foreignOutputAccounts.forEach((foreignOutputAccount) => { 31 | if (data.Data.NanoAddress === foreignOutputAccount.NanoAddress) { 32 | alreadyKnown = true; 33 | return false; 34 | } 35 | }); 36 | 37 | if (!alreadyKnown) { 38 | this.foreignOutputAccounts.push({ 39 | NanoAddress: data.Data.NanoAddress, 40 | Amount: data.Data.Amount 41 | }); 42 | } 43 | 44 | this.emitStateUpdate({ 45 | ForeignOutputAccounts: this.foreignOutputAccounts 46 | }); 47 | 48 | if (this.getOutputTotalMatchesInputTotal()) { 49 | this.markPhaseCompleted(); 50 | } 51 | } 52 | 53 | async NotifyOfUpdatedState(state) { 54 | this.latestState = state; 55 | 56 | if (this.getOutputTotalMatchesInputTotal()) { 57 | this.markPhaseCompleted(); 58 | } 59 | } 60 | 61 | broadcastMyOutputAccounts() { 62 | this.myOutputAccounts.forEach((outputAccount) => { 63 | this.sessionClient.SendEvent(MixEventTypes.AnnounceOutput, { 64 | NanoAddress: outputAccount.NanoAddress, 65 | Amount: outputAccount.Amount 66 | }); 67 | }); 68 | } 69 | 70 | onPeerRequestsOutputs() { 71 | // potential timing attack here (although unlikely, since it all goes through a central server). 72 | // consider adding a short, random-length delay. 73 | this.broadcastMyOutputAccounts(); 74 | } 75 | 76 | getOutputTotalMatchesInputTotal() { 77 | if (!this.IsRunning()) { 78 | return false; 79 | } 80 | 81 | let allLeafSendBlocks = this.latestState.MyLeafSendBlocks.concat(this.latestState.ForeignLeafSendBlocks); 82 | let allOutputs = this.latestState.MyOutputAccounts.concat(this.latestState.ForeignOutputAccounts); 83 | 84 | let sumLeafSendBlocks = '0'; 85 | let sumOutputs = '0'; 86 | 87 | allLeafSendBlocks.forEach((leafSendBlock) => { 88 | sumLeafSendBlocks = NanoAmountConverter.prototype.AddRawAmounts( 89 | sumLeafSendBlocks, 90 | this.latestState.LeafSendBlockAmounts[leafSendBlock.hash] 91 | ); 92 | }); 93 | 94 | allOutputs.forEach((output) => { 95 | sumOutputs = NanoAmountConverter.prototype.AddRawAmounts( 96 | sumOutputs, 97 | NanoAmountConverter.prototype.ConvertNanoAmountToRawAmount(output.Amount) 98 | ); 99 | }); 100 | 101 | // console.log('Outputs calculation:'); 102 | // console.log(allLeafSendBlocks); 103 | // console.log(sumLeafSendBlocks); 104 | // console.log(NanoAmountConverter.prototype.ConvertRawAmountToNanoAmount(sumLeafSendBlocks)); 105 | // console.log(allOutputs); 106 | // console.log(sumOutputs); 107 | // console.log(NanoAmountConverter.prototype.ConvertRawAmountToNanoAmount(sumOutputs)); 108 | 109 | return (sumLeafSendBlocks === sumOutputs); 110 | } 111 | 112 | } 113 | 114 | export default MixAnnounceOutputsPhase; 115 | -------------------------------------------------------------------------------- /experiments/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aggsigjs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bignumber.js": { 8 | "version": "9.0.0", 9 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", 10 | "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" 11 | }, 12 | "blakejs": { 13 | "version": "1.1.0", 14 | "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz", 15 | "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U=" 16 | }, 17 | "bn.js": { 18 | "version": "5.1.1", 19 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz", 20 | "integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==" 21 | }, 22 | "brorand": { 23 | "version": "1.1.0", 24 | "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", 25 | "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" 26 | }, 27 | "elliptic": { 28 | "version": "6.5.3", 29 | "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", 30 | "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", 31 | "requires": { 32 | "bn.js": "^4.4.0", 33 | "brorand": "^1.0.1", 34 | "hash.js": "^1.0.0", 35 | "hmac-drbg": "^1.0.0", 36 | "inherits": "^2.0.1", 37 | "minimalistic-assert": "^1.0.0", 38 | "minimalistic-crypto-utils": "^1.0.0" 39 | }, 40 | "dependencies": { 41 | "bn.js": { 42 | "version": "4.11.9", 43 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", 44 | "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" 45 | } 46 | } 47 | }, 48 | "hash.js": { 49 | "version": "1.1.7", 50 | "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", 51 | "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", 52 | "requires": { 53 | "inherits": "^2.0.3", 54 | "minimalistic-assert": "^1.0.1" 55 | } 56 | }, 57 | "hmac-drbg": { 58 | "version": "1.0.1", 59 | "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", 60 | "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", 61 | "requires": { 62 | "hash.js": "^1.0.3", 63 | "minimalistic-assert": "^1.0.0", 64 | "minimalistic-crypto-utils": "^1.0.1" 65 | } 66 | }, 67 | "inherits": { 68 | "version": "2.0.4", 69 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 70 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 71 | }, 72 | "minimalistic-assert": { 73 | "version": "1.0.1", 74 | "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", 75 | "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" 76 | }, 77 | "minimalistic-crypto-utils": { 78 | "version": "1.0.1", 79 | "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", 80 | "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" 81 | }, 82 | "nanocurrency": { 83 | "version": "2.4.0", 84 | "resolved": "https://registry.npmjs.org/nanocurrency/-/nanocurrency-2.4.0.tgz", 85 | "integrity": "sha512-mmPvHcc6Rwds5YcQpCYdSkotOzqVJwn8JKQjAfkngDustRdWkmc7XCOn8NktI/8njjQWwgatDYgj9GAycfCNqQ==", 86 | "requires": { 87 | "bignumber.js": "^9.0.0", 88 | "blakejs": "^1.1.0" 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /client/src/components/Session/ChooseSessionAction.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Container, Row} from 'react-bootstrap'; 3 | import SessionActionCard from './SessionActionCard'; 4 | import UseJointAccount from './UseJointAccount'; 5 | import UseMixer from './UseMixer'; 6 | import InviteModal from "./InviteModal"; 7 | import JointAccountEventTypes from "../../model/EventTypes/JointAccountEventTypes"; 8 | 9 | class ChooseSessionAction extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | ChosenAction: 'None', 14 | ShowJointAccountInviteModal: false, 15 | UseJointAccountFromInvite: false, 16 | MixSessionInProgress: false // should this be a prop? 17 | }; 18 | 19 | this.onUseJointAccountClicked = this.onUseJointAccountClicked.bind(this); 20 | this.onStartMixSessionClicked = this.onStartMixSessionClicked.bind(this); 21 | this.onChurnFundsClicked = this.onChurnFundsClicked.bind(this); 22 | this.onEscrowClicked = this.onEscrowClicked.bind(this); 23 | this.onJointAccountInviteAccepted = this.onJointAccountInviteAccepted.bind(this); 24 | this.onJointAccountInviteClosed = this.onJointAccountInviteClosed.bind(this); 25 | } 26 | 27 | onJointAccountInviteAccepted() { 28 | this.onJointAccountInviteClosed(); 29 | this.setState({UseJointAccountFromInvite: true}); 30 | this.onUseJointAccountClicked(); 31 | } 32 | 33 | onJointAccountInviteClosed() { 34 | this.setState({ShowJointAccountInviteModal: false}); 35 | } 36 | 37 | onUseJointAccountClicked() { 38 | console.log('Use joint account'); 39 | this.setState({ChosenAction: 'UseJointAccount'}); 40 | } 41 | 42 | onStartMixSessionClicked() { 43 | console.log('Start mix session'); 44 | this.setState({ChosenAction: 'UseMixer'}); 45 | } 46 | 47 | onChurnFundsClicked() { 48 | console.log('Churn funds'); 49 | } 50 | 51 | onEscrowClicked() { 52 | console.log('Escrow'); 53 | } 54 | 55 | componentDidMount() { 56 | this.props.SessionClient.SubscribeToEvent(JointAccountEventTypes.ReadyToUseJointAccount, () => { 57 | this.setState({ShowJointAccountInviteModal: true}); 58 | }); 59 | } 60 | 61 | componentWillUnmount() { 62 | this.props.SessionClient.UnsubscribeFromAllEvents(); 63 | } 64 | 65 | render() { 66 | switch (this.state.ChosenAction) { 67 | case 'UseJointAccount': 68 | return (); 69 | case 'UseMixer': 70 | return (); 71 | default: 72 | break; 73 | } 74 | 75 | return ( 76 | <> 77 | 84 | You have been invited to participate in a joint-account session. 85 | 86 | 87 | 88 | 89 | 90 | Sign and publish transactions for your joint account. Joint accounts require all members to sign transactions. 91 | 92 | 93 | Increase your privacy by mixing funds from many accounts together. No trusted third-party required. 94 | 95 | 96 | Move your funds through a series of intermediate accounts to disconnect them from your identity. 97 | 98 | 99 | Give a third party the authority to pass on or refund your Nano, without the ability to steal it. 100 | 101 | 102 | 103 | 104 | ); 105 | } 106 | } 107 | export default ChooseSessionAction; 108 | -------------------------------------------------------------------------------- /client/src/tests/Cryptography/BlockSigner.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as NanoCurrency from "nanocurrency"; 3 | import * as BN from 'bn.js'; 4 | import BlockSigner from "../../model/Cryptography/BlockSigner"; 5 | import CryptoUtils from "../../model/Cryptography/CryptoUtils"; 6 | import Factory from "../../model/Factory"; 7 | 8 | test('When message is single-signed, then same message can be single-verified.', async t => { 9 | let blockSigner = getTestObjects(); 10 | 11 | let message = '0123456789ABCDEF'; 12 | let privateKey = 'D0965AD27E3E096F10F0B1775C8DD38E44F5C53A042C07D778E4C2229D296442'; 13 | let publicKey = blockSigner.GetPublicKeyFromPrivate(privateKey); 14 | 15 | let signature = blockSigner.SignMessageSingle(message, privateKey); 16 | 17 | t.true(blockSigner.VerifyMessageSingle(message, signature, publicKey)); 18 | }); 19 | 20 | test('When SignMessageSingle is called, and message is not a hex string, then throw an error.', async t => { 21 | let blockSigner = getTestObjects(); 22 | 23 | let privateKey = 'D0965AD27E3E096F10F0B1775C8DD38E44F5C53A042C07D778E4C2229D296442'; 24 | let publicKey = blockSigner.GetPublicKeyFromPrivate(privateKey); 25 | 26 | let message = new BN.BN('00ffaa', 16); 27 | t.throws(() => { 28 | blockSigner.SignMessageSingle(message, privateKey); 29 | }); 30 | 31 | message = '00FFAAXYZ'; 32 | t.throws(() => { 33 | blockSigner.SignMessageSingle(message, privateKey); 34 | }); 35 | 36 | message = '00FFAA'; 37 | blockSigner.SignMessageSingle(message, privateKey); 38 | }); 39 | 40 | test('When aggregate public key is created, then expected aggregate key is returned.', async t => { 41 | let blockSigner = getTestObjects(); 42 | let privateKey1 = '0255A76E9B6F30DB3A201B9F4D07176B518CB24212A5A5822ECE9C5C17C4B9B5'; 43 | let privateKey2 = 'A1D8928B2599FAA13BF96CD07CB8306069C88C9FDF0C8E65E14F8985AC1C1BC9'; 44 | 45 | let publicKey1 = blockSigner.GetPublicKeyFromPrivate(privateKey1); 46 | let publicKey2 = blockSigner.GetPublicKeyFromPrivate(privateKey2); 47 | 48 | let aggregatedPublicKey = blockSigner.GetAggregatedPublicKey([publicKey1, publicKey2]); 49 | 50 | let ec = getEC(); 51 | let aggregatedPublicKeyHex = CryptoUtils.prototype.ByteArrayToHex(ec.encodePoint(aggregatedPublicKey)); 52 | 53 | t.is('49FEC0594D6E7F7040312E400F5F5285CB51FAF5DD8EB10CADBB02915058CCF7', aggregatedPublicKeyHex); 54 | }); 55 | 56 | test('When block is multiple-signed, then the same message can be single-verified by NanoCurrency library.', async t => { 57 | let blockSigner = getTestObjects(); 58 | 59 | let hash = NanoCurrency.hashBlock({ 60 | account: 'nano_3dgj9zw6daepr1qxoa85izzj78zf3jg4e7ad76ontiwho1zqn1tjgozjr9ih', 61 | previous: '0000000000000000000000000000000000000000000000000000000000000000', 62 | representative: 'nano_3akecx3appfbtf6xrzb3qu9c1himzze46uajft1k5x3gkr9iu3mw95noss6i', 63 | balance: '106000000000000000000000000', 64 | link: 'E84F9EE0E3EA8D45ED7468E11597C33C54F7755E3101689FCF5B80D1C280346C' 65 | }); 66 | 67 | t.is('8F835BF3B18AE72CFC31FDBE4BCA3D00EED03FB3083C74CEFB07A80DD4FC9097', hash); 68 | 69 | let ec = getEC(); 70 | 71 | let signatureContributions = [ 72 | new BN.BN('d59a950fb22030dc7237f89d011168775cdc1489ab30a0b65cb08e9c58485a2', 16), 73 | new BN.BN('a0967587b7aa7501be8ea3da9b5ce29c3185a54c57c373458914812d54e68ba', 16), 74 | new BN.BN('a605354fcfc63c414359c6626dce7c7b778096256c0710b35e966a8d73df09b', 16), 75 | ]; 76 | 77 | let RPoints = [ 78 | ec.decodePoint(Array.from(CryptoUtils.prototype.HexToByteArray('9AF8E9305ADD72A54DA2E0C2F698816C7BEAA9C3660A36200E4E81E4236A2049'))), 79 | ec.decodePoint(Array.from(CryptoUtils.prototype.HexToByteArray('60AD74A8D2B93340D5E8A7DFCCE1F5B8F987CE2DCACF4B309BE1398DC9238FAF'))), 80 | ec.decodePoint(Array.from(CryptoUtils.prototype.HexToByteArray('8110B1F749E5008CA81568FC29CB779B2A90AB3ECE1D2311B2D5DCC670613B7D'))) 81 | ]; 82 | 83 | let signature = blockSigner.SignMessageMultiple(signatureContributions, RPoints); 84 | 85 | t.true(NanoCurrency.verifyBlock({ 86 | hash: '8F835BF3B18AE72CFC31FDBE4BCA3D00EED03FB3083C74CEFB07A80DD4FC9097', 87 | signature: signature, 88 | publicKey: 'ADD13FF845A196C02FDAA0C387FF129BED0C5C26150B292B4D438FA83F7A0351' 89 | })); 90 | }); 91 | 92 | let getTestObjects = () => { 93 | let factory = new Factory('test'); 94 | return new BlockSigner(factory.GetCryptoUtils(), factory.GetEllipticCurveProcessor()); 95 | } 96 | 97 | let getEC = () => { 98 | let factory = new Factory() 99 | return factory.GetEllipticCurveProcessor(); 100 | } 101 | -------------------------------------------------------------------------------- /client/src/components/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormControl, InputGroup, Container, Row, Col, Button, Alert } from 'react-bootstrap'; 3 | import { Link, Redirect } from 'react-router-dom'; 4 | import axios from 'axios'; 5 | import config from '../../config'; 6 | import QRCodeImg from "../Session/QRCodeImg"; 7 | import YouTube from "react-youtube"; 8 | 9 | class Home extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | SessionID: '', 14 | RedirectToSession: false 15 | }; 16 | 17 | this.onJoinSessionClicked = this.onJoinSessionClicked.bind(this); 18 | this.onCreateSessionClicked = this.onCreateSessionClicked.bind(this); 19 | 20 | this.onSessionIDChanged = this.onSessionIDChanged.bind(this); 21 | } 22 | 23 | async onJoinSessionClicked() { 24 | this.setState({RedirectToSession: true}); 25 | } 26 | 27 | async onCreateSessionClicked() { 28 | axios.get(config.baseURL+'/api/createSession') 29 | .then((response) => { 30 | this.setState({SessionID: response.data.SessionID}); 31 | }); 32 | } 33 | 34 | onSessionIDChanged(e) { 35 | this.setState({SessionID: e.target.value}); 36 | } 37 | 38 | render() { 39 | if (this.state.RedirectToSession) { 40 | return ( 41 | 47 | ); 48 | } 49 | 50 | return ( 51 |
52 |

NanoFusion

53 | 54 | 55 | 56 | 57 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | or 71 | 72 | 73 | 76 | 77 | 78 |   79 | 80 | 81 | 82 | NanoFusion is still alpha software. It works well enough for demonstration purposes, 83 | but it is known to contain bugs and glitches. Use at your own risk. 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | GitHub Repo 95 | 96 | 97 | 98 | 99 | Documentation 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | Part 1: Joint Account Demo 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Part 2: Trustless Mixing (Video Whitepaper) 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | Part 3: Mixing Demo 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | Donate: nano_1pkhz7jjfda3gsky45jk5oeodmid87fsyecqrgopxhnhuzjrurwd8fdxcqtg 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 |
147 | ); 148 | } 149 | } 150 | export default Home; 151 | -------------------------------------------------------------------------------- /client/src/model/Phases/SignTransaction/SignTransactionAnnounceRCommitmentPhase.js: -------------------------------------------------------------------------------- 1 | import MixEventTypes from "../../EventTypes/MixEventTypes"; 2 | import BaseSigningPhase from "./BaseSigningPhase"; 3 | 4 | class SignTransactionAnnounceRCommitmentPhase extends BaseSigningPhase { 5 | constructor(sessionClient, signatureDataCodec, blockSigner, messageToSign) { 6 | super(); 7 | this.Name = 'Announce RCommitments'; 8 | this.sessionClient = sessionClient; 9 | this.signatureDataCodec = signatureDataCodec; 10 | this.blockSigner = blockSigner; 11 | this.messageToSign = messageToSign; 12 | 13 | this.sessionClient.SubscribeToEvent(MixEventTypes.AnnounceRCommitment, this.onPeerAnnouncesRCommitment.bind(this)); 14 | this.sessionClient.SubscribeToEvent(MixEventTypes.RequestRCommitments, this.onPeerRequestsRCommitments.bind(this)); 15 | 16 | this.myPrivateKeys = null; 17 | this.myPubKeys = null; 18 | this.foreignPubKeys = null; 19 | this.latestState = null; 20 | } 21 | 22 | executeInternal(state) { 23 | this.latestState = state; 24 | 25 | // if (this.KNOWN_TRANSACTIONS.indexOf(this.messageToSign) === -1) { 26 | // console.log('Signing Phase: Announce R Commitments for "'+this.messageToSign+'"'); 27 | // } 28 | 29 | // console.log('Signing Phase: Announcing R Commitments.'); 30 | this.myPrivateKeys = state.MyPrivateKeys; 31 | this.myPubKeys = state.MyPubKeys; 32 | this.foreignPubKeys = state.ForeignPubKeys; 33 | 34 | this.sessionClient.SendEvent(MixEventTypes.RequestRCommitments, {MessageToSign: this.messageToSign}); 35 | this.broadcastMyRCommitments(); 36 | } 37 | 38 | async NotifyOfUpdatedState(state) { 39 | this.latestState = state; 40 | 41 | if (!this.IsRunning()) { 42 | return; 43 | } 44 | 45 | if (this.getAllRCommitmentsReceived()) { 46 | this.markPhaseCompleted(); 47 | } 48 | } 49 | 50 | onPeerAnnouncesRCommitment(data) { 51 | if (!this.getAnnouncementIsForCorrectMessage(data)) { 52 | return; 53 | } 54 | 55 | if (!this.IsRunning()) { 56 | return; 57 | } 58 | 59 | this.checkIncomingMessageIsValid(data, 'RCommitment'); 60 | this.checkAccountTreeDigest(data.Data.AccountTreeDigest); 61 | 62 | let decodedRCommitment = this.signatureDataCodec.DecodeRCommitment(data.Data.RCommitment); 63 | let currentRCommitment = this.latestState.SignatureComponentStore.GetRCommitment(data.Data.MessageToSign, data.Data.PubKey); 64 | if (currentRCommitment && (!currentRCommitment.eq(decodedRCommitment))) { 65 | throw new Error('Peer '+data.Data.PubKey+' tried to update RCommitment. This is not allowed. Skipping.'); 66 | } 67 | 68 | this.latestState.SignatureComponentStore.AddRCommitment(data.Data.MessageToSign, data.Data.PubKey, decodedRCommitment); 69 | this.emitStateUpdate({ 70 | SignatureComponentStore: this.latestState.SignatureComponentStore 71 | }); 72 | } 73 | 74 | onPeerRequestsRCommitments(data) { 75 | if (!this.getAnnouncementIsForCorrectMessage(data)) { 76 | return; 77 | } 78 | 79 | this.broadcastMyRCommitments(); 80 | } 81 | 82 | broadcastMyRCommitments() { 83 | let requiredPubKeysHex = this.latestState.AccountTree.GetPubKeysHexForTransactionHash(this.messageToSign); 84 | 85 | this.myPrivateKeys.forEach((privateKey) => { 86 | // console.log('Broadcasting Signature Contribution for message: '+this.messageToSign); 87 | 88 | let pubKeyPoint = this.blockSigner.GetPublicKeyFromPrivate(privateKey); 89 | let pubKeyHex = this.signatureDataCodec.EncodePublicKey(pubKeyPoint); 90 | 91 | if (requiredPubKeysHex.indexOf(pubKeyHex) === -1) { 92 | return true; 93 | } 94 | 95 | let RCommitment = this.blockSigner.GetRCommitment(privateKey, this.messageToSign); 96 | let RCommitmentEncoded = this.signatureDataCodec.EncodeRCommitment(RCommitment); 97 | 98 | this.sessionClient.SendEvent(MixEventTypes.AnnounceRCommitment, { 99 | PubKey: this.signatureDataCodec.EncodePublicKey(pubKeyPoint), 100 | MessageToSign: this.messageToSign, 101 | AccountTreeDigest: this.latestState.AccountTree.Digest(), 102 | RCommitment: RCommitmentEncoded, 103 | Signature: this.blockSigner.SignMessageSingle(RCommitmentEncoded, privateKey).toHex() 104 | }); 105 | }); 106 | } 107 | 108 | getAllRCommitmentsReceived() { 109 | let requiredForeignPubKeysHex = this.getRequiredForeignPubKeysHexForTransaction(this.messageToSign); 110 | let numForeignRCommitments = this.latestState.SignatureComponentStore.GetAllRCommitments(this.messageToSign) 111 | ? Object.keys(this.latestState.SignatureComponentStore.GetAllRCommitments(this.messageToSign)).length 112 | : 0; 113 | 114 | return (numForeignRCommitments === requiredForeignPubKeysHex.length); 115 | } 116 | 117 | checkAccountTreeDigest(foreignAccountTreeDigest) { 118 | let localAccountTreeDigest = this.latestState.AccountTree.Digest(); 119 | if (foreignAccountTreeDigest !== localAccountTreeDigest) { 120 | throw Error('Account tree digests do not match, aborting. Local: '+localAccountTreeDigest+', Foreign: '+foreignAccountTreeDigest); 121 | } 122 | } 123 | 124 | } 125 | 126 | export default SignTransactionAnnounceRCommitmentPhase; 127 | -------------------------------------------------------------------------------- /client/src/model/Phases/SignTransaction/SignTransactionAnnounceRPointPhase.js: -------------------------------------------------------------------------------- 1 | import MixEventTypes from "../../EventTypes/MixEventTypes"; 2 | import BaseSigningPhase from "./BaseSigningPhase"; 3 | 4 | class SignTransactionAnnounceRPointPhase extends BaseSigningPhase { 5 | constructor(sessionClient, signatureDataCodec, blockSigner, messageToSign) { 6 | super(); 7 | this.Name = 'Announce RPoints'; 8 | this.sessionClient = sessionClient; 9 | this.signatureDataCodec = signatureDataCodec; 10 | this.blockSigner = blockSigner; 11 | this.messageToSign = messageToSign; 12 | 13 | this.sessionClient.SubscribeToEvent(MixEventTypes.AnnounceRPoint, this.onPeerAnnouncesRPoint.bind(this)); 14 | this.sessionClient.SubscribeToEvent(MixEventTypes.RequestRPoints, this.onPeerRequestsRPoints.bind(this)); 15 | 16 | this.myPrivateKeys = null; 17 | this.myPubKeys = null; 18 | this.foreignPubKeys = null; 19 | this.latestState = null; 20 | } 21 | 22 | executeInternal(state) { 23 | this.latestState = state; 24 | 25 | // if (this.KNOWN_TRANSACTIONS.indexOf(this.messageToSign) === -1) { 26 | // console.log('Signing Phase: Announce R Points for "'+this.messageToSign+'"'); 27 | // } 28 | 29 | // console.log('Signing Phase: Announcing R Points.'); 30 | this.myPrivateKeys = state.MyPrivateKeys; 31 | this.myPubKeys = state.MyPubKeys; 32 | this.foreignPubKeys = state.ForeignPubKeys; 33 | 34 | this.sessionClient.SendEvent(MixEventTypes.RequestRPoints, {MessageToSign: this.messageToSign}); 35 | this.broadcastMyRPoints(); 36 | } 37 | 38 | async NotifyOfUpdatedState(state) { 39 | this.latestState = state; 40 | 41 | if (!this.IsRunning()) { 42 | return; 43 | } 44 | 45 | if (this.getAllRPointsReceivedAndValidated()) { 46 | this.markPhaseCompleted(); 47 | } 48 | } 49 | 50 | onPeerAnnouncesRPoint(data) { 51 | if (!this.getAnnouncementIsForCorrectMessage(data)) { 52 | return; 53 | } 54 | 55 | if (!this.IsRunning()) { 56 | return; 57 | } 58 | 59 | this.checkIncomingMessageIsValid(data, 'RPoint'); 60 | 61 | let decodedRPoint = this.signatureDataCodec.DecodeRPoint(data.Data.RPoint); 62 | let currentRPoint = this.latestState.SignatureComponentStore.GetRPoint(data.Data.MessageToSign, data.Data.PubKey); 63 | if (currentRPoint && (!currentRPoint.eq(decodedRPoint))) { 64 | throw new Error('Peer '+data.Data.PubKey+' tried to update RPoint. This is not allowed. Skipping.'); 65 | } 66 | 67 | this.latestState.SignatureComponentStore.AddRPoint(data.Data.MessageToSign, data.Data.PubKey, decodedRPoint); 68 | this.emitStateUpdate({ 69 | SignatureComponentStore: this.latestState.SignatureComponentStore 70 | }); 71 | } 72 | 73 | onPeerRequestsRPoints(data) { 74 | if (!this.getAnnouncementIsForCorrectMessage(data)) { 75 | return; 76 | } 77 | 78 | if (this.IsRunning()) { 79 | this.broadcastMyRPoints(); 80 | } 81 | } 82 | 83 | broadcastMyRPoints() { 84 | let requiredPubKeysHex = this.latestState.AccountTree.GetPubKeysHexForTransactionHash(this.messageToSign); 85 | 86 | this.myPrivateKeys.forEach((privateKey) => { 87 | // console.log('Broadcasting Signature Contribution for message: '+this.messageToSign); 88 | 89 | let pubKeyPoint = this.blockSigner.GetPublicKeyFromPrivate(privateKey); 90 | let pubKeyHex = this.signatureDataCodec.EncodePublicKey(pubKeyPoint); 91 | 92 | if (requiredPubKeysHex.indexOf(pubKeyHex) === -1) { 93 | return true; 94 | } 95 | 96 | let RPoint = this.blockSigner.GetRPoint(privateKey, this.messageToSign); 97 | let RPointEncoded = this.signatureDataCodec.EncodeRPoint(RPoint); 98 | 99 | this.sessionClient.SendEvent(MixEventTypes.AnnounceRPoint, { 100 | PubKey: this.signatureDataCodec.EncodePublicKey(pubKeyPoint), 101 | MessageToSign: this.messageToSign, 102 | RPoint: RPointEncoded, 103 | Signature: this.blockSigner.SignMessageSingle(RPointEncoded, privateKey).toHex() 104 | }); 105 | }); 106 | } 107 | 108 | getAllRPointsReceivedAndValidated() { 109 | let requiredForeignPubKeysHex = this.getRequiredForeignPubKeysHexForTransaction(this.messageToSign); 110 | let numForeignRPoints = this.latestState.SignatureComponentStore.GetAllRPoints(this.messageToSign) 111 | ? Object.keys(this.latestState.SignatureComponentStore.GetAllRPoints(this.messageToSign)).length 112 | : 0; 113 | 114 | if (numForeignRPoints !== requiredForeignPubKeysHex.length) { 115 | return false; 116 | } 117 | 118 | this.checkAllRCommitmentsAreValid(this.messageToSign); 119 | 120 | return true; 121 | } 122 | 123 | checkAllRCommitmentsAreValid(messageToSign) { 124 | if (!( 125 | this.latestState.SignatureComponentStore.GetAllRPoints(this.messageToSign) 126 | && this.latestState.SignatureComponentStore.GetAllRCommitments(this.messageToSign) 127 | && this.latestState.SignatureComponentStore.GetAllRPoints(this.messageToSign).length 128 | && this.latestState.SignatureComponentStore.GetAllRCommitments(this.messageToSign).length 129 | )) { 130 | return; // all RPoints are mine 131 | } 132 | 133 | Object.keys(this.latestState.SignatureComponentStore.GetAllRCommitments(messageToSign)).forEach((key) => { 134 | let RPoint = this.latestState.SignatureComponentStore.GetRPoint(messageToSign, key); 135 | let RCommitment = this.latestState.SignatureComponentStore.GetRCommitment(messageToSign, key); 136 | 137 | if (!this.blockSigner.GetRPointValid(RPoint, RCommitment)) { 138 | throw Error('RCommitment does not match RPoint for PubKey: '+key); 139 | } 140 | }); 141 | } 142 | } 143 | 144 | export default SignTransactionAnnounceRPointPhase; 145 | -------------------------------------------------------------------------------- /client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /client/src/model/Factory.js: -------------------------------------------------------------------------------- 1 | import * as elliptic from 'elliptic'; 2 | import * as blakejs from 'blakejs'; 3 | import config from '../config'; 4 | import CryptoUtils from "./Cryptography/CryptoUtils"; 5 | import JointAccountClient from "./Client/JointAccountClient"; 6 | import AccountFinder from "./Cryptography/AccountFinder"; 7 | import NanoNodeClient from "./NanoNode/NanoNodeClient"; 8 | import BlockBuilder from "./Cryptography/BlockBuilder"; 9 | import BlockSigner from "./Cryptography/BlockSigner"; 10 | import SessionClient from "./SessionClient"; 11 | import WebSocketBuilder from "./WebSocketBuilder"; 12 | import SignatureDataCodec from "./Client/SignatureDataCodec"; 13 | import MixPhaseFactory from "./Phases/MixPhaseFactory"; 14 | import SignTransactionPhaseFactory from "./Phases/SignTransactionPhaseFactory"; 15 | 16 | class Factory { 17 | constructor(mode) { 18 | this.mode = mode || 'production'; 19 | this.ec = null; 20 | this.cryptoUtils = null; 21 | this.jointAccountClient = null; 22 | this.sessionClient = null; 23 | this.webSocketBuilder = null; 24 | this.accountFinder = null; 25 | this.nanoNodeClient = null; 26 | this.blockBuilder = null; 27 | this.blockSigner = null; 28 | this.signatureDataCodec = null; 29 | this.mixPhaseFactory = null; 30 | this.signTransactionPhaseFactory = null; 31 | } 32 | 33 | getOrCreate(existing, createFunc, allowedModes) { 34 | allowedModes = allowedModes || ['production']; 35 | this.checkMode(allowedModes); 36 | if (existing === null) { 37 | return createFunc(); 38 | } 39 | 40 | return existing; 41 | } 42 | 43 | checkMode(allowedModes) { 44 | if (allowedModes.indexOf(this.mode) === -1) { 45 | throw new Error('Must be in one of these modes to create this object through the factory: '+allowedModes.join(', ')); 46 | } 47 | } 48 | 49 | GetEllipticCurveProcessor() { 50 | return this.ec = this.getOrCreate(this.ec, this.createEllipticCurveProcessor.bind(this), ['test', 'production']); 51 | } 52 | 53 | createEllipticCurveProcessor() { 54 | let cryptoUtils = this.GetCryptoUtils(); 55 | let EdDSA = elliptic.eddsa; 56 | let ec = new EdDSA('ed25519'); 57 | 58 | let blake2bhashInt = (...args) => { 59 | let hexInput = ''; 60 | 61 | for (let i = 0; i < args.length; i++) { 62 | hexInput = hexInput + cryptoUtils.ByteArrayToHex(args[i]); 63 | } 64 | 65 | let digest = blakejs.blake2b(this.cryptoUtils.HexToByteArray(hexInput)); 66 | return elliptic.utils.intFromLE(digest).umod(this.ec.curve.n); 67 | }; 68 | 69 | ec.hashInt = blake2bhashInt; 70 | return ec; 71 | } 72 | 73 | GetCryptoUtils() { 74 | return this.cryptoUtils = this.getOrCreate(this.cryptoUtils, this.createCryptoUtils.bind(this), ['test', 'production']); 75 | } 76 | 77 | createCryptoUtils() { 78 | return new CryptoUtils(); 79 | } 80 | 81 | GetJointAccountClient() { 82 | return this.jointAccountClient = this.getOrCreate(this.jointAccountClient, this.createJointAccountClient.bind(this)); 83 | } 84 | 85 | createJointAccountClient() { 86 | return new JointAccountClient( 87 | this.GetSessionClient(), 88 | this.GetAccountFinder(), 89 | this.GetNanoNodeClient(), 90 | this.GetBlockBuilder(), 91 | this.GetBlockSigner(), 92 | this.GetSignatureDataCodec() 93 | ); 94 | } 95 | 96 | GetMixPhaseFactory() { 97 | return this.mixPhaseFactory = this.getOrCreate(this.mixPhaseFactory, this.createMixPhaseFactory.bind(this)); 98 | } 99 | 100 | createMixPhaseFactory() { 101 | return new MixPhaseFactory( 102 | this.GetSessionClient(), 103 | this.GetSignatureDataCodec(), 104 | this.GetBlockBuilder(), 105 | this.GetBlockSigner(), 106 | this.GetNanoNodeClient(), 107 | this.GetSignTransactionPhaseFactory() 108 | ); 109 | } 110 | 111 | GetSessionClient() { 112 | return this.sessionClient = this.getOrCreate(this.sessionClient, this.createSessionClient.bind(this)); 113 | } 114 | 115 | createSessionClient() { 116 | return new SessionClient(this.GetWebSocketBuilder()); 117 | } 118 | 119 | GetWebSocketBuilder() { 120 | return this.webSocketBuilder = this.getOrCreate(this.webSocketBuilder, this.createWebSocketBuilder.bind(this)); 121 | } 122 | 123 | createWebSocketBuilder() { 124 | return new WebSocketBuilder(); 125 | } 126 | 127 | GetAccountFinder() { 128 | return this.accountFinder = this.getOrCreate(this.accountFinder, this.createAccountFinder.bind(this)); 129 | } 130 | 131 | createAccountFinder() { 132 | return new AccountFinder(); 133 | } 134 | 135 | GetNanoNodeClient() { 136 | return this.nanoNodeClient = this.getOrCreate(this.nanoNodeClient, this.createNanoNodeClient.bind(this)); 137 | } 138 | 139 | createNanoNodeClient() { 140 | return new NanoNodeClient(config.nanoNodeAPIURL); 141 | } 142 | 143 | GetBlockBuilder() { 144 | return this.blockBuilder = this.getOrCreate(this.blockBuilder, this.createBlockBuilder.bind(this)); 145 | } 146 | 147 | createBlockBuilder() { 148 | return new BlockBuilder(); 149 | } 150 | 151 | GetBlockSigner() { 152 | return this.blockSigner = this.getOrCreate(this.blockSigner, this.createBlockSigner.bind(this)); 153 | } 154 | 155 | createBlockSigner() { 156 | return new BlockSigner(this.GetCryptoUtils(), this.GetEllipticCurveProcessor()); 157 | } 158 | 159 | GetSignatureDataCodec() { 160 | return this.signatureDataCodec = this.getOrCreate(this.signatureDataCodec, this.createSignatureDataCodec.bind(this), ['test', 'production']); 161 | } 162 | 163 | createSignatureDataCodec() { 164 | return new SignatureDataCodec(this.GetCryptoUtils(), this.GetEllipticCurveProcessor()); 165 | } 166 | 167 | GetSignTransactionPhaseFactory() { 168 | return this.signTransactionPhaseFactory = this.getOrCreate(this.signTransactionPhaseFactory, this.createSignTransactionPhaseFactory.bind(this)); 169 | } 170 | 171 | createSignTransactionPhaseFactory() { 172 | return new SignTransactionPhaseFactory(this.GetSessionClient(), this.GetSignatureDataCodec(), this.GetBlockSigner()); 173 | } 174 | 175 | } 176 | 177 | export default Factory; 178 | -------------------------------------------------------------------------------- /client/src/tests/Cryptography/BlockBuilder.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as NanoCurrency from "nanocurrency"; 3 | import BlockBuilder from "../../model/Cryptography/BlockBuilder"; 4 | 5 | test('When a receive block is created, correct block data is returned.', async t => { 6 | let blockBuilder = getTestObjects(); 7 | let recipientAddressPrivateKey = '2211ABAE11F9721C550FCEDFC5034CF84CB51327E1545099023098E820D0DB66'; 8 | let recipientAddressPublicKey = NanoCurrency.derivePublicKey(recipientAddressPrivateKey); 9 | let recipientAddress = NanoCurrency.deriveAddress(recipientAddressPublicKey, {useNanoPrefix: true}); 10 | 11 | let repNodeAddress = 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou'; // Nano Foundation #2 12 | 13 | let unsignedReceiveBlock = blockBuilder.GetUnsignedReceiveBlock( 14 | recipientAddress, 15 | 'E03D646E37DAE61E4D21281054418EF733CCFB9943B424B36B203ED063340A88', 16 | repNodeAddress, 17 | '106000000000000000000000000', 18 | 'E84F9EE0E3EA8D45ED7468E11597C33C54F7755E3101689FCF5B80D1C280346C' 19 | ); 20 | 21 | t.deepEqual({ 22 | block: { 23 | account: recipientAddress, 24 | balance: '106000000000000000000000000', 25 | link: 'E84F9EE0E3EA8D45ED7468E11597C33C54F7755E3101689FCF5B80D1C280346C', 26 | link_as_account: 'xrb_3t4hmuig9tnfaqpqat934pdw8h4nyxtowea3f4hwypw1t93a1f5ehspxniuy', 27 | previous: 'E03D646E37DAE61E4D21281054418EF733CCFB9943B424B36B203ED063340A88', 28 | representative: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', 29 | signature: null, 30 | type: 'state', 31 | work: null 32 | }, 33 | hash: 'FFBC31635A70258C7245A7DE79A92D2FE6F1365EB1E405A5D653DEDD4607DFCB' 34 | }, unsignedReceiveBlock); 35 | }); 36 | 37 | test('When a receive block is created for an open block, correct block data is returned.', async t => { 38 | let blockBuilder = getTestObjects(); 39 | let recipientAddressPrivateKey = '2211ABAE11F9721C550FCEDFC5034CF84CB51327E1545099023098E820D0DB66'; 40 | let recipientAddressPublicKey = NanoCurrency.derivePublicKey(recipientAddressPrivateKey); 41 | let recipientAddress = NanoCurrency.deriveAddress(recipientAddressPublicKey, {useNanoPrefix: true}); 42 | 43 | let repNodeAddress = null; // Nano Foundation #2 44 | 45 | let unsignedReceiveBlock = blockBuilder.GetUnsignedReceiveBlock( 46 | recipientAddress, 47 | null /* open block */, 48 | repNodeAddress, 49 | '106000000000000000000000000', 50 | 'E84F9EE0E3EA8D45ED7468E11597C33C54F7755E3101689FCF5B80D1C280346C' 51 | ); 52 | 53 | t.deepEqual({ 54 | block: { 55 | account: recipientAddress, 56 | balance: '106000000000000000000000000', 57 | link: 'E84F9EE0E3EA8D45ED7468E11597C33C54F7755E3101689FCF5B80D1C280346C', 58 | link_as_account: 'xrb_3t4hmuig9tnfaqpqat934pdw8h4nyxtowea3f4hwypw1t93a1f5ehspxniuy', 59 | previous: '0000000000000000000000000000000000000000000000000000000000000000', 60 | representative: 'nano_3arg3asgtigae3xckabaaewkx3bzsh7nwz7jkmjos79ihyaxwphhm6qgjps4', // Nano Foundation #1 61 | signature: null, 62 | type: 'state', 63 | work: null 64 | }, 65 | hash: '2079072F6DBA1CE36585DB6E38D7323474E9C36DF118FA7DCECBD73EC9CB780E' 66 | }, unsignedReceiveBlock); 67 | }); 68 | 69 | test('When a send block is created, correct block data is returned.', async t => { 70 | let blockBuilder = getTestObjects(); 71 | let senderAddressPrivateKey = '2211ABAE11F9721C550FCEDFC5034CF84CB51327E1545099023098E820D0DB66'; 72 | let senderAddressPublicKey = NanoCurrency.derivePublicKey(senderAddressPrivateKey); 73 | let senderAddress = NanoCurrency.deriveAddress(senderAddressPublicKey, {useNanoPrefix: true}); 74 | 75 | let repNodeAddress = 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou'; // Nano Foundation #2 76 | 77 | let unsignedReceiveBlock = blockBuilder.GetUnsignedSendBlock( 78 | senderAddress, 79 | 'E03D646E37DAE61E4D21281054418EF733CCFB9943B424B36B203ED063340A88', 80 | repNodeAddress, 81 | '106000000000000000000000000', 82 | 'nano_1pkhz7jjfda3gsky45jk5oeodmid87fsyecqrgopxhnhuzjrurwd8fdxcqtg' 83 | ); 84 | 85 | t.deepEqual({ 86 | block: { 87 | account: senderAddress, 88 | balance: '106000000000000000000000000', 89 | link: '5A4FF96316AD017665E10E321D5955CE0B315B9F3157C3AB6EBE8FDFE38DE38B', 90 | link_as_account: 'nano_1pkhz7jjfda3gsky45jk5oeodmid87fsyecqrgopxhnhuzjrurwd8fdxcqtg', 91 | previous: 'E03D646E37DAE61E4D21281054418EF733CCFB9943B424B36B203ED063340A88', 92 | representative: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', 93 | signature: null, 94 | type: 'state', 95 | work: null 96 | }, 97 | hash: 'D1C37B34B975A0410FC08F2F6B023A87BB7975CF972BCF2579C84535E1090219' 98 | }, unsignedReceiveBlock); 99 | }); 100 | 101 | test('When signing block with single private key, then verification works correctly.', async t => { 102 | let recipientAddressPrivateKey = '2211ABAE11F9721C550FCEDFC5034CF84CB51327E1545099023098E820D0DB66'; 103 | let recipientAddressPublicKey = NanoCurrency.derivePublicKey(recipientAddressPrivateKey); 104 | let recipientAddress = NanoCurrency.deriveAddress(recipientAddressPublicKey, {useNanoPrefix: true}); 105 | 106 | console.log(recipientAddress); 107 | console.log(NanoCurrency.derivePublicKey(recipientAddress)); 108 | 109 | let blockData = { 110 | block: { 111 | account: recipientAddress, 112 | balance: '106000000000000000000000000', 113 | link: 'E84F9EE0E3EA8D45ED7468E11597C33C54F7755E3101689FCF5B80D1C280346C', 114 | link_as_account: 'xrb_3t4hmuig9tnfaqpqat934pdw8h4nyxtowea3f4hwypw1t93a1f5ehspxniuy', 115 | previous: '0000000000000000000000000000000000000000000000000000000000000000', 116 | representative: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', 117 | signature: null, 118 | type: 'state', 119 | work: '77226980634b997b' // pre-calculated. Real-world will call web api to get this. 120 | // work: null 121 | }, 122 | hash: 'A4EBC3DE1974A82941618590A83F83295ABE2C52C6A23140D2AB615DFE4D589B' 123 | }; 124 | 125 | let signature = NanoCurrency.signBlock({ 126 | hash: blockData.hash, 127 | secretKey: recipientAddressPrivateKey 128 | }); 129 | 130 | let blockVerified = NanoCurrency.verifyBlock({ 131 | hash: blockData.hash, 132 | signature: signature, 133 | publicKey: recipientAddressPublicKey 134 | }); 135 | 136 | let workValidated = NanoCurrency.validateWork({ 137 | blockHash: blockData.hash, 138 | work: blockData.block.work 139 | }); 140 | 141 | t.true(blockVerified); 142 | t.true(workValidated); 143 | }); 144 | 145 | let getTestObjects = () => { 146 | return new BlockBuilder(); 147 | } 148 | -------------------------------------------------------------------------------- /client/src/model/NanoNode/NanoNodeClient.js: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios'; 2 | import * as NanoCurrency from 'nanocurrency'; 3 | 4 | class NanoNodeClient { 5 | constructor(nodeEndpoint) { 6 | this.NANO_NODE_ENDPOINT = nodeEndpoint; 7 | } 8 | 9 | async GetAccountInfo(account) { 10 | // return this.getCachedAccountInfo(account); 11 | 12 | let response = await axios.post(this.NANO_NODE_ENDPOINT+'/api/v2', { 13 | action: "account_info", 14 | account: account 15 | }); 16 | 17 | return response.data; 18 | 19 | // .then((response) => { 20 | // console.log(response); 21 | // resolve(this.getWorkFromResponse(response)); 22 | // }).catch((error) => { 23 | // console.log(error); 24 | // reject(error); 25 | // }); 26 | 27 | // return { 28 | // frontier: "C023A4C6E10B056340CF9999F2C1738047BE740A8E0B7EC262E7E9B180CDB754", 29 | // open_block: "B70A6D2A2A1F945F51F9B81BC12E17C9AF337CAC95BEBA07293BCB2AA71E060E", 30 | // representative_block: "C023A4C6E10B056340CF9999F2C1738047BE740A8E0B7EC262E7E9B180CDB754", 31 | // balance: "185473595530000000000000000000000", 32 | // modified_timestamp: "1586538841", 33 | // block_count: "21", 34 | // account_version: "1", 35 | // confirmation_height: "21", 36 | // representative: "nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd" 37 | // }; 38 | 39 | // return { 40 | // "error": "Account not found" 41 | // }; 42 | } 43 | 44 | async GetPendingBlocks(account) { 45 | let response = await axios.post(this.NANO_NODE_ENDPOINT+'/api/v2', { 46 | action: "pending", 47 | account: account 48 | }); 49 | 50 | return response.data; 51 | 52 | // return { 53 | // "blocks": [ 54 | // "13D9A6D0972DDD0FC80F2E2509211221978A1D913D9583E17D03544FE11E6736" 55 | // ] 56 | // }; 57 | } 58 | 59 | async GetBlocksInfo(blockHashes) { 60 | let response = await axios.post(this.NANO_NODE_ENDPOINT+'/api/v2', { 61 | action: "blocks_info", 62 | hashes: blockHashes 63 | }); 64 | 65 | return response.data; 66 | 67 | // return { 68 | // blocks: { 69 | // "13D9A6D0972DDD0FC80F2E2509211221978A1D913D9583E17D03544FE11E6736": { 70 | // block_account: "nano_1pkhz7jjfda3gsky45jk5oeodmid87fsyecqrgopxhnhuzjrurwd8fdxcqtg", 71 | // amount: "1000000000000000000000000000", 72 | // balance: "185462595530000000000000000000000", 73 | // height: "23", 74 | // local_timestamp: "1589462683", 75 | // confirmed: "true", 76 | // contents: "{\n \"type\": \"state\",\n \"account\": \"nano_1pkhz7jjfda3gsky45jk5oeodmid87fsyecqrgopxhnhuzjrurwd8fdxcqtg\",\n \"previous\": \"CE3140A84DAED77B796790EA9799E0DE8BCF5E0147540E81816C0AC73776EE8D\",\n \"representative\": \"nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd\",\n \"balance\": \"185462595530000000000000000000000\",\n \"link\": \"49FEC0594D6E7F7040312E400F5F5285CB51FAF5DD8EB10CADBB02915058CCF7\",\n \"link_as_account\": \"nano_1khyr3entumzg3154dk13xho73gdc9xhdqegp68cugr4k7a7jm9q9oqw3b18\",\n \"signature\": \"AC3FFABC11EC7C2D7D014A9A6CCD27E40AF96277FFAB0E38D7B2C4B67D0C3F36A43D302D511A3C18CFE90A2DE816FB001B09C0604FC2803B1D88D87BE8739909\",\n \"work\": \"105e01ef34c81da6\"\n}\n", 77 | // subtype: "send" 78 | // } 79 | // } 80 | // }; 81 | } 82 | 83 | async GetWork(workInput) { 84 | let response = await axios.post(this.NANO_NODE_ENDPOINT+'/api/v2', { 85 | action: "work_generate", 86 | hash: workInput 87 | }); 88 | 89 | return response.data; 90 | 91 | // return { 92 | // "hash": "49FEC0594D6E7F7040312E400F5F5285CB51FAF5DD8EB10CADBB02915058CCF7", 93 | // "work": "e7dd1ecdcb31eb47", 94 | // "difficulty": "ffffffc5b7ef671d", 95 | // "multiplier": "1.098118552669357" 96 | // }; 97 | } 98 | 99 | async ProcessBlock(block, isSend) { 100 | let accountInfo = await this.GetAccountInfo(block.account); 101 | let workInput = accountInfo.frontier ? accountInfo.frontier : NanoCurrency.derivePublicKey(block.account); 102 | let workInfo = await this.GetWork(workInput); 103 | block.work = workInfo.work; 104 | 105 | console.log('Submitting block to network...'); 106 | console.log({ 107 | action: "process", 108 | json_block: true, 109 | subtype: isSend ? 'send' : 'receive', 110 | block: block 111 | }); 112 | 113 | let response = await axios.post(this.NANO_NODE_ENDPOINT+'/api/v2', { 114 | action: "process", 115 | json_block: true, 116 | subtype: isSend ? 'send' : 'receive', 117 | block: block 118 | }); 119 | 120 | return response.data; 121 | 122 | // return { 123 | // "hash": "E03D646E37DAE61E4D21281054418EF733CCFB9943B424B36B203ED063340A88" 124 | // }; 125 | 126 | } 127 | 128 | getCachedAccountInfo(account) { 129 | switch (account) { 130 | case 'nano_1bhjcifu6mpz69a6rx45mc86nibirer8poawh8dq79gnj358maj5z3ae3ipy': 131 | return { 132 | account_version: "1", 133 | balance: "30000000000000000000000000000", 134 | block_count: "1", 135 | confirmation_height: "1", 136 | frontier: "26742E2B88EC16FD5E3937D6074DAB90DBD305894EE60FE08346C39918AB83C4", 137 | modified_timestamp: "1590469095", 138 | open_block: "26742E2B88EC16FD5E3937D6074DAB90DBD305894EE60FE08346C39918AB83C4", 139 | representative_block: "26742E2B88EC16FD5E3937D6074DAB90DBD305894EE60FE08346C39918AB83C4" 140 | }; 141 | 142 | case 'nano_3rgr1bzxxuup939mf4qb4o6oe85johiymq4oriodowuzdn31tqh91reg77fi': 143 | return { 144 | account_version: "1", 145 | balance: "20000000000000000000000000000", 146 | block_count: "1", 147 | confirmation_height: "1", 148 | frontier: "EFF16DBA32883495728B53F32DF598D4E34E2AA5BABCE689E1805BE694946287", 149 | modified_timestamp: "1590469091", 150 | open_block: "EFF16DBA32883495728B53F32DF598D4E34E2AA5BABCE689E1805BE694946287", 151 | representative_block: "EFF16DBA32883495728B53F32DF598D4E34E2AA5BABCE689E1805BE694946287" 152 | }; 153 | 154 | case 'nano_14odeip7msw3hfy75dosmfaotzfiqzaxty4gdekk9z7471i8zm6937upwrw6': 155 | return { 156 | account_version: "1", 157 | balance: "10000000000000000000000000000", 158 | block_count: "1", 159 | confirmation_height: "1", 160 | frontier: "824969E861EDBE63CCD7405BFC58AC6AFDEBEF622D3026EBBF14667BD3E50182", 161 | modified_timestamp: "1590469100", 162 | open_block: "824969E861EDBE63CCD7405BFC58AC6AFDEBEF622D3026EBBF14667BD3E50182", 163 | representative_block: "824969E861EDBE63CCD7405BFC58AC6AFDEBEF622D3026EBBF14667BD3E50182" 164 | }; 165 | 166 | case 'nano_16fz4nztc4wp6ataz9x7fa4xgp1hg49a4ig46xqymmpduqwupj3imy5hhq6c': 167 | return { 168 | account_version: "1", 169 | balance: "10000000000000000000000000000", 170 | block_count: "1", 171 | confirmation_height: "1", 172 | frontier: "C1A70D8F9BC82417D226AB7EEDEDF34C95060A911EB0C99998702D3C434DAED0", 173 | modified_timestamp: "1590469070", 174 | open_block: "C1A70D8F9BC82417D226AB7EEDEDF34C95060A911EB0C99998702D3C434DAED0", 175 | representative_block: "C1A70D8F9BC82417D226AB7EEDEDF34C95060A911EB0C99998702D3C434DAED0", 176 | }; 177 | 178 | case 'nano_3z1scpktndkphq9h3pewktgwxxxjh9ptqcg9midf4fk9wd8wibtfgir1iuoz': 179 | return { 180 | account_version: "1", 181 | balance: "10000000000000000000000000000", 182 | block_count: "1", 183 | confirmation_height: "1", 184 | frontier: "1619D99ABF370DBBE5B74A62E5357A2541D2874416FAF0813EFE2B5F333BD0C5", 185 | modified_timestamp: "1590467815", 186 | open_block: "1619D99ABF370DBBE5B74A62E5357A2541D2874416FAF0813EFE2B5F333BD0C5", 187 | representative_block: "1619D99ABF370DBBE5B74A62E5357A2541D2874416FAF0813EFE2B5F333BD0C5" 188 | }; 189 | 190 | default: 191 | return { 192 | "error": "Account not found" 193 | }; 194 | } 195 | } 196 | } 197 | 198 | export default NanoNodeClient; 199 | -------------------------------------------------------------------------------- /experiments/testblocksigning.js: -------------------------------------------------------------------------------- 1 | const elliptic = require('elliptic'); 2 | const blakejs = require('blakejs'); 3 | const nanocurrency = require('nanocurrency'); 4 | 5 | let EdDSA = elliptic.eddsa; 6 | let ec = new EdDSA('ed25519'); 7 | 8 | let blake2bhashInt = function () { 9 | let hexInput = ''; 10 | let args = Array.from(arguments); 11 | 12 | for (let i = 0; i < args.length; i++) { 13 | hexInput = hexInput + byteArrayToHex(args[i]); 14 | } 15 | 16 | let digest = blakejs.blake2b(hexToByteArray(hexInput)); 17 | return elliptic.utils.intFromLE(digest).umod(ec.curve.n); 18 | }; 19 | 20 | ec.hashInt = blake2bhashInt; 21 | 22 | function getPlayerData(secret) { 23 | // To prevent key re-use attacks, the zValue should be a random value per-message in production. Here it is derived 24 | // deterministically to ensure consistent output on repeated runs of this demo program. Consistent output aids in the 25 | // debugging process. 26 | let zValue = byteArrayToHex(blakejs.blake2b(hexToByteArray(secret))); 27 | let key = ec.keyFromSecret(secret); // hex string, array or Buffer 28 | 29 | return { 30 | 'secretKeyBytes': key.privBytes(), 31 | 'publicKeyBytes': key.pubBytes(), 32 | 'publicKeyPoint': ec.decodePoint(key.pubBytes()), 33 | 'messagePrefix': key.messagePrefix(), 34 | 'zValue': hexToByteArray(zValue), 35 | 'nanoAddress': nanocurrency.deriveAddress(nanocurrency.derivePublicKey(secret), {useNanoPrefix: true}) 36 | }; 37 | } 38 | 39 | function getSignatureComponentsForPlayer(playerData, message) { 40 | let r = ec.hashInt(playerData.messagePrefix, message, playerData.zValue); 41 | let R = ec.g.mul(r); 42 | let Rencoded = ec.encodePoint(R); 43 | let t = ec.hashInt(Rencoded); 44 | 45 | return { 46 | 'rHash': r, 47 | 'RPoint': R, 48 | 'RPointCommitment': t 49 | }; 50 | } 51 | 52 | function getAggregatedRPoint(RPoints) { 53 | let aggregatedRPoint = null; 54 | 55 | for (let i = 0; i < RPoints.length; i++) { 56 | if (aggregatedRPoint === null) { 57 | aggregatedRPoint = RPoints[i]; 58 | } else { 59 | aggregatedRPoint = aggregatedRPoint.add(RPoints[i]); // point addition 60 | } 61 | } 62 | 63 | return aggregatedRPoint; 64 | } 65 | 66 | function getAHashSignatureComponent(playerPublicKeyPoint, pubKeys) { 67 | let hashArguments = [ec.encodePoint(playerPublicKeyPoint)]; 68 | 69 | for (let i = 0; i < pubKeys.length; i++) { 70 | hashArguments.push(ec.encodePoint(pubKeys[i])); 71 | } 72 | 73 | return ec.hashInt.apply(ec, hashArguments); 74 | } 75 | 76 | function getAggregatedPublicKeyPoint(pubKeys) { 77 | let sortPointsByHexRepresentation = (point1, point2) => { 78 | let point1Hex = byteArrayToHex(ec.encodePoint(point1)); 79 | let point2Hex = byteArrayToHex(ec.encodePoint(point2)); 80 | 81 | return point1Hex.localeCompare(point2Hex); 82 | }; 83 | 84 | pubKeys.sort(sortPointsByHexRepresentation); 85 | 86 | let aggregatedPublicKeyPoint = null; 87 | let aHashComponent = null; 88 | let aggregationComponentPoint = null; 89 | 90 | for (let i = 0; i < pubKeys.length; i++) { 91 | aHashComponent = getAHashSignatureComponent(pubKeys[i], pubKeys); 92 | aggregationComponentPoint = pubKeys[i].mul(aHashComponent); 93 | 94 | if (aggregatedPublicKeyPoint === null) { 95 | aggregatedPublicKeyPoint = aggregationComponentPoint; 96 | } else { 97 | aggregatedPublicKeyPoint = aggregatedPublicKeyPoint.add(aggregationComponentPoint); // point addition 98 | } 99 | } 100 | 101 | return aggregatedPublicKeyPoint; // need to convert to key? 102 | } 103 | 104 | function getKHash(aggregatedRPoint, aggregatedPublicKeyPoint, message) { 105 | return ec.hashInt(ec.encodePoint(aggregatedRPoint), ec.encodePoint(aggregatedPublicKeyPoint), message); 106 | } 107 | 108 | function getSignatureContribution(aggregatedRPoint, pubKeys, message, playerData, sigComponents) { 109 | let aggregatedPublicKeyPoint = getAggregatedPublicKeyPoint(pubKeys); 110 | let aHashSignatureComponent = getAHashSignatureComponent(playerData['publicKeyPoint'], pubKeys); 111 | let kHash = getKHash(aggregatedRPoint, aggregatedPublicKeyPoint, message); 112 | 113 | let signatureContribution = kHash.mul(ec.decodeInt(playerData['secretKeyBytes'])); 114 | signatureContribution = signatureContribution.mul(aHashSignatureComponent); 115 | signatureContribution = sigComponents['rHash'].add(signatureContribution); // bigint addition 116 | signatureContribution = signatureContribution.umod(ec.curve.n); // appears to not be needed? Rust implementation doesn't seem to have it, even for single sig. 117 | 118 | return signatureContribution; 119 | } 120 | 121 | function getAggregatedSignature(signatureContributions, aggregatedRPoint) { 122 | let aggregatedSignature = null; 123 | 124 | for (let i = 0; i < signatureContributions.length; i++) { 125 | if (aggregatedSignature === null) { 126 | aggregatedSignature = signatureContributions[i]; 127 | } else { 128 | aggregatedSignature = aggregatedSignature.add(signatureContributions[i]); // bigint addition 129 | } 130 | } 131 | 132 | return ec.makeSignature({ R: aggregatedRPoint, S: aggregatedSignature, Rencoded: ec.encodePoint(aggregatedRPoint) }); 133 | } 134 | 135 | function byteArrayToHex(byteArray) { 136 | if (!byteArray) { 137 | return ''; 138 | } 139 | 140 | let hexStr = ''; 141 | for (let i = 0; i < byteArray.length; i++) { 142 | let hex = (byteArray[i] & 0xff).toString(16); 143 | hex = hex.length === 1 ? `0${hex}` : hex; 144 | hexStr += hex; 145 | } 146 | 147 | return hexStr.toUpperCase(); 148 | } 149 | 150 | function hexToByteArray (hexString) { 151 | if (!hexString) { 152 | return new Uint8Array(); 153 | } 154 | 155 | const a = []; 156 | for (let i = 0; i < hexString.length; i += 2) { 157 | a.push(parseInt(hexString.substr(i, 2), 16)); 158 | } 159 | 160 | return new Uint8Array(a); 161 | } 162 | 163 | // let blockHash = hexToByteArray('E03D646E37DAE61E4D21281054418EF733CCFB9943B424B36B203ED063340A88'); // hash of test block (from joint-account demo). 164 | let blockHash = hexToByteArray('FC86A202843AA75389383FA0C5ACE814B948B7CB0FBA428CC378ED83B84D9364'); // hash of test block (from mix demo). 165 | 166 | // let playerData1 = getPlayerData('0255A76E9B6F30DB3A201B9F4D07176B518CB24212A5A5822ECE9C5C17C4B9B5'); // joint-account demo. 167 | let playerData1 = getPlayerData('4EB76F58195746851E24C10F131D9F1BE5AB433707F57A66609147669688A227'); // 0.02 account (mix demo). 168 | let signatureComponents1 = getSignatureComponentsForPlayer(playerData1, blockHash); 169 | 170 | // let playerData2 = getPlayerData('A1D8928B2599FAA13BF96CD07CB8306069C88C9FDF0C8E65E14F8985AC1C1BC9'); // joint-account demo. 171 | let playerData2 = getPlayerData('79FF486DADC60D7045CFEB509F9E977CD79D640489F323C07A8ABABF574A2373'); // 0.01 (solo account, mix demo). 172 | let signatureComponents2 = getSignatureComponentsForPlayer(playerData2, blockHash); 173 | 174 | // Adding extra signatories works just fine. But this one is commented out by default so that this file matches the 175 | // output from the NanoFusion video demo. 176 | // 177 | // let playerData3 = getPlayerData('0fed3e2bd78ba62073fef23222b23bd26fd15baf360cda9b55b520be228c3617'); 178 | // let signatureComponents3 = getSignatureComponentsForPlayer(playerData3, blockHash); 179 | 180 | let nanoAddresses = [ 181 | playerData1.nanoAddress, 182 | playerData2.nanoAddress, 183 | // playerData3.nanoAddress, 184 | ]; 185 | 186 | console.log('Signing account nano addresses:'); 187 | console.log(nanoAddresses); 188 | 189 | let pubKeys = [ 190 | playerData1.publicKeyPoint, 191 | playerData2.publicKeyPoint, 192 | // playerData3.publicKeyPoint, 193 | ]; 194 | 195 | let RPoints = [ 196 | signatureComponents1.RPoint, 197 | signatureComponents2.RPoint, 198 | // signatureComponents3.RPoint, 199 | ]; 200 | 201 | let aggregatedRPoint = getAggregatedRPoint(RPoints); 202 | let signatureContribution1 = getSignatureContribution(aggregatedRPoint, pubKeys, blockHash, playerData1, signatureComponents1); 203 | let signatureContribution2 = getSignatureContribution(aggregatedRPoint, pubKeys, blockHash, playerData2, signatureComponents2); 204 | // let signatureContribution3 = getSignatureContribution(aggregatedRPoint, pubKeys, blockHash, playerData3, signatureComponents3); 205 | 206 | let signatureContributions = [ 207 | signatureContribution1, 208 | signatureContribution2, 209 | // signatureContribution3, 210 | ]; 211 | 212 | let aggregatedSignature = getAggregatedSignature(signatureContributions, aggregatedRPoint); 213 | 214 | let aggregatedPublicKeyPoint = getAggregatedPublicKeyPoint(pubKeys); 215 | let aggPubKey = ec.keyFromPublic(aggregatedPublicKeyPoint); 216 | let aggPubKeyHex = byteArrayToHex(aggPubKey.pubBytes()); 217 | console.log('Aggregate Public Key: ' + aggPubKeyHex); 218 | console.log('Nano address for Agg. Pub Key: ' + nanocurrency.deriveAddress(aggPubKeyHex, {useNanoPrefix: true})); 219 | console.log('Aggegated Signature: ' + aggregatedSignature.toHex()); 220 | console.log('Attempting to verify aggregated signature...'); 221 | console.log('EC Verification Passed: ' + ec.verify(blockHash, aggregatedSignature, aggPubKey)); 222 | console.log('Nano verification passed: '+nanocurrency.verifyBlock({ 223 | hash: byteArrayToHex(blockHash), 224 | signature: aggregatedSignature.toHex(), 225 | publicKey: aggPubKeyHex 226 | })); 227 | -------------------------------------------------------------------------------- /client/src/components/Session/UseJointAccount.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Container, Row, Col, InputGroup, FormControl, Button, ButtonGroup, Alert, Table} from 'react-bootstrap'; 3 | import QRCodeImg from "./QRCodeImg"; 4 | 5 | class UseJointAccount extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | MyAccountSeed: '', 10 | MyNanoAddress: '', 11 | JointNanoAddress: null, 12 | JointAccountCurrentBalance: null, 13 | JointAccountPendingBlocks: null, 14 | NumJointAccountContributors: null, 15 | ReceivePendingBlocksButtonEnabled: true, 16 | SendButtonEnabled: true, 17 | BlockData: null, 18 | TransactionsWaitingForApproval: [], 19 | TransactionsApproved: [] 20 | }; 21 | 22 | this.jointAccountClient = this.props.JointAccountClient; 23 | 24 | this.jointAccountClient.OnStateUpdated((state) => { 25 | this.setState(state); 26 | }); 27 | 28 | this.onDemoData1Clicked = this.onDemoData1Clicked.bind(this); 29 | this.onDemoData2Clicked = this.onDemoData2Clicked.bind(this); 30 | this.onReadyClicked = this.onReadyClicked.bind(this); 31 | this.onScanClicked = this.onScanClicked.bind(this); 32 | this.onAccountSeedChanged = this.onAccountSeedChanged.bind(this); 33 | this.onNanoAddressChanged = this.onNanoAddressChanged.bind(this); 34 | this.onDestinationAddressChanged = this.onDestinationAddressChanged.bind(this); 35 | this.onSendAmountChanged = this.onSendAmountChanged.bind(this); 36 | this.onReceivePendingBlocksClicked = this.onReceivePendingBlocksClicked.bind(this); 37 | this.onSendClicked = this.onSendClicked.bind(this); 38 | this.onApproveAllTransactionsClicked = this.onApproveAllTransactionsClicked.bind(this); 39 | 40 | this.nanoAddressStyle = { 41 | overflowWrap: "anywhere", 42 | marginTop: "1em", 43 | fontSize: "0.8em", 44 | color: "#999" 45 | }; 46 | } 47 | 48 | componentDidMount() { 49 | this.jointAccountClient.SetUp(); 50 | } 51 | 52 | componentWillUnmount() { 53 | this.jointAccountClient.TearDown(); 54 | } 55 | 56 | onAccountSeedChanged(e) { 57 | let myAccountSeed = e.target.value; 58 | this.setState({MyAccountSeed: myAccountSeed}); 59 | this.jointAccountClient.UpdatePrivateKey(myAccountSeed, this.state.MyNanoAddress); 60 | } 61 | 62 | onNanoAddressChanged(e) { 63 | let myNanoAddress = e.target.value; 64 | this.setState({MyNanoAddress: myNanoAddress}); 65 | this.jointAccountClient.UpdatePrivateKey(this.state.MyAccountSeed, myNanoAddress); 66 | } 67 | 68 | onDemoData1Clicked() { 69 | let myAccountSeed = 'FF939E8BA1E213E6E599D79D5D7C21974FC8C1E12CA50796D8449653141B1C0F'; 70 | let myNanoAddress = 'nano_1sw9c9aszj7kbwzagk9cmwz49k9muagkbzccc4kb3cf5s1yn9o7yoxbi11ug'; 71 | 72 | this.setState({ 73 | MyAccountSeed: myAccountSeed, 74 | MyNanoAddress: myNanoAddress 75 | }); 76 | 77 | this.jointAccountClient.UpdatePrivateKey(myAccountSeed, myNanoAddress); 78 | } 79 | 80 | onDemoData2Clicked() { 81 | let myAccountSeed = 'E22CE53FFDD738EEFA415A7EA7FFF92B49EED4EC59EF655F234E20441388BE16'; 82 | let myNanoAddress = 'nano_1o6q79j8g6qw3d979zwqy9mg6ufo5k43n1md5m1t3myk79yamqy9koy8oxtp'; 83 | 84 | this.setState({ 85 | MyAccountSeed: myAccountSeed, 86 | MyNanoAddress: myNanoAddress 87 | }); 88 | 89 | this.jointAccountClient.UpdatePrivateKey(myAccountSeed, myNanoAddress); 90 | } 91 | 92 | onReadyClicked() { 93 | this.jointAccountClient.SignalReady(); 94 | } 95 | 96 | async onScanClicked() { 97 | await this.jointAccountClient.ScanAddress(this.state.JointNanoAddress); 98 | } 99 | 100 | onDestinationAddressChanged(e) { 101 | let destinationAddress = e.target.value; 102 | this.setState({DestinationAddress: destinationAddress}); 103 | } 104 | 105 | onSendAmountChanged(e) { 106 | let sendAmount = e.target.value; 107 | this.setState({SendAmount: sendAmount}); 108 | } 109 | 110 | async onReceivePendingBlocksClicked() { 111 | await this.jointAccountClient.ReceivePendingBlocks(this.state.JointNanoAddress); 112 | } 113 | 114 | async onSendClicked() { 115 | await this.jointAccountClient.SendFunds(this.state.JointNanoAddress, this.state.DestinationAddress, this.state.SendAmount); 116 | } 117 | 118 | async onApproveAllTransactionsClicked() { 119 | let waiting = this.state.TransactionsWaitingForApproval; 120 | let alreadyApproved = this.state.TransactionsApproved; 121 | 122 | let hashes = waiting.map((transaction) => { 123 | return transaction.Hash; 124 | }); 125 | 126 | let newApproved = alreadyApproved.concat(waiting); 127 | 128 | this.setState({ 129 | TransactionsApproved: newApproved, 130 | TransactionsWaitingForApproval: [] 131 | }); 132 | 133 | this.jointAccountClient.ApproveTransactions(hashes); 134 | } 135 | 136 | getValueOrUnknown(value, unknownValue) { 137 | unknownValue = unknownValue || 'Unknown (click scan when ready)' 138 | return (value !== null) ? value : unknownValue; 139 | } 140 | 141 | formatPendingBlocks(blocks) { 142 | if (!blocks) { 143 | return 'Unknown'; 144 | } 145 | 146 | if (blocks.length === 0) { 147 | return 'None'; 148 | } 149 | 150 | return ( 151 |
    152 | {blocks.map((block) => { 153 | return (
  • {block.Amount} from {block.SenderAccount}
  • ); 154 | })} 155 |
156 | ); 157 | } 158 | 159 | formatTransactionsWaitingForApproval(transactions) { 160 | // transaction looks like this: 161 | // 162 | // { 163 | // Block: { 164 | // account: "nano_31f6ggm4xrbyix7qu3wtcnfce3q6qxz43qwp8x3brs67sfr5bam454nksrrr", 165 | // balance: "106000000000000000000000000", 166 | // link: "E84F9EE0E3EA8D45ED7468E11597C33C54F7755E3101689FCF5B80D1C280346C", 167 | // link_as_account: "xrb_3t4hmuig9tnfaqpqat934pdw8h4nyxtowea3f4hwypw1t93a1f5ehspxniuy", 168 | // previous: "0000000000000000000000000000000000000000000000000000000000000000", 169 | // representative: "nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou", 170 | // signature: null, 171 | // type: "state", 172 | // work: "77226980634b997b" 173 | // }, 174 | // Hash: 'A4EBC3DE1974A82941618590A83F83295ABE2C52C6A23140D2AB615DFE4D589B' 175 | // } 176 | 177 | return ( 178 | 179 | 180 | {transactions.map((transaction) => { 181 | console.log('AccountNode:'); 182 | console.log(transaction); 183 | let tofrom = (transaction.IsSend ? 'to' : 'from'); 184 | 185 | return ( 186 | 187 | 188 | 189 | ); 190 | })} 191 | 192 | 193 | 194 | 195 |
{transaction.Amount} {tofrom} {transaction.OtherAccount}
196 | ); 197 | } 198 | 199 | render() { 200 | let isHidden = {display: 'none'}; 201 | let isVisible = {}; 202 | 203 | return ( 204 | 205 | 206 | 207 | 208 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 |   241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | Joint Nano Address: {this.getValueOrUnknown(this.state.JointNanoAddress)} 254 | 255 | 256 | 257 | 258 | Contributors: {this.getValueOrUnknown(this.state.NumJointAccountContributors)} 259 | 260 | 261 | 262 | 263 | Current Balance: {this.getValueOrUnknown(this.state.JointAccountCurrentBalance)} 264 | 265 | 266 | 267 | 268 | Pending Blocks: {this.formatPendingBlocks(this.state.JointAccountPendingBlocks)} 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 |   278 | 279 | 0) ? isVisible : isHidden}> 280 | 281 | Transactions waiting for all peers to approve: {this.formatTransactionsWaitingForApproval(this.state.TransactionsWaitingForApproval)} 282 | 283 | 284 | 285 |   286 | 287 | 288 | 289 | 290 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | ); 316 | } 317 | } 318 | export default UseJointAccount; 319 | -------------------------------------------------------------------------------- /client/src/model/MixLogic/AccountTree.js: -------------------------------------------------------------------------------- 1 | import * as blakejs from 'blakejs'; 2 | import AccountNode from "./AccountNode"; 3 | import NanoAmountConverter from "../Cryptography/NanoAmountConverter"; 4 | 5 | class AccountTree { 6 | constructor(signatureDataCodec, blockSigner, blockBuilder) { 7 | this.signatureDataCodec = signatureDataCodec; 8 | this.blockSigner = blockSigner; 9 | this.blockBuilder = blockBuilder; 10 | 11 | this.inputPubKeys = null; 12 | this.MixNode = null; 13 | this.LeafNodes = []; 14 | this.NonLeafNodesByLayer = []; 15 | this.OutputAccounts = []; 16 | } 17 | 18 | SetInputPubKeysHex(pubKeys) { 19 | this.inputPubKeys = pubKeys; 20 | let leftPubKeyOfLeafNode = null; 21 | let rightPubKeyOfLeafNode = null; 22 | 23 | for (let i = 0; i < this.inputPubKeys.length; i++) { 24 | if (i % 2 === 0) { 25 | leftPubKeyOfLeafNode = this.inputPubKeys[i]; 26 | } else { 27 | rightPubKeyOfLeafNode = this.inputPubKeys[i]; 28 | this.LeafNodes.push(this.createLeafAccountNode([leftPubKeyOfLeafNode, rightPubKeyOfLeafNode])); 29 | leftPubKeyOfLeafNode = null; 30 | rightPubKeyOfLeafNode = null; 31 | } 32 | } 33 | 34 | if (leftPubKeyOfLeafNode) { 35 | this.LeafNodes.push(this.createLeafAccountNode([leftPubKeyOfLeafNode])); 36 | } 37 | } 38 | 39 | SetOutputAccounts(outputAccounts) { 40 | this.OutputAccounts = outputAccounts; 41 | 42 | let branchLayerNodes = this.LeafNodes; 43 | 44 | while (branchLayerNodes.length > 1) { 45 | branchLayerNodes = this.addAccountNodeLayer(branchLayerNodes); 46 | } 47 | 48 | this.MixNode = branchLayerNodes[0]; 49 | 50 | this.calculateMixAmounts(this.MixNode); 51 | this.buildTransactionPaths(this.MixNode, this.OutputAccounts); 52 | } 53 | 54 | addAccountNodeLayer(branchLayerNodes) { 55 | let nodeLayer = []; 56 | let leftAccountNode = null; 57 | let rightAccountNode = null; 58 | 59 | for (let i = 0; i < branchLayerNodes.length; i++) { 60 | if (i % 2 === 0) { 61 | leftAccountNode = branchLayerNodes[i]; 62 | } else { 63 | rightAccountNode = branchLayerNodes[i]; 64 | nodeLayer.push(this.createAccountNode([leftAccountNode, rightAccountNode])); 65 | leftAccountNode = null; 66 | rightAccountNode = null; 67 | } 68 | } 69 | 70 | if (leftAccountNode) { 71 | nodeLayer.push(this.createAccountNode([leftAccountNode])); 72 | } 73 | 74 | this.NonLeafNodesByLayer.push(nodeLayer); 75 | return nodeLayer; 76 | } 77 | 78 | calculateMixAmounts(accountNode) { 79 | if (accountNode.IsLeafNode()) { 80 | return this.getMixAmountFromLeafSendNodes(accountNode); 81 | } 82 | 83 | let result = '0'; 84 | [accountNode.AccountNodeLeft, accountNode.AccountNodeRight].forEach((branchNode) => { 85 | if (!branchNode) { 86 | return true; 87 | } 88 | 89 | result = NanoAmountConverter.prototype.AddRawAmounts(result, this.calculateMixAmounts(branchNode)); 90 | }); 91 | 92 | accountNode.SetMixAmountRaw(result); 93 | return result; 94 | } 95 | 96 | getMixAmountFromLeafSendNodes(accountNode) { 97 | let result = '0'; 98 | accountNode.IncomingLeafSendBlocks.forEach((leafSendBlock) => { 99 | result = NanoAmountConverter.prototype.AddRawAmounts(result, leafSendBlock.AmountRaw); 100 | }); 101 | 102 | accountNode.SetMixAmountRaw(result); 103 | return result; 104 | } 105 | 106 | GetLeafAccountNodeForPublicKeyHex(publicKeyHex) { 107 | let result = null; 108 | 109 | this.LeafNodes.forEach((leafNode) => { 110 | if (leafNode.GetComponentPublicKeysHex().indexOf(publicKeyHex) !== -1) { 111 | result = leafNode; 112 | return false; 113 | } 114 | }); 115 | 116 | return result; 117 | } 118 | 119 | GetTreeDump() { 120 | let addBranchNodes = (parentObject, node) => { 121 | parentObject.left = node.AccountNodeLeft ? { node: node.AccountNodeLeft } : null; 122 | parentObject.right = node.AccountNodeRight ? { node: node.AccountNodeRight } : null; 123 | 124 | if (parentObject.left) { 125 | addBranchNodes(parentObject.left, node.AccountNodeLeft); 126 | } 127 | 128 | if (parentObject.right) { 129 | addBranchNodes(parentObject.right, node.AccountNodeRight); 130 | } 131 | }; 132 | 133 | let rootNodeObject = {node: this.MixNode}; 134 | addBranchNodes(rootNodeObject, this.MixNode); 135 | 136 | return rootNodeObject; 137 | } 138 | 139 | Digest() { 140 | if (!this.MixNode) { 141 | return null; 142 | } 143 | 144 | let stringifyNode = (accountNode) => { 145 | if (!accountNode) { 146 | return ''; 147 | } 148 | 149 | let result = ''; 150 | 151 | result += stringifyNode(accountNode.AccountNodeLeft); 152 | result += stringifyNode(accountNode.AccountNodeRight); 153 | 154 | Object.keys(accountNode.TransactionPaths).forEach((pathName) => { 155 | result += accountNode.TransactionPaths[pathName].map((transaction) => { 156 | return transaction.hash; 157 | }).join(','); 158 | }); 159 | 160 | return result; 161 | }; 162 | 163 | let string = stringifyNode(this.MixNode); 164 | let bytes = (new TextEncoder()).encode(string); 165 | return blakejs.blake2bHex(bytes); 166 | } 167 | 168 | GetPubKeysHexForTransactionHash(hash) { 169 | if (!this.MixNode) { 170 | throw Error('Cannot search account tree before all nodes are built.'); 171 | } 172 | 173 | return this.getPubKeysHexForTransactionHashInternal(hash, this.MixNode); 174 | } 175 | 176 | getPubKeysHexForTransactionHashInternal(hash, accountNode) { 177 | if (!accountNode) { 178 | return null; 179 | } 180 | 181 | let result = null; 182 | 183 | Object.keys(accountNode.TransactionPaths).forEach((pathName) => { 184 | accountNode.TransactionPaths[pathName].forEach((transaction) => { 185 | if (transaction.hash === hash) { 186 | result = accountNode.GetComponentPublicKeysHex(); 187 | return false; 188 | } 189 | }); 190 | 191 | if (result) { 192 | return false; 193 | } 194 | }); 195 | 196 | if (!result) { 197 | let resultLeft = this.getPubKeysHexForTransactionHashInternal(hash, accountNode.AccountNodeLeft); 198 | let resultRight = this.getPubKeysHexForTransactionHashInternal(hash, accountNode.AccountNodeRight); 199 | result = resultLeft ? resultLeft : resultRight; 200 | } 201 | 202 | return result; 203 | } 204 | 205 | createLeafAccountNode(componentPublicKeysHex) { 206 | let componentPublicKeys = componentPublicKeysHex.map((pubKeyHex) => { 207 | return this.signatureDataCodec.DecodePublicKey(pubKeyHex); 208 | }); 209 | 210 | let aggregatedNanoAddress = this.blockSigner.GetNanoAddressForAggregatedPublicKey(componentPublicKeys); 211 | 212 | return new AccountNode(componentPublicKeysHex, aggregatedNanoAddress); 213 | } 214 | 215 | createAccountNode(branchNodes) { 216 | let componentPublicKeys = []; 217 | let componentPublicKeysHex = []; 218 | 219 | branchNodes.forEach((branchNode) => { 220 | branchNode.GetComponentPublicKeysHex().forEach((publicKeyHex) => { 221 | componentPublicKeys.push(this.signatureDataCodec.DecodePublicKey(publicKeyHex)); 222 | componentPublicKeysHex.push(publicKeyHex); 223 | }); 224 | }); 225 | 226 | let aggregatedNanoAddress = this.blockSigner.GetNanoAddressForAggregatedPublicKey(componentPublicKeys); 227 | let node = new AccountNode(componentPublicKeysHex, aggregatedNanoAddress); 228 | 229 | node.AccountNodeLeft = branchNodes[0]; 230 | if (branchNodes.length === 2) { 231 | node.AccountNodeRight = branchNodes[1]; 232 | } 233 | 234 | return node; 235 | } 236 | 237 | buildTransactionPaths(accountNode, outputAccounts) { 238 | if (accountNode.IsLeafNode()) { 239 | this.buildTransactionPathsForLeafNode(accountNode, outputAccounts); 240 | return; 241 | } 242 | 243 | let lastSuccessPathBlock = null; 244 | let accountBalance = '0'; 245 | [accountNode.AccountNodeLeft, accountNode.AccountNodeRight].forEach((branchNode) => { 246 | if (!branchNode) { 247 | return true; 248 | } 249 | 250 | if (!branchNode.GetSuccessPathSendBlock(accountNode.NanoAddress)) { 251 | this.buildTransactionPaths(branchNode, [ 252 | { 253 | NanoAddress: accountNode.NanoAddress, 254 | Amount: NanoAmountConverter.prototype.ConvertRawAmountToNanoAmount(accountNode.MixAmountRaw) 255 | } 256 | ]); 257 | } 258 | 259 | let incomingSendBlock = branchNode.GetSuccessPathSendBlock(accountNode.NanoAddress); 260 | 261 | accountBalance = NanoAmountConverter.prototype.AddRawAmounts(accountBalance, branchNode.MixAmountRaw); 262 | lastSuccessPathBlock = this.blockBuilder.GetUnsignedReceiveBlock( 263 | accountNode.NanoAddress, 264 | lastSuccessPathBlock ? lastSuccessPathBlock.hash : null, 265 | this.blockBuilder.DefaultRepNodeAddress, 266 | accountBalance, 267 | incomingSendBlock.hash 268 | ); 269 | 270 | accountNode.TransactionPaths.Success.push(lastSuccessPathBlock); 271 | }); 272 | 273 | this.buildTransactionPathsForOutputs(outputAccounts, lastSuccessPathBlock, accountNode); 274 | } 275 | 276 | buildTransactionPathsForOutputs(outputAccounts, lastSuccessPathBlock, accountNode) { 277 | let accountBalance = accountNode.MixAmountRaw; 278 | 279 | if (outputAccounts.length === 1) { 280 | // Intermediate Nodes 281 | lastSuccessPathBlock = this.blockBuilder.GetUnsignedSendBlock( 282 | accountNode.NanoAddress, 283 | lastSuccessPathBlock.hash, 284 | this.blockBuilder.DefaultRepNodeAddress, 285 | '0', 286 | outputAccounts[0].NanoAddress 287 | ); 288 | 289 | accountNode.TransactionPaths.Success.push(lastSuccessPathBlock); 290 | } else { 291 | // Main Mix Node 292 | outputAccounts.forEach((outputAccount) => { 293 | let sendAmountInRaw = NanoAmountConverter.prototype.ConvertNanoAmountToRawAmount(outputAccount.Amount); 294 | accountBalance = NanoAmountConverter.prototype.SubtractSendAmount(accountBalance, sendAmountInRaw); 295 | 296 | lastSuccessPathBlock = this.blockBuilder.GetUnsignedSendBlock( 297 | accountNode.NanoAddress, 298 | lastSuccessPathBlock.hash, 299 | this.blockBuilder.DefaultRepNodeAddress, 300 | accountBalance, 301 | outputAccount.NanoAddress 302 | ); 303 | 304 | accountNode.TransactionPaths.Success.push(lastSuccessPathBlock); 305 | }); 306 | } 307 | } 308 | 309 | buildTransactionPathsForLeafNode(accountNode, outputAccounts) { 310 | let lastSuccessPathBlock = null; 311 | let accountBalance = '0'; 312 | 313 | // if (accountNode.NanoAddress === 'nano_1hsmfopn1mzhrutqe7pzbjd66gwtrwdcancreptu1f1m99j8tbysh79x7ji5') { 314 | // console.log('Leaf send blocks for culprit Nano Address.'); 315 | // console.log('-'); 316 | // console.log('-'); 317 | // console.log('-'); 318 | // console.log('-'); 319 | // } 320 | 321 | accountNode.IncomingLeafSendBlocks.sort((a, b) => { 322 | return a.Block.hash.localeCompare(b.Block.hash); 323 | }); 324 | 325 | accountNode.IncomingLeafSendBlocks.forEach((leafSendBlock) => { 326 | accountBalance = NanoAmountConverter.prototype.AddRawAmounts(accountBalance, leafSendBlock.AmountRaw); 327 | 328 | // if (accountNode.NanoAddress === 'nano_1hsmfopn1mzhrutqe7pzbjd66gwtrwdcancreptu1f1m99j8tbysh79x7ji5') { 329 | // console.log('Leaf: ' + leafSendBlock.Block.hash); 330 | // console.log('Last Success: ' + leafSendBlock.Block.hash); 331 | // } 332 | 333 | lastSuccessPathBlock = this.blockBuilder.GetUnsignedReceiveBlock( 334 | accountNode.NanoAddress, 335 | lastSuccessPathBlock ? lastSuccessPathBlock.hash : null, 336 | this.blockBuilder.DefaultRepNodeAddress, 337 | accountBalance, 338 | leafSendBlock.Block.hash 339 | ); 340 | 341 | accountNode.TransactionPaths.Success.push(lastSuccessPathBlock); 342 | }); 343 | 344 | this.buildTransactionPathsForOutputs(outputAccounts, lastSuccessPathBlock, accountNode); 345 | } 346 | 347 | } 348 | 349 | export default AccountTree; 350 | -------------------------------------------------------------------------------- /client/src/model/Cryptography/BlockSigner.js: -------------------------------------------------------------------------------- 1 | import * as NanoCurrency from 'nanocurrency'; 2 | import * as blakejs from 'blakejs'; 3 | 4 | class BlockSigner { 5 | constructor(cryptoUtils, ec) { 6 | this.cryptoUtils = cryptoUtils; 7 | this.ec = ec; 8 | this.zValues = {}; 9 | } 10 | 11 | GetPublicKeyFromPrivate(privateKey) { 12 | /** 13 | * NOTE: the public key generated by the elliptic curve library is _different_ to the public key generated by 14 | * the NanoCurrency library. The signing/verification algorithms are NOT identical. The elliptic curve library 15 | * must be used for the aggregated-signature stuff. Here, we also use it for convenience to prove our identity 16 | * when passing messages. 17 | * 18 | * When we want to generate a Nano address for an aggregated public key, we take the _aggregated_ public key 19 | * from the elliptic curve library, covert it to a hex string, and derive a Nano address from that. 20 | * 21 | * When we want to generate a Nano address for a single public key, then we must take a different route. We 22 | * must use NanoCurrency to generate a public key from our private key, then generate the Nano address from 23 | * that public key. 24 | * 25 | * This has the potential to lead to confusion, since there are two different public keys for each private 26 | * key. Hopefully anyone who gets them confused will hopefully stumble on this comment, and find their answer. 27 | */ 28 | let keyPair = this.ec.keyFromSecret(privateKey); 29 | return this.ec.decodePoint(keyPair.pubBytes()); 30 | } 31 | 32 | GetNanoAddressForAggregatedPublicKey(pubKeys) { 33 | let aggregatedPublicKeyPoint = this.GetAggregatedPublicKey(pubKeys); 34 | let aggPubKey = this.ec.keyFromPublic(aggregatedPublicKeyPoint); 35 | 36 | let aggPubKeyHex = this.cryptoUtils.ByteArrayToHex(aggPubKey.pubBytes()); 37 | return NanoCurrency.deriveAddress(aggPubKeyHex, {useNanoPrefix: true}); 38 | } 39 | 40 | GetRCommitment(privateKey, messageToSign) { 41 | let playerData = this.getPlayerData(privateKey, messageToSign); 42 | let sigComponents = this.getSignatureComponentsForPlayer(playerData, messageToSign); 43 | 44 | return sigComponents['RPointCommitment']; 45 | } 46 | 47 | GetRPoint(privateKey, messageToSign) { 48 | let playerData = this.getPlayerData(privateKey, messageToSign); 49 | let sigComponents = this.getSignatureComponentsForPlayer(playerData, messageToSign); 50 | 51 | return sigComponents['RPoint']; 52 | } 53 | 54 | GetSignatureContribution(privateKey, messageToSign, pubKeys, RPoints) { 55 | let messageBytes = this.cryptoUtils.HexToByteArray(messageToSign); 56 | 57 | let playerData = this.getPlayerData(privateKey, messageToSign); 58 | let sigComponents = this.getSignatureComponentsForPlayer(playerData, messageToSign); 59 | let aggregatedRPoint = this.getAggregatedRPoint(RPoints); 60 | 61 | // console.log('Signature Contribution Inputs:'); 62 | // console.log('Aggregated R Point: ' + this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(aggregatedRPoint))); 63 | // console.log('PubKeys: ' + pubKeys.map((pubKey) => { return this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(pubKey)); }).join('\n')); 64 | // console.log('Message: ' + messageBytes); 65 | // console.log('PlayerData:'); 66 | // console.log('\tSecret Key: '+this.cryptoUtils.ByteArrayToHex(playerData.secretKeyBytes)); 67 | // console.log('\tPublic Key: '+this.cryptoUtils.ByteArrayToHex(playerData.publicKeyBytes)); 68 | // console.log('\tMessage Prefix: '+this.cryptoUtils.ByteArrayToHex(playerData.messagePrefix)); 69 | // console.log('\tZValue: '+playerData.zValue); 70 | // console.log('Signature Components:'); 71 | // console.log('\trHash:' + sigComponents.rHash.toString(16)); 72 | // console.log('\tRCommitment:' + sigComponents.RPointCommitment.toString(16)); 73 | // console.log('\tRPoint:' + this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(sigComponents.RPoint).toString(16))); 74 | 75 | return this.getSignatureContributionInternal(aggregatedRPoint, pubKeys, messageBytes, playerData, sigComponents); 76 | } 77 | 78 | SignMessageSingle(message, privateKey) { 79 | if (typeof message !== 'string') { 80 | throw new Error("Message parameter must be a hexadecimal string."); 81 | } 82 | 83 | let nonHexadecimalRegexp = RegExp('[^ABCDEF1234567890]'); 84 | if (nonHexadecimalRegexp.test(message)) { 85 | throw new Error("Message parameter must be a hexadecimal string."); 86 | } 87 | 88 | return this.ec.sign(message, privateKey); 89 | } 90 | 91 | VerifyMessageSingle(message, signature, pubKey) { 92 | return this.ec.verify(message, signature, pubKey); 93 | } 94 | 95 | SignMessageMultiple(signatureContributions, RPoints) { 96 | let aggregatedRPoint = this.getAggregatedRPoint(RPoints); 97 | 98 | // console.log('SignMessageMultiple Inputs:'); 99 | // console.log('Signature Contributions:'); 100 | // console.log(signatureContributions.map((sigcon) => { 101 | // return sigcon.toString(16); 102 | // })); 103 | // console.log('Aggregated R Point:'); 104 | // console.log(this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(aggregatedRPoint))); 105 | 106 | let aggregatedSignature = null; 107 | 108 | for (let i = 0; i < signatureContributions.length; i++) { 109 | // console.log('Signature Contribution '+i+': '+signatureContributions[i].toString(16)); 110 | if (aggregatedSignature === null) { 111 | aggregatedSignature = signatureContributions[i]; 112 | // console.log("Aggregate sig progress:" + aggregatedSignature); 113 | } else { 114 | aggregatedSignature = aggregatedSignature.add(signatureContributions[i]); // bigint addition 115 | // console.log("Aggregate sig progress:" + aggregatedSignature); 116 | } 117 | } 118 | 119 | let sigStruct = this.ec.makeSignature({ 120 | R: aggregatedRPoint, 121 | S: aggregatedSignature, 122 | Rencoded: this.ec.encodePoint(aggregatedRPoint), 123 | Sencoded: this.ec.encodeInt(aggregatedSignature) 124 | }); 125 | 126 | return sigStruct.toHex(); 127 | } 128 | 129 | GetAggregatedPublicKey(pubKeys) { 130 | // console.log('Generating aggregated public key point.'); 131 | pubKeys.sort(this.SortPointsByHexRepresentation.bind(this)); 132 | 133 | let aggregatedPublicKeyPoint = null; 134 | let aHashComponent = null; 135 | let aggregationComponentPoint = null; 136 | 137 | for (let i = 0; i < pubKeys.length; i++) { 138 | aHashComponent = this.getAHashSignatureComponent(pubKeys[i], pubKeys); 139 | aggregationComponentPoint = pubKeys[i].mul(aHashComponent); 140 | // console.log('AHash: ' + aHashComponent); 141 | 142 | if (aggregatedPublicKeyPoint === null) { 143 | aggregatedPublicKeyPoint = aggregationComponentPoint; 144 | } else { 145 | aggregatedPublicKeyPoint = aggregatedPublicKeyPoint.add(aggregationComponentPoint); // point addition 146 | } 147 | } 148 | 149 | // console.log(this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(aggregatedPublicKeyPoint))); 150 | return aggregatedPublicKeyPoint; 151 | } 152 | 153 | SortPointsByHexRepresentation(point1, point2) { 154 | let point1Hex = this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(point1)); 155 | let point2Hex = this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(point2)); 156 | 157 | return point1Hex.localeCompare(point2Hex); 158 | } 159 | 160 | GetRPointValid(RPoint, RCommitment) { 161 | let RPointEncoded = this.ec.encodePoint(RPoint); 162 | let digest = this.ec.hashInt(RPointEncoded); 163 | 164 | return (digest.eq(RCommitment)); 165 | } 166 | 167 | getAHashSignatureComponent(playerPublicKeyPoint, pubKeys) { 168 | let hashArguments = [this.ec.encodePoint(playerPublicKeyPoint)]; 169 | // console.log('Hash Arguments: ' + hashArguments); 170 | 171 | for (let i = 0; i < pubKeys.length; i++) { 172 | hashArguments.push(this.ec.encodePoint(pubKeys[i])); 173 | // console.log('Hash Arguments ('+i+'): ' + hashArguments); 174 | } 175 | 176 | return this.ec.hashInt.apply(this.ec, hashArguments); 177 | } 178 | 179 | getPlayerData(secret, messageToSign) { 180 | let key = this.ec.keyFromSecret(secret); // hex string, array or Buffer 181 | 182 | return { 183 | 'secretKeyBytes': key.privBytes(), 184 | 'publicKeyBytes': key.pubBytes(), 185 | 'publicKeyPoint': this.ec.decodePoint(key.pubBytes()), 186 | 'messagePrefix': key.messagePrefix(), 187 | // 'zValue': this.getZValue(secret, messageToSign) 188 | 'zValue': this.getZValueDeterministic(secret) 189 | }; 190 | } 191 | 192 | getZValueDeterministic(secret) { 193 | let zValue = this.cryptoUtils.ByteArrayToHex(blakejs.blake2b(this.cryptoUtils.HexToByteArray(secret))); 194 | return this.cryptoUtils.HexToByteArray(zValue); 195 | } 196 | 197 | getZValue(secret, messageToSign) { 198 | if (!this.zValues[secret][messageToSign]) { 199 | if (!this.zValues[secret]) { 200 | this.zValues[secret] = {}; 201 | } 202 | 203 | this.zValues[secret][messageToSign] = this.getRandomBytes(32); 204 | } 205 | 206 | return this.zValues[secret][messageToSign]; 207 | } 208 | 209 | getRandomBytes(length) { 210 | let result = []; 211 | for (let i = 0; i < length; i++) { 212 | result.push(Math.floor(Math.random() * 256)); 213 | } 214 | 215 | return result; 216 | } 217 | 218 | getSignatureComponentsForPlayer(playerData, message) { 219 | message = this.cryptoUtils.HexToByteArray(message); 220 | 221 | // console.log('Signature components inputs:'); 222 | // console.log('PlayerData.MessagePrefix: '+this.cryptoUtils.ByteArrayToHex(playerData.messagePrefix)); 223 | // console.log('PlayerData.zValue: '+this.cryptoUtils.ByteArrayToHex(playerData.zValue)); 224 | // console.log('Message: '+message); 225 | 226 | let r = this.ec.hashInt(playerData.messagePrefix, message, playerData.zValue); 227 | let R = this.ec.g.mul(r); 228 | let Rencoded = this.ec.encodePoint(R); 229 | let t = this.ec.hashInt(Rencoded); 230 | 231 | return { 232 | 'rHash': r, 233 | 'RPoint': R, 234 | 'RPointCommitment': t 235 | }; 236 | } 237 | 238 | getAggregatedRPoint(RPoints) { 239 | // console.log('Aggregated R Point Inputs:'); 240 | // console.log(RPoints.map((RPoint) => { 241 | // return this.cryptoUtils.ByteArrayToHex(this.ec.encodePoint(RPoint)); 242 | // })); 243 | 244 | // RPoints.sort(this.sortPointsByHexRepresentation.bind(this)); 245 | let aggregatedRPoint = null; 246 | 247 | for (let i = 0; i < RPoints.length; i++) { 248 | if (aggregatedRPoint === null) { 249 | aggregatedRPoint = RPoints[i]; 250 | } else { 251 | aggregatedRPoint = aggregatedRPoint.add(RPoints[i]); // point addition 252 | } 253 | } 254 | 255 | return aggregatedRPoint; 256 | } 257 | 258 | getKHash(aggregatedRPoint, aggregatedPublicKeyPoint, message) { 259 | return this.ec.hashInt(this.ec.encodePoint(aggregatedRPoint), this.ec.encodePoint(aggregatedPublicKeyPoint), message); 260 | } 261 | 262 | getSignatureContributionInternal(aggregatedRPoint, pubKeys, message, playerData, signatureComponents) { 263 | let aggregatedPublicKeyPoint = this.GetAggregatedPublicKey(pubKeys); 264 | let aHashSignatureComponent = this.getAHashSignatureComponent(playerData['publicKeyPoint'], pubKeys); 265 | let kHash = this.getKHash(aggregatedRPoint, aggregatedPublicKeyPoint, message); 266 | 267 | let signatureContribution = kHash.mul(this.ec.decodeInt(playerData['secretKeyBytes'])); 268 | signatureContribution = signatureContribution.mul(aHashSignatureComponent); // not absolutely certain about the order of operations here. 269 | signatureContribution = signatureComponents['rHash'].add(signatureContribution); // bigint addition 270 | signatureContribution = signatureContribution.umod(this.ec.curve.n); // appears to not be needed? Rust implementation doesn't seem to have it, even for single sig. 271 | 272 | return signatureContribution; 273 | } 274 | 275 | } 276 | 277 | export default BlockSigner; 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NanoFusion 2 | 3 | NanoFusion is a trustless mixing protocol for the Nano cryptocurrency. It is loosely inspired by CashFusion (https://github.com/cashshuffle/spec/blob/master/CASHFUSION.md), the protocol developed by Jonald Fyookball for Bitcoin Cash. 4 | 5 | ### Getting Started 6 | 7 | If you want to actually try running the software, see [GettingStarted.md](GettingStarted.md). 8 | 9 | If you want to try reading the code, a good starting point is [testblocksigning.js](https://github.com/unyieldinggrace/nanofusion/blob/master/experiments/testblocksigning.js), which gives a concise demonstration of signing a block with an aggregated signature and verifying it as if it were a standard Nano block. The rest of the repo is mostly to do with communications between signing parties. 10 | 11 | ### See it in action 12 | * Joint-Account Demo: [https://www.youtube.com/watch?v=E-m64VPORbw](https://www.youtube.com/watch?v=E-m64VPORbw) 13 | * Video whitepaper: [https://www.youtube.com/watch?v=CtMMETZcAQY](https://www.youtube.com/watch?v=CtMMETZcAQY) 14 | * Mixing Demo: [https://www.youtube.com/watch?v=JScTUJr8jac](https://www.youtube.com/watch?v=JScTUJr8jac) 15 | 16 | ### Status and Security Issues 17 | 18 | NanoFusion is currently in an alpha (or even pre-alpha) state. The code published here is intended as a proof-of-concept ONLY. There are some outstanding security issues, meaning this software is not ready to be used for anything other than experimentation. You can see more information about these outstanding issues [in the GitHub issue tracker](https://github.com/unyieldinggrace/nanofusion/issues). 19 | 20 | ## Describing the Problem 21 | 22 | Because Nano is account-based, rather than UTXO-based, some changes are required in order to created a trustless mixing protocol. In a UTXO-based currency, one transaction can have many inputs. CashFusion works by having these many inputs come from different owners. In contrast, each Nano transaction has exactly one sending account and one receiving account. This makes it difficult to mix coins without trusting a central server, because at some point, someone has to have the authority to cryptographically sign the send-transactions from the mixing account. Whoever can sign transactions from the mixing account can send all the money to themselves if they wish. 23 | 24 | ## Accounts with Aggregated Signatures 25 | 26 | To get around this trust problem, we could modify the Nano protocol in some way so that nodes would require multiple signatures on some types transactions before accepting them. But that is ugly, it goes against the minimalist spirit of Nano, and it requires navigating the politics of a protocol change. A more ideal solution would be to do signature aggregation. 27 | 28 | Nano uses the Ed25519 curve, which means its signatures are Schnorr signatures. Schnorr signatures have the useful property that aggregated signatures can exist which are indistinguishable from single signatures. An aggregate signature is a signature that is created by two or more parties collaborating to sign a message, without any of the parties having to reveal their private key to the others. This is useful, because once we create an account that can be signed with an aggregate signature, transactions can only occur on that account if all the signers individually agree to them. Because these aggregate signatures are indistinguishable from single signatures, a transaction for this type of joint account can be submitted to the Nano network and verified by the nodes as if it were any other transaction. 29 | 30 | There is javascript code in this repository for creating an aggregate signature on the Ed25519 curve. The original [Rust implementation](https://github.com/KZen-networks/multi-party-eddsa) by KZen Networks uses a SHA-512 hash. This javascript implementation uses the Ed25519 implementation in the [elliptic](https://www.npmjs.com/package/elliptic) npm library, but replaces the SHA-512 hashes with Blake2B hashes (using [blakejs](https://www.npmjs.com/package/blakejs)) in order to be compatible with Nano. 31 | 32 | ## The trustless mixing algorithm 33 | 34 | Aggregated signatures are a technical challenge, but on their own, they are not enough to enable trustless coin mixing. For that, we need a more detailed communication protocol, which will be described below. 35 | 36 | ### First Problem: Refunds 37 | 38 | In order to allow a group of parties to trustlessly mix their Nano funds, we need to do the following: 39 | * create an account that can only send funds if _all_ the parties sign the send transaction. 40 | * get a list of accounts from each participant where their funds will be sent after they have gone through the mixing account. 41 | * generate a series of send transactions from the mixing account which distribute all the funds to the accounts specified by the participants. 42 | * Have all participants send _unsigned_ copies of the transactions that they will eventually broadcast to send their funds into the mixing account. 43 | * Have all the participants sign the send transactions out of the mixing account. 44 | * Once all the players have verified that there are signed send transactions _out_ of the mixing account, they can safely send all of their funds _to_ the mixing account, knowing that once everyone's funds have arrived, they will be able to get their own funds out, but no one will be able to steal funds from anyone else, because the outgoing send transactions have been pre-arranged. 45 | 46 | This is the basic concept of trustless mixing. However, it presents a practical problem. What if one of the participants is malicious, or loses their network connection part-way through the process? What if they sign the transactions _out_ of the mixing account, but never send their funds _into_ the mixing account? Everyone else will have their funds burned. Nano transactions must happen in a specific order, since each transaction references the hash of the transaction before it. The transactions to distribute funds _out_ of the mixing account cannot be executed until all of the send transactions _into_ the mixing account have been completed. How then can we prevent funds from being burned if one party is malicious or their connection fails? 47 | 48 | ### Solving the refund problem 49 | 50 | To solve the refund problem, we simply pre-sign multiple alternative sets of transactions which distribute the funds in the mixing account back to their original owners. Then the original owners can start the process over, without the "bad" party participating. 51 | 52 | For instance, if we were going to mix accounts A, B and C, then we would have all players sign transactions that send out the mixed funds (the success case), but also sign the following sequences of transactions: 53 | 54 | * Mix -> A, Mix -> B 55 | * Mix -> A, Mix -> C 56 | * Mix -> B, Mix -> A 57 | * Mix -> B, Mix -> C 58 | * Mix -> C, Mix -> A 59 | * Mix -> C, Mix -> B 60 | 61 | This way, no matter who drops out, the other participants will be able to redeem their funds. For example, if B drops out, then we could execute the sequence `Mix -> A, Mix -> C`. If both A and B drop out, then C can still redeem their funds, because they can execute the `Mix -> C` transaction from either the `Mix -> C, Mix -> A` sequence or the `Mix -> C, Mix -> B` sequence. 62 | 63 | However, there is a problem with this strategy. The number of possible transaction sequences goes up dramatically with the number of participants. It is not even exponential, but actually combinatoric (an even steeper curve). If there are 10 input accounts, then there are over 3.6 million possible sequences in which those refund transactions might need to happen. Creating 3.6 million aggregated signatures for all those hypothetical transactions will take an annoyingly large amount of time and bandwidth. Having 20 input accounts is totally out the question. 64 | 65 | To get around this, we create binary tree of aggregated accounts. This drastically reduces the number of exit paths for which we need to sign hypothetical transaction chains. Instead of A, B and C all paying directly into the mixing account, we do this: 66 | 67 | * A and B pay into AB 68 | * C and D pay into CD 69 | * E and F pay into EF 70 | * AB and CD pay into ABCD 71 | * ABCD and EF pay into ABCDEF 72 | 73 | Now, let's suppose that C drops out before sending funds to CD. Everyone else has published their send transaction. To get everyone's money back, we only need to execute these transactions: 74 | 75 | * ABCD -> AB 76 | * AB -> A 77 | * AB -> B 78 | * CD -> D 79 | * ABCDEF -> EF 80 | * EF -> E 81 | * EF -> F 82 | 83 | We don't need any path where ABCDEF pays to E, then to B, then to C. Since transactions are in a tree, not individual, there are fewer valid orders to execute them in. When everyone pays to one account, we need to be able to execute send transactions in any order, because no player can depend on any other in case one drops out. But with a tree, if C doesn't pay to CD, then CD cannot pay to ABCD, so the chain goes no further, and D can get execute a send transaction for a refund from CD without worrying about what A, B, E or F are doing. 84 | 85 | ## Hiding linkages between inputs and outputs 86 | 87 | One problem that still remains is hiding the linkages between inputs and outputs. The mixing protocol above allows mixing funds, safe in the knowledge that no funds can be stolen at any point. However, mixing is much less useful if the other participants, or a server that coordinates the process, is able to tell that the same person owns input account A and output account B. The point of mixing is to obscure that information. 88 | 89 | To make that happen, we need a way for all of the participants to communicate a list of input and output accounts to each other without knowing which participant provided which account (and ideally without the server knowing either). To do that, we implement a scheme called "ring communication". 90 | 91 | Suppose that 3 participants connect to a server, and announce that they will be providing 1 input each (iA, iB, iC) and 2 outputs each (oA, oB, oC, oD, oE, oF). 92 | 93 | Each participant supplies a public encryption key to the server, so that the server cannot read messages sent between players. To keep things anonymous, each participant supplies 6 new accounts, 18 accounts in total. 94 | 95 | Ring communication occurs by the server notifying a random participant to start the ring by sending a message to their left-side neighbour. The participant does this by sending a message to the server, encrypted with their left-side neighbour's public key. 96 | 97 | Participants start ring communication by sending sets of addresses to each other (say, 3 at a time), randomised from their own and others' lists. At any time, no player (except the initiator) knows whether the player before them is the initiator, so they do not know whether the first 3 addresses belong together. This goes on until all participants have seen 18 unique addresses, and verified that all of their own desired outputs are present in that list of 18. 98 | 99 | Ring communication begins again, this time with each player passing on the full list of 18 addresses, minus 1-4 addresses that are theirs, but which they do not wish to use. They randomly choose how many of their own to remove (1-4), so that it is not clear whether the list is down 2 because of 2 players, or one player removing 2 addresses. The first player in the ring must remove at least 2 to preserve the ambiguity. 100 | 101 | Once the list of addresses is down to 6, all unwanted addresses have been discarded, and no player knows which addresses belong to any other player. 102 | 103 | At this point, the server creates a binary tree for the input accounts, and sends messages to the participants to have them create aggregated-signature addresses matching the layers of the binary tree, down to the single root element, which is the mixing account. The server also constructs a set of transactions _out_ of the mixing account, which it asks all participants to sign. 104 | 105 | Once all of these transactions have been created and signed, all participants can go ahead and send their funds out of their input accounts down to the first layer of the binary tree, safe in the knowledge that the only possible outcomes are that the mix succeeds (and no one else knows which output accounts are theirs), or all of their funds are refunded to their original input accounts. 106 | 107 | -------------------------------------------------------------------------------- /client/src/model/Phases/SignTransaction/SignTransactionAnnounceSignatureContributionPhase.js: -------------------------------------------------------------------------------- 1 | import MixEventTypes from "../../EventTypes/MixEventTypes"; 2 | import BaseSigningPhase from "./BaseSigningPhase"; 3 | import * as NanoCurrency from "nanocurrency"; 4 | 5 | class SignTransactionAnnounceSignatureContributionPhase extends BaseSigningPhase { 6 | constructor(sessionClient, signatureDataCodec, blockSigner, messageToSign) { 7 | super(); 8 | this.Name = 'Announce Signature Contributions'; 9 | this.sessionClient = sessionClient; 10 | this.signatureDataCodec = signatureDataCodec; 11 | this.blockSigner = blockSigner; 12 | this.messageToSign = messageToSign; 13 | 14 | this.sessionClient.SubscribeToEvent(MixEventTypes.AnnounceSignatureContribution, this.onPeerAnnouncesSignatureContribution.bind(this)); 15 | this.sessionClient.SubscribeToEvent(MixEventTypes.RequestSignatureContributions, this.onPeerRequestsSignatureContributions.bind(this)); 16 | 17 | this.myPrivateKeys = null; 18 | this.myPubKeys = null; 19 | this.foreignPubKeys = null; 20 | this.latestState = null; 21 | } 22 | 23 | executeInternal(state) { 24 | this.latestState = state; 25 | 26 | if (this.KNOWN_TRANSACTIONS.indexOf(this.messageToSign) === -1) { 27 | console.log('Signing Phase: Announce Signature Contributions for "'+this.messageToSign+'"'); 28 | } 29 | 30 | // console.log('Signing Phase: Announcing Signature Contributions.'); 31 | this.myPrivateKeys = state.MyPrivateKeys; 32 | this.myPubKeys = state.MyPubKeys; 33 | this.foreignPubKeys = state.ForeignPubKeys; 34 | 35 | this.sessionClient.SendEvent(MixEventTypes.RequestSignatureContributions, {MessageToSign: this.messageToSign}); 36 | this.broadcastMySignatureContributions(); 37 | 38 | if (this.getAllPubKeysForTransactionAreMine()) { 39 | this.latestState.SignatureComponentStore.AddJointSignatureForHash(this.messageToSign, this.getJointSignature(this.messageToSign)); 40 | 41 | this.emitStateUpdate({ 42 | TransactionsSigned: Object.keys(this.latestState.SignatureComponentStore.GetAllJointSignaturesForHashes()) 43 | }); 44 | 45 | this.markPhaseCompleted(); 46 | } 47 | } 48 | 49 | async NotifyOfUpdatedState(state) { 50 | this.latestState = state; 51 | } 52 | 53 | onPeerAnnouncesSignatureContribution(data) { 54 | if (!this.getAnnouncementIsForCorrectMessage(data)) { 55 | // console.log('Signature contributrion for incorrect message. Skippking.'); 56 | return; 57 | } 58 | 59 | if (data.Data.MessageToSign === 'FC86A202843AA75389383FA0C5ACE814B948B7CB0FBA428CC378ED83B84D9364') { 60 | console.log(this.latestState.SignatureComponentStore); 61 | } 62 | 63 | if (!this.IsRunning()) { 64 | return; 65 | } 66 | 67 | this.checkIncomingMessageIsValid(data, 'SignatureContribution'); 68 | 69 | let decodedSignatureContribution = this.signatureDataCodec.DecodeSignatureContribution(data.Data.SignatureContribution); 70 | let currentSignatureContribution = this.latestState.SignatureComponentStore.GetSignatureContribution(data.Data.MessageToSign, data.Data.PubKey); 71 | if (currentSignatureContribution && (!currentSignatureContribution.eq(decodedSignatureContribution))) { 72 | throw new Error('Peer '+data.Data.PubKey+' tried to update Signature Contribution. This is not allowed. Skipping.'); 73 | } 74 | 75 | this.latestState.SignatureComponentStore.AddSignatureContribution(data.Data.MessageToSign, data.Data.PubKey, decodedSignatureContribution); 76 | // this.emitStateUpdate({ 77 | // SignatureComponentStore: this.latestState.SignatureComponentStore 78 | // }); 79 | 80 | if (this.getAllSignatureContributionsReceivedAndJointSignatureValidated()) { 81 | this.latestState.SignatureComponentStore.AddJointSignatureForHash(this.messageToSign, this.getJointSignature(this.messageToSign)); 82 | 83 | this.emitStateUpdate({ 84 | TransactionsSigned: Object.keys(this.latestState.SignatureComponentStore.GetAllJointSignaturesForHashes()) 85 | }); 86 | 87 | this.markPhaseCompleted(); 88 | } 89 | } 90 | 91 | onPeerRequestsSignatureContributions(data) { 92 | if (!this.getAnnouncementIsForCorrectMessage(data)) { 93 | return; 94 | } 95 | 96 | if (this.IsRunning()) { 97 | this.broadcastMySignatureContributions(); 98 | } 99 | } 100 | 101 | broadcastMySignatureContributions() { 102 | let requiredPubKeysHex = this.latestState.AccountTree.GetPubKeysHexForTransactionHash(this.messageToSign); 103 | 104 | this.myPrivateKeys.forEach((privateKey) => { 105 | // console.log('Broadcasting Signature Contribution for message: '+this.messageToSign); 106 | 107 | let pubKeyPoint = this.blockSigner.GetPublicKeyFromPrivate(privateKey); 108 | let pubKeyHex = this.signatureDataCodec.EncodePublicKey(pubKeyPoint); 109 | 110 | if (requiredPubKeysHex.indexOf(pubKeyHex) === -1) { 111 | return true; 112 | } 113 | 114 | let signatureContribution = this.blockSigner.GetSignatureContribution( 115 | privateKey, 116 | this.messageToSign, 117 | this.getAllPubKeys(this.messageToSign), 118 | this.getAllRPoints(this.messageToSign) 119 | ); 120 | 121 | let signatureContributionEncoded = this.signatureDataCodec.EncodeSignatureContribution(signatureContribution); 122 | 123 | this.sessionClient.SendEvent(MixEventTypes.AnnounceSignatureContribution, { 124 | PubKey: this.signatureDataCodec.EncodePublicKey(pubKeyPoint), 125 | MessageToSign: this.messageToSign, 126 | SignatureContribution: signatureContributionEncoded, 127 | Signature: this.blockSigner.SignMessageSingle(signatureContributionEncoded, privateKey).toHex() 128 | }); 129 | }); 130 | } 131 | 132 | getAllSignatureContributionsReceivedAndJointSignatureValidated() { 133 | let requiredForeignPubKeysHex = this.getRequiredForeignPubKeysHexForTransaction(this.messageToSign); 134 | 135 | // if (this.KNOWN_TRANSACTIONS.indexOf(this.messageToSign) === -1) { 136 | // console.log('PubKeys for Message: '+this.messageToSign); 137 | // console.log(requiredForeignPubKeysHex); 138 | // } 139 | 140 | let numForeignSignatureContributions = this.latestState.SignatureComponentStore.GetAllSignatureContributions(this.messageToSign) 141 | ? Object.keys(this.latestState.SignatureComponentStore.GetAllSignatureContributions(this.messageToSign)).length 142 | : 0; 143 | 144 | if (numForeignSignatureContributions !== requiredForeignPubKeysHex.length) { 145 | return false; 146 | } 147 | 148 | // if (this.KNOWN_TRANSACTIONS.indexOf(this.messageToSign) === -1) { 149 | // console.log('Num contributrions required for '+this.messageToSign+': '+requiredForeignPubKeysHex.length); 150 | // console.log('Num contributrions found for '+this.messageToSign+': '+numForeignSignatureContributions); 151 | // } 152 | 153 | let jointSignature = this.getJointSignature(this.messageToSign); 154 | if (!jointSignature) { 155 | return false; 156 | } 157 | 158 | let aggregatedPublicKey = this.blockSigner.GetAggregatedPublicKey(this.getAllPubKeys(this.messageToSign)); 159 | // return this.blockSigner.VerifyMessageSingle(this.messageToSign, jointSignature, aggregatedPublicKey); 160 | 161 | return this.getNanoTransactionIsValid(this.messageToSign, jointSignature, this.signatureDataCodec.EncodePublicKey(aggregatedPublicKey)); 162 | } 163 | 164 | getNanoTransactionIsValid(blockHash, aggregatedSignature, aggPubKeyHex) { 165 | let nanoResult = NanoCurrency.verifyBlock({ 166 | // hash: byteArrayToHex(blockHash), 167 | hash: blockHash, 168 | signature: aggregatedSignature, 169 | publicKey: aggPubKeyHex 170 | }); 171 | 172 | if (!nanoResult) { 173 | console.log('Failed nano verification for '+blockHash); 174 | } 175 | 176 | return nanoResult; 177 | } 178 | 179 | getJointSignature(messageToSign) { 180 | let signatureContributions = this.getAllSignatureContributions(messageToSign); 181 | if (!signatureContributions.length) { 182 | return null; 183 | } 184 | 185 | return this.blockSigner.SignMessageMultiple( 186 | signatureContributions, 187 | this.getAllRPoints(messageToSign) 188 | ); 189 | } 190 | 191 | getAllSignatureContributions(messageToSign) { 192 | let allSignatureContributions = []; 193 | let requiredPubKeysHex = this.getAllPubKeysHex(messageToSign); 194 | 195 | if (messageToSign === 'FC86A202843AA75389383FA0C5ACE814B948B7CB0FBA428CC378ED83B84D9364') { 196 | console.log('Required Pub Keys:'); 197 | console.log(requiredPubKeysHex); 198 | console.log('Available Foreign Pub Keys:'); 199 | console.log(Object.keys(this.latestState.SignatureComponentStore.GetAllSignatureContributions(messageToSign)).length); 200 | console.log(Object.keys(this.latestState.SignatureComponentStore.GetAllSignatureContributions(messageToSign)).join(', ')); 201 | console.log(this.latestState.SignatureComponentStore.GetAllSignatureContributions(messageToSign)); 202 | } 203 | 204 | requiredPubKeysHex.forEach((key) => { 205 | let signatureContribution = this.latestState.SignatureComponentStore.GetSignatureContribution(messageToSign, key); 206 | if (!signatureContribution) { 207 | return true; 208 | } 209 | 210 | allSignatureContributions.push({ 211 | PubKeyHex: key, 212 | SignatureContribution: signatureContribution, 213 | SignatureContributionHex: this.signatureDataCodec.EncodeSignatureContribution(signatureContribution) 214 | }); 215 | }); 216 | 217 | this.myPrivateKeys.forEach((privateKey) => { 218 | let pubKey = this.blockSigner.GetPublicKeyFromPrivate(privateKey); 219 | let pubKeyHex = this.signatureDataCodec.EncodePublicKey(pubKey); 220 | 221 | if (requiredPubKeysHex.indexOf(pubKeyHex) === -1) { 222 | return true; 223 | } 224 | 225 | let signatureContribution = this.blockSigner.GetSignatureContribution( 226 | privateKey, 227 | messageToSign, 228 | this.getAllPubKeys(messageToSign), 229 | this.getAllRPoints(messageToSign) 230 | ); 231 | 232 | allSignatureContributions.push({ 233 | PubKeyHex: pubKeyHex, 234 | SignatureContribution: signatureContribution, 235 | SignatureContributionHex: this.signatureDataCodec.EncodeSignatureContribution(signatureContribution) 236 | }); 237 | }); 238 | 239 | allSignatureContributions.sort((a, b) => { 240 | return a.PubKeyHex.localeCompare(b.PubKeyHex); 241 | }); 242 | 243 | if (messageToSign === 'FC86A202843AA75389383FA0C5ACE814B948B7CB0FBA428CC378ED83B84D9364') { 244 | console.log('Signature contributions for "'+messageToSign+'":'); 245 | console.log(allSignatureContributions); 246 | } 247 | 248 | // if (this.KNOWN_TRANSACTIONS.indexOf(messageToSign) === -1) { 249 | // console.log('Signature contributions for "'+messageToSign+'":'); 250 | // console.log(allSignatureContributions); 251 | // } 252 | 253 | return allSignatureContributions.map((obj) => { 254 | return obj.SignatureContribution; 255 | }); 256 | } 257 | 258 | getAllRPoints(messageToSign) { 259 | let allRPoints = []; 260 | let requiredPubKeysHex = this.getAllPubKeysHex(messageToSign); 261 | 262 | if (this.latestState.SignatureComponentStore.GetAllRPoints(messageToSign)) { 263 | Object.keys(this.latestState.SignatureComponentStore.GetAllRPoints(messageToSign)).forEach((key) => { 264 | allRPoints.push({ 265 | PubKeyHex: key, 266 | RPoint: this.latestState.SignatureComponentStore.GetRPoint(messageToSign, key) 267 | }); 268 | }); 269 | } 270 | 271 | this.myPrivateKeys.forEach((privateKey) => { 272 | let pubKey = this.blockSigner.GetPublicKeyFromPrivate(privateKey); 273 | let pubKeyHex = this.signatureDataCodec.EncodePublicKey(pubKey); 274 | 275 | if (requiredPubKeysHex.indexOf(pubKeyHex) === -1) { 276 | return true; 277 | } 278 | 279 | allRPoints.push({ 280 | PubKeyHex: pubKeyHex, 281 | RPoint: this.blockSigner.GetRPoint(privateKey, messageToSign) 282 | }); 283 | }); 284 | 285 | allRPoints.sort((a, b) => { 286 | return a.PubKeyHex.localeCompare(b.PubKeyHex); 287 | }); 288 | 289 | return allRPoints.map((obj) => { 290 | return obj.RPoint; 291 | }); 292 | } 293 | 294 | getAllPubKeys(messageToSign) { 295 | return this.getAllPubKeysHex(messageToSign).map((pubKeyHex) => { 296 | return this.signatureDataCodec.DecodePublicKey(pubKeyHex); 297 | }); 298 | } 299 | 300 | getAllPubKeysHex(messageToSign) { 301 | return this.latestState.AccountTree.GetPubKeysHexForTransactionHash(messageToSign); 302 | } 303 | 304 | getAllPubKeysForTransactionAreMine() { 305 | let result = true; 306 | 307 | let allPubKeysHex = this.getAllPubKeysHex(this.messageToSign); 308 | let myPubKeysHex = this.myPubKeys.map((pubKey) => { 309 | return this.signatureDataCodec.EncodePublicKey(pubKey); 310 | }); 311 | 312 | allPubKeysHex.forEach((pubKeyHex) => { 313 | if (myPubKeysHex.indexOf(pubKeyHex) === -1) { 314 | result = false; 315 | return false; 316 | } 317 | }); 318 | 319 | return result; 320 | } 321 | } 322 | 323 | export default SignTransactionAnnounceSignatureContributionPhase; 324 | --------------------------------------------------------------------------------