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 |
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 |
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 |
--------------------------------------------------------------------------------