├── .env.dist ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── .yarnrc ├── LICENSE ├── decs.d.ts ├── examples ├── connect.js └── scp.js ├── fixtures └── stellar-message.fixtures.ts ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── prettier.config.js ├── readme.md ├── src ├── connection │ ├── connection-authentication.ts │ ├── connection.ts │ ├── flow-controller.ts │ ├── handshake-message-creator.ts │ ├── is-flood-message.ts │ ├── xdr-buffer-converter.ts │ └── xdr-message-handler.ts ├── crypto-helper.ts ├── index.ts ├── map-unknown-to-error.ts ├── node-config.ts ├── node.ts ├── scp-reader.ts ├── scp-statement-dto.ts ├── stellar-message-router.ts ├── stellar-message-service.ts ├── unique-scp-statement-transform.ts └── worker │ └── crypto-worker.ts ├── test ├── connection-authentication.test.ts ├── flow-controller.test.ts ├── integration │ └── node.test.ts ├── is-flood-message.test.ts ├── stellar-message-service.test.ts ├── xdr-buffer-service.test.ts └── xdr-message-handler.test.ts └── tsconfig.json /.env.dist: -------------------------------------------------------------------------------- 1 | LOG_LEVEL=debug 2 | PRIVATE_KEY=SECRET_DO_NOT_SHARE 3 | LEDGER_VERSION=17 4 | OVERLAY_VERSION=17, 5 | OVERLAY_MIN_VERSION=16 6 | VERSION_STRING=v15.0.0 7 | LISTENING_PORT=11625 8 | RECEIVE_TRANSACTION_MSG=false 9 | RECEIVE_SCP_MSG=true 10 | NETWORK="Public Global Stellar Network ; September 2015" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage 7 | examples 8 | lib 9 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | root: true, 4 | parser: '@typescript-eslint/parser', 5 | plugins: [ 6 | '@typescript-eslint', 7 | ], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | rules: { 13 | "@typescript-eslint/ban-ts-comment": "off" 14 | }, 15 | env: { 16 | node: true 17 | } 18 | }; -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [20.x, 21.x, 22.x] 15 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: pnpm/action-setup@v4 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'pnpm' 25 | - run: pnpm install 26 | - run: pnpm build 27 | - run: pnpm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules 3 | dist/ 4 | lib/ 5 | flow-typed/ 6 | .env 7 | .clinic/ 8 | build/ 9 | .DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /package.json 2 | /node_modules 3 | /lib 4 | /dist 5 | /src/generated 6 | /src/vendor 7 | /docs -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --install.ignore-optional true 2 | ignore-optional true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 stellarbeat.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /decs.d.ts: -------------------------------------------------------------------------------- 1 | declare module "js-xdr/lib/cursor.js" 2 | declare module "js-xdr" -------------------------------------------------------------------------------- /examples/connect.js: -------------------------------------------------------------------------------- 1 | const { xdr, StrKey } = require('@stellar/stellar-base'); 2 | const { createNode } = require('../lib'); 3 | const getConfigFromEnv = require('../lib').getConfigFromEnv; 4 | 5 | let node = createNode(getConfigFromEnv()); 6 | 7 | connect(); 8 | 9 | function connect() { 10 | if (process.argv.length <= 2) { 11 | console.log( 12 | 'Parameters: ' + 'NODE_IP(required) ' + 'NODE_PORT(default: 11625) ' 13 | ); 14 | process.exit(-1); 15 | } 16 | 17 | let ip = process.argv[2]; 18 | let port = 11625; 19 | 20 | let portArg = process.argv[3]; 21 | if (portArg) { 22 | port = parseInt(portArg); 23 | } 24 | 25 | let connectedPublicKey; 26 | let connection = node.connectTo(ip, port); 27 | connection 28 | .on('connect', (publicKey, nodeInfo) => { 29 | console.log('Connected to Stellar Node: ' + publicKey); 30 | console.log(nodeInfo); 31 | connectedPublicKey = publicKey; 32 | //connection.sendStellarMessage(xdr.StellarMessage.getScpState(0)); 33 | }) 34 | .on('data', (stellarMessageJob) => { 35 | const stellarMessage = stellarMessageJob.stellarMessage; 36 | //console.log(stellarMessage.toXDR('base64')) 37 | switch (stellarMessage.switch()) { 38 | case xdr.MessageType.scpMessage(): 39 | let publicKey = StrKey.encodeEd25519PublicKey( 40 | stellarMessage.envelope().statement().nodeId().value() 41 | ).toString(); 42 | console.log( 43 | publicKey + 44 | ' sent StellarMessage of type ' + 45 | stellarMessage.envelope().statement().pledges().switch().name + 46 | ' for ledger ' + 47 | stellarMessage.envelope().statement().slotIndex().toString() 48 | ); 49 | if ( 50 | stellarMessage.envelope().statement().pledges().switch() === 51 | xdr.ScpStatementType.scpStExternalize() 52 | ) { 53 | const value = stellarMessage 54 | .envelope() 55 | .statement() 56 | .pledges() 57 | .externalize() 58 | .commit() 59 | .value(); 60 | const closeTime = xdr.StellarValue.fromXDR(value) 61 | .closeTime() 62 | .toXDR() 63 | .readBigUInt64BE(); 64 | //console.log(new Date(1000 * Number(closeTime))); 65 | } 66 | break; 67 | default: 68 | console.log( 69 | 'rcv StellarMessage of type ' + stellarMessage.switch().name + 70 | ': ' + 71 | stellarMessage.toXDR('base64') 72 | ); 73 | if(stellarMessage.switch().value === 0) { 74 | console.log(stellarMessage.error().msg().toString()); 75 | console.log(stellarMessage.error().code()); 76 | } 77 | break; 78 | } 79 | stellarMessageJob.done(); 80 | }) 81 | .on('error', (err) => { 82 | console.log(err); 83 | }) 84 | .on('close', () => { 85 | console.log('closed connection'); 86 | }) 87 | .on('timeout', () => { 88 | console.log('timeout'); 89 | connection.destroy(); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /examples/scp.js: -------------------------------------------------------------------------------- 1 | const { xdr, StrKey } = require('@stellar/stellar-base'); 2 | const { createNode } = require('../lib'); 3 | const getConfigFromEnv = require('../lib').getConfigFromEnv; 4 | const http = require('http'); 5 | const https = require('https'); 6 | const { ScpReader } = require('../lib/scp-reader'); 7 | const pino = require('pino')(); 8 | let node = createNode(getConfigFromEnv()); 9 | 10 | connect(); 11 | 12 | async function connect() { 13 | if (process.argv.length <= 2) { 14 | console.log( 15 | 'Parameters: ' + 'NODE_IP(required) ' + 'NODE_PORT(default: 11625) ' 16 | ); 17 | process.exit(-1); 18 | } 19 | 20 | let ip = process.argv[2]; 21 | let port = 11625; 22 | 23 | let portArg = process.argv[3]; 24 | if (portArg) { 25 | port = parseInt(portArg); 26 | } 27 | 28 | const nodes = await fetchData('https://api.stellarbeat.io/v1/nodes'); 29 | const nodeNames = new Map( 30 | nodes.map((node) => { 31 | return [node.publicKey, node.name ?? node.publicKey]; 32 | }) 33 | ); 34 | 35 | const scpReader = new ScpReader(pino); 36 | scpReader.read(node, ip, port, nodeNames); 37 | } 38 | 39 | function fetchData(url) { 40 | return new Promise((resolve, reject) => { 41 | const client = url.startsWith('https') ? https : http; 42 | 43 | const request = client.get(url, (response) => { 44 | let data = ''; 45 | 46 | // A chunk of data has been received. 47 | response.on('data', (chunk) => { 48 | data += chunk; 49 | }); 50 | 51 | // The whole response has been received. 52 | response.on('end', () => { 53 | resolve(JSON.parse(data)); 54 | }); 55 | }); 56 | 57 | // Handle errors during the request. 58 | request.on('error', (error) => { 59 | reject(error); 60 | }); 61 | }); 62 | } 63 | 64 | function trimString(str, lengthToShow = 5) { 65 | if (str.length <= lengthToShow * 2) { 66 | return str; 67 | } 68 | 69 | const start = str.substring(0, lengthToShow); 70 | const end = str.substring(str.length - lengthToShow); 71 | 72 | return `${start}...${end}`; 73 | } 74 | -------------------------------------------------------------------------------- /fixtures/stellar-message.fixtures.ts: -------------------------------------------------------------------------------- 1 | import { hash, Keypair, Networks, xdr } from '@stellar/stellar-base'; 2 | import { createSCPEnvelopeSignature } from '../src'; 3 | 4 | export function createDummyExternalizeMessage(keyPair: Keypair) { 5 | const commit = new xdr.ScpBallot({ counter: 1, value: Buffer.alloc(32) }); 6 | const externalize = new xdr.ScpStatementExternalize({ 7 | commit: commit, 8 | nH: 1, 9 | commitQuorumSetHash: Buffer.alloc(32) 10 | }); 11 | const pledges = xdr.ScpStatementPledges.scpStExternalize(externalize); 12 | 13 | const statement = new xdr.ScpStatement({ 14 | nodeId: xdr.PublicKey.publicKeyTypeEd25519(keyPair.rawPublicKey()), 15 | slotIndex: xdr.Uint64.fromString('1'), 16 | pledges: pledges 17 | }); 18 | 19 | const signatureResult = createSCPEnvelopeSignature( 20 | statement, 21 | keyPair.rawPublicKey(), 22 | keyPair.rawSecretKey(), 23 | hash(Buffer.from(Networks.PUBLIC)) 24 | ); 25 | 26 | if (signatureResult.isErr()) { 27 | throw signatureResult.error; 28 | } 29 | 30 | const envelope = new xdr.ScpEnvelope({ 31 | statement: statement, 32 | signature: signatureResult.value 33 | }); 34 | 35 | return xdr.StellarMessage.scpMessage(envelope); 36 | } 37 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "transform": { 3 | ".(ts|tsx)": "ts-jest" 4 | }, 5 | "testPathIgnorePatterns": [ 6 | "/node_modules/", 7 | "/lib/" 8 | ], 9 | "testRegex": "(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$", 10 | "moduleFileExtensions": [ 11 | "ts", 12 | "tsx", 13 | "js", 14 | "json" 15 | ] 16 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stellarbeat/js-stellar-node-connector", 3 | "version": "7.0.1", 4 | "description": "Connect and interact with nodes in the Stellar Network over the tcp protocol", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/stellarbeat/js-stellar-node-connector.git" 8 | }, 9 | "scripts": { 10 | "preversion": "pnpm run build", 11 | "build": "tsc --declaration", 12 | "examples:connect": "pnpm run build; node examples/connect", 13 | "examples:scp": "pnpm run build; node examples/scp", 14 | "test": "jest" 15 | }, 16 | "types": "lib/index.d.ts", 17 | "files": [ 18 | ".env.dist", 19 | "readme.md", 20 | "lib/**", 21 | "LICENSE", 22 | "examples/**", 23 | "pnpm-lock.yaml" 24 | ], 25 | "main": "lib/index.js", 26 | "author": "pieterjan84@github", 27 | "license": "MIT", 28 | "engines": { 29 | "node": "^20.0.0" 30 | }, 31 | "dependencies": { 32 | "@stellar/stellar-base": "12.1.1", 33 | "async": "^3.2.6", 34 | "dotenv": "^16.4.5", 35 | "lru-cache": "^11.0.1", 36 | "neverthrow": "^8.0.0", 37 | "pino": "9.4.0", 38 | "sodium-native": "^4.1.1", 39 | "workerpool": "^9.1.3", 40 | "yn": "^4.0.0" 41 | }, 42 | "devDependencies": { 43 | "@types/async": "^3.2.24", 44 | "@types/jest": "^29.5.12", 45 | "@types/node": "^20.0.0", 46 | "@types/sodium-native": "^2.3.9", 47 | "@types/workerpool": "^6.4.7", 48 | "bignumber.js": "9.1.2", 49 | "eslint": "9.11.1", 50 | "jest": "29.7.0", 51 | "np": "^10.0.7", 52 | "prettier": "3.3.3", 53 | "ts-jest": "29.1.1", 54 | "typescript": "^5.6.2" 55 | }, 56 | "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a" 57 | } 58 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | bracketSpacing: true, 4 | jsxBracketSameLine: false, 5 | printWidth: 80, 6 | proseWrap: 'always', 7 | semi: true, 8 | singleQuote: true, 9 | tabWidth: 2, 10 | trailingComma: 'none', 11 | useTabs: true 12 | }; 13 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 2 | [![test](https://github.com/stellarbeat/js-stellar-node-connector/actions/workflows/test.yml/badge.svg)](https://github.com/stellarbeat/js-stellar-node-connector/actions/workflows/test.yml) 3 | 4 | # stellar-js-node-connector 5 | 6 | Connect and interact with nodes in the Stellar Network over the tcp protocol. 7 | 8 | This package consists of two main classes. Node and Connection. 9 | 10 | The Node class allows you to connect to and accept connections from other nodes. 11 | 12 | A connection to a Node is encapsulated in the Connection class. It handles the Stellar Network handshake and message 13 | authentication. It is a custom [duplex stream](https://nodejs.org/api/stream.html#stream_class_stream_duplex) 14 | in [object mode](https://nodejs.org/api/stream.html#stream_object_mode) that wraps 15 | a [tcp socket](https://nodejs.org/api/net.html#net_class_net_socket) and 16 | respects [backpressure](https://nodejs.org/en/docs/guides/backpressuring-in-streams/). It emits and allows you to 17 | send [Stellar Messages](https://github.com/stellar/js-stellar-base/blob/6e0fa3e1a25910e193041d1f377b71f125ec4d1c/src/generated/stellar-xdr_generated.js#L2470) 18 | . 19 | 20 | Stellar Messages are the [xdr](https://github.com/stellar/stellar-core/tree/master/src/xdr) structures used in Stellar 21 | core used to pass data between nodes. They are made available in javascript thanks to 22 | the [Stellar base](https://github.com/stellar/js-stellar-base) and [js-xdr](https://github.com/stellar/js-xdr) packages. 23 | 24 | ## Install, build and run tests 25 | 26 | `pnpm install` 27 | 28 | `pnpm run build` : builds code in lib folder 29 | 30 | `pnpm run test` 31 | 32 | #### Optional: copy .env.dist to .env and fill in parameters 33 | 34 | ## Usage 35 | 36 | ### Initiate connection to other node 37 | 38 | ``` 39 | import { createNode } from 'src' 40 | let node = createNode(true, getConfigFromEnv()); 41 | //Interact with the public network. Configuration in environment variables. Uses defaults if env values are missing.` 42 | 43 | let connection:Connection = node.connectTo(peerIp, peerPort); //connect to a node; 44 | ``` 45 | 46 | The Connection class wraps a [net socket](https://nodejs.org/api/net.html#net_class_net_socket) and emits the same 47 | events with two twists: 48 | 49 | * the connect event includes PublicKey and NodeInfo (version, overlayVersion,...). 50 | * data/readable passes StellarMessageWork objects that 51 | contain [StellarMessages](https://github.com/stellar/js-stellar-base/blob/6e0fa3e1a25910e193041d1f377b71f125ec4d1c/src/generated/stellar-xdr_generated.js#L2470) 52 | and a 'done' callback. The done callback is needed for the custom flow control protocol implemented in stellar nodes. 53 | This protocol controls the amount of flood messages (transaction, scp) that are sent to peers. 54 | 55 | For example handling an SCP message: 56 | 57 | ``` 58 | connection.on("data", (stellarMessageWork: StellarMessageWork) => { 59 | const stellarMessage = stellarMessageWork.stellarMessage; 60 | if (stellarMessage.switch().value === MessageType.scpMessage().value) { 61 | console.log(stellarMessage.envelope().signature().toString()); 62 | //do work... 63 | //signal done processing for flow control 64 | stellarMessageWork.done(); 65 | } 66 | } 67 | ``` 68 | 69 | To send a StellarMessage to a node use the sendStellarMessage or more generic write method: 70 | 71 | `connection.sendStellarMessage(StellarMessage.getScpState(0));` 72 | 73 | ### Accept connections from other nodes 74 | 75 | *Disclaimer: at the moment this is rather limited and only used for integration testing. For example flow control is not 76 | implemented* 77 | 78 | ``` 79 | node.acceptIncomingConnections(11623, '127.0.0.1'); 80 | node.on("connection", (connection:Connection) => { 81 | connection.on("connect", () => { 82 | console.log("Fully connected and ready to send/receive Stellar Messages"); 83 | }); 84 | connection.on("data", (stellarMessageWork: StellarMessageWork) => { 85 | //do something 86 | }); 87 | }); 88 | ``` 89 | 90 | ### Configuration 91 | 92 | Checkout the NodeConf class. The following env parameters are available: 93 | 94 | * LOG_LEVEL=debug | info | trace 95 | * PRIVATE_KEY //If no secret key is supplied, one is generated at startup. 96 | * [LEDGER_VERSION](https://github.com/stellar/stellar-core/blob/7d73fddb0489081bfc1350a691515ff39556c1d6/src/main/Config.h#L318) 97 | * [OVERLAY_VERSION](https://github.com/stellar/stellar-core/blob/7d73fddb0489081bfc1350a691515ff39556c1d6/src/main/Config.h#L328) 98 | * [OVERLAY_MIN_VERSION](https://github.com/stellar/stellar-core/blob/7d73fddb0489081bfc1350a691515ff39556c1d6/src/main/Config.h#L327) 99 | * [VERSION_STRING](https://github.com/stellar/stellar-core/blob/7d73fddb0489081bfc1350a691515ff39556c1d6/src/main/Config.h#L329) 100 | * LISTENING_PORT=11625 101 | * RECEIVE_TRANSACTION_MSG=true //will the Connection class emit Transaction messages 102 | * RECEIVE_SCP_MSG=true //will the Connection class emit SCP messages 103 | * PEER_FLOOD_READING_CAPACITY=200 //max number of messages that can be processed simultaneously from a peer 104 | * FLOW_CONTROL_SEND_MORE_BATCH_SIZE=40 //number of messages that can be received before sending a FLOW_CONTROL_SEND_MORE and reclaiming flood reading capacity 105 | * PEER_FLOOD_READING_CAPACITY_BYTES=3000000 //max number of bytes that can be processed simultaneously from a peer 106 | * FLOW_CONTROL_SEND_MORE_BATCH_SIZE_BYTES=1000000 //number of bytes that can be received before sending a FLOW_CONTROL_SEND_MORE and reclaiming flood reading capacity 107 | //see [stellar core config](https://github.com/stellar/stellar-core/blob/6177299100b114aa108584053414371f38aebf53/docs/stellar-core_example.cfg#L485) for info on flood control parameters 108 | ### Example: Connect to a node 109 | 110 | You can connect to any node with the example script: 111 | 112 | ``` 113 | pnpm examples:connect ip port 114 | ``` 115 | 116 | You can find ip/port of nodes on https://stellarbeat.io 117 | 118 | The script connects to the node and logs the xdr stellar messages it receives to standard output. 119 | Using [Stellar laboratory](https://laboratory.stellar.org/#xdr-viewer?input=AAAACAAAAAIAAAAAVLkjMqFSTqiF2nhSF6zfatXkIxwm9h3NAah7%2FoJqpfwAAABkAhPUSgAPY%2FIAAAAAAAAAAAAAAAEAAAAAAAAAAwAAAAFHVE4AAAAAACJWAPBnEjR3slaKYj1uzT4ZkcOW8dg2e6shBFN2ro8wAAAAAAAAAAAAAAAAAAKKOwADDUAAAAAAMHXkhQAAAAAAAAABgmql%2FAAAAEAPXdZYvTZvbFUU0phuw5JwH6REiiTS5NiwRvlmtvQacigoyeYWF1PWOyN6ITKUu1CFUb6iY0WKV69y69seTSQI&type=StellarMessage&network=test) 120 | you can inspect the content of the messages without coding. 121 | 122 | ### Publish to npm 123 | 124 | ``` 125 | pnpm np 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /src/connection/connection-authentication.ts: -------------------------------------------------------------------------------- 1 | import { hash, Keypair, xdr } from '@stellar/stellar-base'; 2 | import * as sodium from 'sodium-native'; 3 | import EnvelopeType = xdr.EnvelopeType; 4 | import Uint64 = xdr.Uint64; 5 | import UnsignedHyper = xdr.UnsignedHyper; 6 | import BigNumber from 'bignumber.js'; 7 | import { createSHA256Hmac, verifySignature } from '../crypto-helper'; 8 | 9 | type Curve25519SecretBuffer = Buffer; 10 | type Curve25519PublicBuffer = Buffer; 11 | 12 | interface AuthCert { 13 | publicKeyECDH: Curve25519PublicBuffer; 14 | expiration: UnsignedHyper; 15 | signature: Buffer; 16 | } 17 | 18 | export class ConnectionAuthentication { 19 | secretKeyECDH: Curve25519SecretBuffer; 20 | publicKeyECDH: Curve25519PublicBuffer; 21 | weCalledRemoteSharedKeys: Map = new Map(); 22 | remoteCalledUsSharedKeys: Map = new Map(); 23 | networkId: Buffer; 24 | keyPair: Keypair; 25 | 26 | protected authCert?: AuthCert; 27 | protected authCertExpiration = 0; 28 | 29 | static AUTH_EXPIRATION_LIMIT = 360000; //60 minutes 30 | 31 | constructor(keyPair: Keypair, networkId: Buffer) { 32 | this.networkId = networkId; 33 | this.keyPair = keyPair; 34 | this.secretKeyECDH = Buffer.alloc(32); 35 | sodium.randombytes_buf(this.secretKeyECDH); 36 | this.publicKeyECDH = Buffer.alloc(32); 37 | sodium.crypto_scalarmult_base(this.publicKeyECDH, this.secretKeyECDH); 38 | } 39 | 40 | getAuthCert(validAt: Date): AuthCert { 41 | if ( 42 | !this.authCert || 43 | this.authCertExpiration < 44 | validAt.getTime() + ConnectionAuthentication.AUTH_EXPIRATION_LIMIT / 2 45 | ) { 46 | this.authCert = this.createAuthCert(validAt); 47 | } 48 | 49 | return this.authCert; 50 | } 51 | 52 | getSharedKey( 53 | remotePublicKeyECDH: Curve25519PublicBuffer, 54 | weCalledRemote = true 55 | ): Buffer { 56 | const remotePublicKeyECDHString = remotePublicKeyECDH.toString(); 57 | let sharedKey; 58 | if (weCalledRemote) 59 | sharedKey = this.weCalledRemoteSharedKeys.get(remotePublicKeyECDHString); 60 | else 61 | sharedKey = this.remoteCalledUsSharedKeys.get(remotePublicKeyECDHString); 62 | 63 | if (!sharedKey) { 64 | let buf = Buffer.alloc(sodium.crypto_scalarmult_BYTES); 65 | sodium.crypto_scalarmult(buf, this.secretKeyECDH, remotePublicKeyECDH); 66 | 67 | if (weCalledRemote) 68 | buf = Buffer.concat([buf, this.publicKeyECDH, remotePublicKeyECDH]); 69 | else buf = Buffer.concat([buf, remotePublicKeyECDH, this.publicKeyECDH]); 70 | 71 | const zeroSalt = Buffer.alloc(32); 72 | 73 | sharedKey = createSHA256Hmac(buf, zeroSalt); 74 | if (weCalledRemote) 75 | this.weCalledRemoteSharedKeys.set(remotePublicKeyECDHString, sharedKey); 76 | else 77 | this.remoteCalledUsSharedKeys.set(remotePublicKeyECDHString, sharedKey); 78 | } 79 | 80 | return sharedKey; 81 | } 82 | 83 | public createAuthCert(time: Date): AuthCert { 84 | this.authCertExpiration = 85 | time.getTime() + ConnectionAuthentication.AUTH_EXPIRATION_LIMIT; 86 | const expiration = Uint64.fromString(this.authCertExpiration.toString()); 87 | const rawSigData = Buffer.concat([ 88 | this.networkId, 89 | //@ts-ignore 90 | EnvelopeType.envelopeTypeAuth().toXDR(), 91 | expiration.toXDR(), 92 | this.publicKeyECDH 93 | ]); 94 | const sha256RawSigData = hash(rawSigData); 95 | const signature = this.keyPair.sign(sha256RawSigData); 96 | 97 | return { 98 | publicKeyECDH: this.publicKeyECDH, 99 | expiration: expiration, 100 | signature: signature 101 | }; 102 | } 103 | 104 | public verifyRemoteAuthCert( 105 | time: Date, 106 | remotePublicKey: Buffer, 107 | authCert: xdr.AuthCert 108 | ): boolean { 109 | const expiration = new BigNumber(authCert.expiration().toString()); 110 | if (expiration.lt(Math.round(time.getTime() / 1000))) { 111 | return false; 112 | } 113 | 114 | const rawSigData = Buffer.concat([ 115 | this.networkId, 116 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 117 | //@ts-ignore 118 | EnvelopeType.envelopeTypeAuth().toXDR(), 119 | authCert.expiration().toXDR(), 120 | authCert.pubkey().key() 121 | ]); 122 | const sha256RawSigData = hash(rawSigData); 123 | 124 | return verifySignature(remotePublicKey, authCert.sig(), sha256RawSigData); 125 | } 126 | 127 | public getSendingMacKey( 128 | localNonce: Buffer, 129 | remoteNonce: Buffer, 130 | remotePublicKeyECDH: Curve25519PublicBuffer, 131 | weCalledRemote = true 132 | ): Buffer { 133 | const buf = Buffer.concat([ 134 | weCalledRemote ? Buffer.from([0]) : Buffer.from([1]), 135 | localNonce, 136 | remoteNonce, 137 | Buffer.from([1]) 138 | ]); 139 | 140 | const sharedKey = this.getSharedKey(remotePublicKeyECDH, weCalledRemote); 141 | 142 | return createSHA256Hmac(buf, sharedKey); 143 | } 144 | 145 | public getReceivingMacKey( 146 | localNonce: Buffer, 147 | remoteNonce: Buffer, 148 | remotePublicKeyECDH: Curve25519PublicBuffer, 149 | weCalledRemote = true 150 | ): Buffer { 151 | const buf = Buffer.concat([ 152 | weCalledRemote ? Buffer.from([1]) : Buffer.from([0]), 153 | remoteNonce, 154 | localNonce, 155 | Buffer.from([1]) 156 | ]); 157 | 158 | const sharedKey = this.getSharedKey(remotePublicKeyECDH, weCalledRemote); 159 | 160 | return createSHA256Hmac(buf, sharedKey); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/connection/connection.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { hash, Keypair, StrKey, xdr } from '@stellar/stellar-base'; 3 | import { err, ok, Result } from 'neverthrow'; 4 | import { Socket } from 'net'; 5 | import { ConnectionAuthentication } from './connection-authentication'; 6 | import { createSHA256Hmac, verifyHmac } from '../crypto-helper'; 7 | import { Duplex } from 'stream'; 8 | import xdrMessageCreator from './handshake-message-creator'; 9 | import xdrBufferConverter from './xdr-buffer-converter'; 10 | import * as async from 'async'; 11 | import { 12 | AuthenticatedMessageV0, 13 | parseAuthenticatedMessageXDR 14 | } from './xdr-message-handler'; 15 | import * as P from 'pino'; 16 | import { NodeInfo } from '../node'; 17 | import { FlowController } from './flow-controller'; 18 | import StellarMessage = xdr.StellarMessage; 19 | import MessageType = xdr.MessageType; 20 | import { mapUnknownToError } from '../map-unknown-to-error'; 21 | 22 | type PublicKey = string; 23 | 24 | enum ReadState { 25 | ReadyForLength, 26 | ReadyForMessage, 27 | Blocked 28 | } 29 | 30 | enum HandshakeState { 31 | CONNECTING, 32 | CONNECTED, 33 | GOT_HELLO, 34 | COMPLETED 35 | } 36 | 37 | export type ConnectionOptions = { 38 | ip: string; 39 | port: number; 40 | keyPair: Keypair; 41 | localNodeInfo: NodeInfo; 42 | listeningPort?: number; 43 | remoteCalledUs: boolean; 44 | receiveTransactionMessages: boolean; 45 | receiveSCPMessages: boolean; 46 | }; 47 | 48 | export type StellarMessageWork = { 49 | stellarMessage: StellarMessage; 50 | done: () => void; //flow control: call when done processing 51 | }; 52 | 53 | /** 54 | * Duplex stream that wraps a tcp socket and handles the handshake to a stellar core node and all authentication verification of overlay messages. It encapsulates incoming and outgoing connections to and from stellar nodes. 55 | * 56 | * https://github.com/stellar/stellar-core/blob/9c3e67776449ae249aa811e99cbd6eee202bd2b6/src/xdr/Stellar-overlay.x#L219 57 | * It returns xdr.StellarMessages to the consumer. 58 | * It accepts xdr.StellarMessages when handshake is completed and wraps them in a correct AuthenticatedMessage before sending 59 | * 60 | * inspired by https://www.derpturkey.com/extending-tcp-socket-in-node-js/ 61 | */ 62 | export class Connection extends Duplex { 63 | protected keyPair: Keypair; 64 | protected localListeningPort = 11625; 65 | protected remotePublicKeyECDH?: Buffer; 66 | protected localNonce: Buffer; 67 | protected remoteNonce?: Buffer; 68 | protected localSequence: Buffer; 69 | protected remoteSequence: Buffer; 70 | protected sendingMacKey?: Buffer; 71 | protected receivingMacKey?: Buffer; 72 | protected lengthNextMessage = 0; 73 | protected reading = false; 74 | protected readState: ReadState = ReadState.ReadyForLength; 75 | protected handshakeState: HandshakeState = HandshakeState.CONNECTING; 76 | protected remoteCalledUs = true; 77 | protected receiveTransactionMessages = true; 78 | protected receiveSCPMessages = true; 79 | public localNodeInfo: NodeInfo; 80 | public remoteNodeInfo?: NodeInfo; 81 | public sendMoreMsgReceivedCounter = 0; 82 | public remoteIp: string; 83 | public remotePort: number; 84 | 85 | public remotePublicKey?: string; 86 | public remotePublicKeyRaw?: Buffer; 87 | 88 | constructor( 89 | connectionOptions: ConnectionOptions, 90 | private socket: Socket, 91 | private readonly connectionAuthentication: ConnectionAuthentication, 92 | private flowController: FlowController, 93 | private logger: P.Logger 94 | ) { 95 | super({ objectMode: true }); 96 | this.remoteIp = connectionOptions.ip; 97 | this.remotePort = connectionOptions.port; 98 | this.socket = socket; //if we initiate, could we create the socket here? 99 | if (this.socket.readable) this.handshakeState = HandshakeState.CONNECTED; 100 | this.remoteCalledUs = connectionOptions.remoteCalledUs; 101 | this.socket.setTimeout(2500); 102 | this.connectionAuthentication = connectionAuthentication; 103 | this.keyPair = connectionOptions.keyPair; 104 | this.localNonce = hash(Buffer.from(BigNumber.random())); 105 | this.localSequence = Buffer.alloc(8); 106 | this.remoteSequence = Buffer.alloc(8); 107 | 108 | this.localNodeInfo = connectionOptions.localNodeInfo; 109 | this.receiveSCPMessages = connectionOptions.receiveSCPMessages; 110 | this.receiveTransactionMessages = 111 | connectionOptions.receiveTransactionMessages; 112 | 113 | this.socket.on('close', (hadError) => this.emit('close', hadError)); 114 | this.socket.on('connect', () => this.onConnected()); 115 | this.socket.on('drain', () => this.emit('drain')); 116 | this.socket.on('end', () => this.emit('end')); 117 | this.socket.on('error', (error) => this.emit('error', error)); 118 | this.socket.on('lookup', (e, a, f, h) => this.emit('lookup', e, a, f, h)); 119 | this.socket.on('readable', () => this.onReadable()); 120 | this.socket.on('timeout', () => this.emit('timeout')); 121 | 122 | this.logger = logger; 123 | } 124 | 125 | get localPublicKey(): PublicKey { 126 | return this.keyPair.publicKey(); 127 | } 128 | 129 | get localPublicKeyRaw(): Buffer { 130 | return this.keyPair.rawPublicKey(); 131 | } 132 | 133 | get remoteAddress(): string { 134 | return this.remoteIp + ':' + this.remotePort; 135 | } 136 | 137 | get localAddress(): string { 138 | return this.socket.localAddress + ':' + this.socket.localPort; 139 | } 140 | 141 | public connect(): void { 142 | this.handshakeState = HandshakeState.CONNECTING; 143 | this.socket.connect(this.remotePort, this.remoteIp); 144 | } 145 | 146 | public isConnected(): boolean { 147 | return this.handshakeState === HandshakeState.COMPLETED; 148 | } 149 | 150 | public end(): this { 151 | this.socket.end(); 152 | return this; 153 | } 154 | 155 | public destroy(error?: Error): this { 156 | this.socket.destroy(error); 157 | return this; 158 | } 159 | 160 | /** 161 | * Fires when the socket has connected. This method initiates the 162 | * handshake and if there is a failure, terminates the connection. 163 | */ 164 | protected onConnected(): void { 165 | this.logger.debug( 166 | { 167 | remote: this.remoteAddress, 168 | local: this.localAddress 169 | }, 170 | 'Connected to socket, initiating handshake' 171 | ); 172 | this.handshakeState = HandshakeState.CONNECTED; 173 | const result = this.sendHello(); 174 | if (result.isErr()) { 175 | this.logger.error( 176 | { remote: this.remoteAddress, local: this.localAddress }, 177 | result.error.message 178 | ); 179 | this.socket.destroy(result.error); 180 | } 181 | } 182 | 183 | protected onReadable(): void { 184 | this.logger.trace( 185 | { remote: this.remoteAddress, local: this.localAddress }, 186 | 'Rcv readable event' 187 | ); 188 | 189 | //a socket can receive a 'readable' event when already processing a previous readable event. 190 | // Because the same internal read buffer is processed (the running whilst loop will also loop over the new data), 191 | // we can safely ignore it. 192 | if (this.reading) { 193 | this.logger.trace( 194 | { remote: this.remoteAddress, local: this.localAddress }, 195 | 'Ignoring, already reading' 196 | ); 197 | return; 198 | } 199 | 200 | this.reading = true; 201 | //a socket is a duplex stream. It has a write buffer (when we write messages to the socket, to be sent to the peer). And it has a read buffer, data we have to read from the socket, data that is sent by the peer to us. If we don't read the data (or too slow), we will exceed the readableHighWatermark of the socket. This will make the socket stop receiving data or using tcp to signal to the sender that we want to receive the data slower. 202 | if (this.socket.readableLength >= this.socket.readableHighWaterMark) 203 | this.logger.debug( 204 | { 205 | remote: this.remoteAddress, 206 | local: this.localAddress 207 | }, 208 | 'Socket buffer exceeding high watermark' 209 | ); 210 | 211 | let processedMessages = 0; 212 | async.whilst( 213 | (cb) => { 214 | // async loop to interleave sockets, otherwise handling all the messages in the buffer is a blocking loop 215 | return cb(null, this.reading); 216 | }, 217 | (done) => { 218 | let processError = null; 219 | 220 | if (this.readState === ReadState.ReadyForLength) { 221 | if (this.processNextMessageLength()) { 222 | this.readState = ReadState.ReadyForMessage; 223 | } else { 224 | this.reading = false; //we stop processing the buffer 225 | } 226 | } 227 | 228 | if (this.readState === ReadState.ReadyForMessage) { 229 | this.processNextMessage() 230 | .map((containedAMessage) => { 231 | if (containedAMessage) { 232 | this.readState = ReadState.ReadyForLength; 233 | processedMessages++; 234 | } else this.reading = false; 235 | }) 236 | .mapErr((error) => { 237 | processError = error; 238 | this.reading = false; 239 | }); 240 | } 241 | if (this.readState === ReadState.Blocked) { 242 | //we don't process anymore messages because consumer cant handle it. 243 | // When our internal buffer reaches the high watermark, the underlying tcp protocol will signal the sender that we can't handle the traffic. 244 | this.logger.debug( 245 | { remote: this.remoteAddress, local: this.localAddress }, 246 | 'Reading blocked' 247 | ); 248 | this.reading = false; 249 | } 250 | 251 | if (processError || !this.reading) { 252 | done(processError); //end the loop 253 | } else if (processedMessages % 10 === 0) { 254 | //if ten messages are sequentially processed, we give control back to event loop 255 | setImmediate(() => done(null)); //other sockets will be able to process messages 256 | } else done(null); //another iteration 257 | }, 258 | (err) => { 259 | //function gets called when we are no longer reading 260 | if (err) { 261 | const error = mapUnknownToError(err); 262 | this.logger.error( 263 | { remote: this.remoteAddress, local: this.localAddress }, 264 | error.message 265 | ); 266 | this.socket.destroy(error); 267 | } 268 | 269 | this.logger.trace( 270 | { 271 | remote: this.remoteAddress, 272 | local: this.localAddress 273 | }, 274 | 'handled messages in chunk: ' + processedMessages 275 | ); 276 | } 277 | ); 278 | } 279 | 280 | protected processNextMessage(): Result { 281 | //If size bytes are not available to be read, 282 | // null will be returned unless the stream has ended, 283 | // in which case all the data remaining in the internal buffer will be returned. 284 | let data: Buffer | null = null; 285 | try { 286 | data = this.socket.read(this.lengthNextMessage); 287 | } catch (e) { 288 | this.logger.error( 289 | { 290 | remote: this.remoteAddress, 291 | error: mapUnknownToError(e).message, 292 | length: this.lengthNextMessage 293 | }, 294 | 'Error reading from socket' 295 | ); 296 | return err(mapUnknownToError(e)); 297 | } 298 | if (!data || data.length !== this.lengthNextMessage) { 299 | this.logger.trace( 300 | { 301 | remote: this.remoteAddress, 302 | local: this.localAddress 303 | }, 304 | 'Not enough data left in buffer' 305 | ); 306 | return ok(false); 307 | } 308 | 309 | const result = parseAuthenticatedMessageXDR(data); //if transactions are not required, we avoid parsing them to objects and verifying the macs to gain performance 310 | if (result.isErr()) { 311 | return err(result.error); 312 | } 313 | 314 | const authenticatedMessageV0XDR = result.value; 315 | 316 | const stellarMessageSize = data.length - 32 - 12; 317 | 318 | const messageType = authenticatedMessageV0XDR.messageTypeXDR.readInt32BE(0); 319 | this.logger.trace( 320 | { 321 | remote: this.remoteAddress, 322 | local: this.localAddress 323 | }, 324 | 'Rcv msg of type: ' + 325 | messageType + 326 | ' with seq: ' + 327 | authenticatedMessageV0XDR.sequenceNumberXDR.readInt32BE(4) 328 | ); 329 | 330 | this.logger.trace( 331 | { 332 | remote: this.remoteAddress, 333 | local: this.localAddress 334 | }, 335 | 'Rcv ' + messageType 336 | ); 337 | 338 | if ( 339 | ( 340 | [ 341 | MessageType.transaction().value, 342 | MessageType.floodAdvert().value 343 | ] as number[] 344 | ).includes(messageType) && 345 | !this.receiveTransactionMessages 346 | ) { 347 | this.increaseRemoteSequenceByOne(); 348 | this.doneProcessing( 349 | messageType === MessageType.transaction().value 350 | ? MessageType.transaction() 351 | : MessageType.floodAdvert(), 352 | stellarMessageSize 353 | ); 354 | return ok(true); 355 | } 356 | 357 | if ( 358 | messageType === MessageType.scpMessage().value && 359 | !this.receiveSCPMessages 360 | ) { 361 | this.increaseRemoteSequenceByOne(); 362 | return ok(true); 363 | } 364 | 365 | if ( 366 | this.handshakeState >= HandshakeState.GOT_HELLO && 367 | messageType !== MessageType.errorMsg().value 368 | ) { 369 | const result = this.verifyAuthentication( 370 | authenticatedMessageV0XDR, 371 | messageType, 372 | data.slice(4, data.length - 32) 373 | ); 374 | this.increaseRemoteSequenceByOne(); 375 | if (result.isErr()) return err(result.error); 376 | } 377 | 378 | let stellarMessage: xdr.StellarMessage; 379 | try { 380 | stellarMessage = StellarMessage.fromXDR(data.slice(12, data.length - 32)); 381 | } catch (error) { 382 | if (error instanceof Error) return err(error); 383 | else return err(new Error('Error converting xdr to StellarMessage')); 384 | } 385 | 386 | const handleStellarMessageResult = this.handleStellarMessage( 387 | stellarMessage, 388 | stellarMessageSize 389 | ); 390 | if (handleStellarMessageResult.isErr()) { 391 | return err(handleStellarMessageResult.error); 392 | } 393 | if (!handleStellarMessageResult.value) { 394 | this.logger.debug( 395 | { 396 | remote: this.remoteAddress, 397 | local: this.localAddress 398 | }, 399 | 'Consumer cannot handle load, stop reading from socket' 400 | ); 401 | this.readState = ReadState.Blocked; 402 | return ok(false); 403 | } //our read buffer is full, meaning the consumer did not process the messages timely 404 | 405 | return ok(true); 406 | } 407 | 408 | protected verifyAuthentication( 409 | authenticatedMessageV0XDR: AuthenticatedMessageV0, 410 | messageType: number, 411 | body: Buffer 412 | ): Result { 413 | if ( 414 | !this.remoteSequence.equals(authenticatedMessageV0XDR.sequenceNumberXDR) 415 | ) { 416 | //must be handled on main thread because workers could mix up order of messages. 417 | return err(new Error('Invalid sequence number')); 418 | } 419 | 420 | try { 421 | if ( 422 | this.receivingMacKey && 423 | !verifyHmac( 424 | authenticatedMessageV0XDR.macXDR, 425 | this.receivingMacKey, 426 | body 427 | ) 428 | ) { 429 | return err(new Error('Invalid hmac')); 430 | } 431 | } catch (error) { 432 | if (error instanceof Error) return err(error); 433 | else return err(new Error('Error verifying authentication')); 434 | } 435 | 436 | return ok(undefined); 437 | } 438 | 439 | protected processNextMessageLength(): boolean { 440 | this.logger.trace( 441 | { remote: this.remoteAddress, local: this.localAddress }, 442 | 'Parsing msg length' 443 | ); 444 | const data = this.socket.read(4); 445 | if (data) { 446 | this.lengthNextMessage = 447 | xdrBufferConverter.getMessageLengthFromXDRBuffer(data); 448 | this.logger.trace( 449 | { 450 | remote: this.remoteAddress, 451 | local: this.localAddress 452 | }, 453 | 'Next msg length: ' + this.lengthNextMessage 454 | ); 455 | return true; 456 | } else { 457 | this.logger.trace( 458 | { 459 | remote: this.remoteAddress, 460 | local: this.localAddress 461 | }, 462 | 'Not enough data left in buffer' 463 | ); 464 | return false; 465 | //we stay in the ReadyForLength state until the next readable event 466 | } 467 | } 468 | 469 | //return true if handling was successful, false if consumer was overloaded, Error on error 470 | protected handleStellarMessage( 471 | stellarMessage: StellarMessage, 472 | stellarMessageSize: number 473 | ): Result { 474 | switch (stellarMessage.switch()) { 475 | case MessageType.hello(): { 476 | const processHelloMessageResult = this.processHelloMessage( 477 | stellarMessage.hello() 478 | ); 479 | if (processHelloMessageResult.isErr()) { 480 | return err(processHelloMessageResult.error); 481 | } 482 | this.handshakeState = HandshakeState.GOT_HELLO; 483 | 484 | let result: Result; 485 | if (this.remoteCalledUs) result = this.sendHello(); 486 | else result = this.sendAuthMessage(); 487 | 488 | if (result.isErr()) { 489 | return err(result.error); 490 | } 491 | return ok(true); 492 | } 493 | 494 | case MessageType.auth(): { 495 | const completedHandshakeResult = this.completeHandshake(); 496 | if (completedHandshakeResult.isErr()) 497 | return err(completedHandshakeResult.error); 498 | return ok(true); 499 | } 500 | 501 | case MessageType.sendMoreExtended(): { 502 | this.sendMoreMsgReceivedCounter++; //server send more functionality not implemented; only for testing purposes; 503 | return ok(true); 504 | } 505 | 506 | case MessageType.sendMore(): { 507 | this.sendMoreMsgReceivedCounter++; //server send more functionality not implemented; only for testing purposes; 508 | return ok(true); 509 | } 510 | 511 | default: 512 | // we push non-handshake messages to our readable buffer for our consumers 513 | this.logger.debug( 514 | { 515 | remote: this.remoteAddress, 516 | local: this.localAddress 517 | }, 518 | 'Rcv ' + stellarMessage.switch().name 519 | ); 520 | 521 | return ok( 522 | this.push({ 523 | stellarMessage: stellarMessage, 524 | done: () => 525 | this.doneProcessing(stellarMessage.switch(), stellarMessageSize) 526 | } as StellarMessageWork) 527 | ); 528 | } 529 | } 530 | 531 | protected sendHello(): Result { 532 | this.logger.trace( 533 | { remote: this.remoteAddress, local: this.localAddress }, 534 | 'send HELLO' 535 | ); 536 | const certResult = xdrMessageCreator.createAuthCert( 537 | this.connectionAuthentication 538 | ); 539 | if (certResult.isErr()) return err(certResult.error); 540 | 541 | const helloResult = xdrMessageCreator.createHelloMessage( 542 | this.keyPair.xdrPublicKey(), 543 | this.localNonce, 544 | certResult.value, 545 | this.connectionAuthentication.networkId, 546 | this.localNodeInfo.ledgerVersion, 547 | this.localNodeInfo.overlayVersion, 548 | this.localNodeInfo.overlayMinVersion, 549 | this.localNodeInfo.versionString, 550 | this.localListeningPort 551 | ); 552 | 553 | if (helloResult.isErr()) { 554 | return err(helloResult.error); 555 | } 556 | 557 | this.write(helloResult.value); 558 | 559 | return ok(undefined); 560 | } 561 | 562 | protected completeHandshake(): Result { 563 | if (this.remoteCalledUs) { 564 | const authResult = this.sendAuthMessage(); 565 | if (authResult.isErr()) return err(authResult.error); 566 | } 567 | 568 | this.logger.debug( 569 | { remote: this.remoteAddress, local: this.localAddress }, 570 | 'Handshake Completed' 571 | ); 572 | this.handshakeState = HandshakeState.COMPLETED; 573 | this.socket.setTimeout(10000); 574 | 575 | this.emit('connect', this.remotePublicKey, this.remoteNodeInfo); 576 | this.emit('ready'); 577 | 578 | if (!this.remoteNodeInfo) 579 | throw new Error('No remote overlay version after handshake'); 580 | 581 | const stellarMessageOrNull = this.flowController.start(); 582 | if (stellarMessageOrNull !== null) 583 | this.sendStellarMessage(stellarMessageOrNull); 584 | 585 | return ok(undefined); 586 | } 587 | 588 | protected doneProcessing( 589 | messageType: MessageType, 590 | stellarMessageSize: number 591 | ): void { 592 | const stellarMessageOrNull = this.flowController.sendMore( 593 | messageType, 594 | stellarMessageSize 595 | ); 596 | if (stellarMessageOrNull !== null) 597 | this.sendStellarMessage(stellarMessageOrNull); 598 | } 599 | 600 | /** 601 | * Convenience method that encapsulates write. Pass callback that will be invoked when message is successfully sent. 602 | * Returns: false if the stream wishes for the calling code to wait for the 'drain' event to be emitted before continuing to write additional data; otherwise true. 603 | */ 604 | public sendStellarMessage( 605 | message: StellarMessage, 606 | cb?: (error: Error | null | undefined) => void 607 | ): boolean { 608 | this.logger.debug( 609 | { remote: this.remoteAddress, local: this.localAddress }, 610 | 'send ' + message.switch().name 611 | ); 612 | return this.write(message, cb); 613 | } 614 | 615 | protected sendAuthMessage(): Result { 616 | this.logger.trace( 617 | { remote: this.remoteAddress, local: this.localAddress }, 618 | 'send auth' 619 | ); 620 | 621 | const authMessageResult = xdrMessageCreator.createAuthMessage( 622 | this.localNodeInfo.overlayVersion >= 28 //remove when min overlay version is 28 623 | ); 624 | if (authMessageResult.isErr()) return err(authMessageResult.error); 625 | 626 | this.write(authMessageResult.value); 627 | 628 | return ok(undefined); 629 | } 630 | 631 | protected increaseLocalSequenceByOne(): void { 632 | this.localSequence = this.increaseBufferByOne(this.localSequence); 633 | } 634 | 635 | protected increaseBufferByOne(buf: Buffer): Buffer { 636 | //todo: move to helper 637 | for (let i = buf.length - 1; i >= 0; i--) { 638 | if (buf[i]++ !== 255) break; 639 | } 640 | return buf; 641 | } 642 | 643 | protected increaseRemoteSequenceByOne(): void { 644 | this.remoteSequence = this.increaseBufferByOne(this.remoteSequence); 645 | } 646 | 647 | protected authenticateMessage( 648 | message: xdr.StellarMessage 649 | ): Result { 650 | try { 651 | const xdrAuthenticatedMessageV0 = new xdr.AuthenticatedMessageV0({ 652 | sequence: xdr.Uint64.fromXDR(this.localSequence), 653 | message: message, 654 | mac: this.getMacForAuthenticatedMessage(message) 655 | }); 656 | 657 | //@ts-ignore wrong type information. Because the switch is a number, not an enum, it does not work as advertised. 658 | // We have to create the union object through the constructor https://github.com/stellar/js-xdr/blob/892b662f98320e1221d8f53ff17c6c10442e086d/src/union.js#L9 659 | // However the constructor type information is also missing. 660 | const authenticatedMessage = new xdr.AuthenticatedMessage( 661 | //@ts-ignore 662 | 0, 663 | xdrAuthenticatedMessageV0 664 | ); 665 | 666 | if ( 667 | message.switch() !== MessageType.hello() && 668 | message.switch() !== MessageType.errorMsg() 669 | ) 670 | this.increaseLocalSequenceByOne(); 671 | 672 | return ok(authenticatedMessage); 673 | } catch (error) { 674 | if (error instanceof Error) 675 | return err(new Error('authenticateMessage failed: ' + error.message)); 676 | else return err(new Error('authenticateMessage failed')); 677 | } 678 | } 679 | 680 | protected getMacForAuthenticatedMessage( 681 | message: xdr.StellarMessage 682 | ): xdr.HmacSha256Mac { 683 | let mac; 684 | if ( 685 | this.remotePublicKeyECDH === undefined || 686 | this.sendingMacKey === undefined 687 | ) 688 | mac = Buffer.alloc(32); 689 | else 690 | mac = createSHA256Hmac( 691 | Buffer.concat([this.localSequence, message.toXDR()]), 692 | this.sendingMacKey 693 | ); 694 | 695 | return new xdr.HmacSha256Mac({ 696 | mac: mac 697 | }); 698 | } 699 | 700 | protected processHelloMessage(hello: xdr.Hello): Result { 701 | if ( 702 | !this.connectionAuthentication.verifyRemoteAuthCert( 703 | new Date(), 704 | hello.peerId().value(), 705 | hello.cert() 706 | ) 707 | ) 708 | return err(new Error('Invalid auth cert')); 709 | try { 710 | this.remoteNonce = hello.nonce(); 711 | this.remotePublicKeyECDH = hello.cert().pubkey().key(); 712 | this.remotePublicKey = StrKey.encodeEd25519PublicKey( 713 | hello.peerId().value() 714 | ); 715 | this.remotePublicKeyRaw = hello.peerId().value(); 716 | this.remoteNodeInfo = { 717 | ledgerVersion: hello.ledgerVersion(), 718 | overlayVersion: hello.overlayVersion(), 719 | overlayMinVersion: hello.overlayMinVersion(), 720 | versionString: hello.versionStr().toString(), 721 | networkId: hello.networkId().toString('base64') 722 | }; 723 | this.sendingMacKey = this.connectionAuthentication.getSendingMacKey( 724 | this.localNonce, 725 | this.remoteNonce, 726 | this.remotePublicKeyECDH, 727 | !this.remoteCalledUs 728 | ); 729 | this.receivingMacKey = this.connectionAuthentication.getReceivingMacKey( 730 | this.localNonce, 731 | this.remoteNonce, 732 | this.remotePublicKeyECDH, 733 | !this.remoteCalledUs 734 | ); 735 | return ok(undefined); 736 | } catch (error) { 737 | if (error instanceof Error) return err(error); 738 | else return err(new Error('Error processing hello message')); 739 | } 740 | } 741 | 742 | public _read(): void { 743 | if (this.handshakeState !== HandshakeState.COMPLETED) { 744 | return; 745 | } 746 | 747 | if (this.readState === ReadState.Blocked) { 748 | //the consumer wants to read again 749 | this.logger.trace( 750 | { 751 | remote: this.remoteAddress, 752 | local: this.localAddress 753 | }, 754 | 'ReadState unblocked by consumer' 755 | ); 756 | this.readState = ReadState.ReadyForLength; 757 | } 758 | // Trigger a read but wait until the end of the event loop. 759 | // This is necessary when reading in paused mode where 760 | // _read was triggered by stream.read() originating inside 761 | // a "readable" event handler. Attempting to push more data 762 | // synchronously will not trigger another "readable" event. 763 | setImmediate(() => this.onReadable()); 764 | } 765 | 766 | public _write( 767 | message: StellarMessage, 768 | encoding: string, 769 | callback: (error?: Error | null) => void 770 | ): void { 771 | this.logger.trace( 772 | { 773 | remote: this.remoteAddress, 774 | local: this.localAddress 775 | }, 776 | 'write ' + message.switch().name + ' to socket' 777 | ); 778 | 779 | const authenticatedMessageResult = this.authenticateMessage(message); 780 | if (authenticatedMessageResult.isErr()) { 781 | this.logger.error( 782 | { 783 | remote: this.remoteAddress, 784 | local: this.localAddress 785 | }, 786 | authenticatedMessageResult.error.message 787 | ); 788 | return callback(authenticatedMessageResult.error); 789 | } 790 | const bufferResult = xdrBufferConverter.getXdrBufferFromMessage( 791 | authenticatedMessageResult.value 792 | ); 793 | if (bufferResult.isErr()) { 794 | this.logger.error( 795 | { remote: this.remoteAddress, local: this.localAddress }, 796 | bufferResult.error.message 797 | ); 798 | return callback(bufferResult.error); 799 | } 800 | 801 | this.logger.trace( 802 | { 803 | remote: this.remoteAddress, 804 | local: this.localAddress 805 | }, 806 | 'Write msg xdr: ' + bufferResult.value.toString('base64') 807 | ); 808 | if (!this.socket.write(bufferResult.value)) { 809 | this.socket.once('drain', callback); //respecting backpressure 810 | } else { 811 | process.nextTick(callback); 812 | } 813 | } 814 | 815 | _final(cb?: () => void): void { 816 | this.socket.end(cb); 817 | } 818 | } 819 | -------------------------------------------------------------------------------- /src/connection/flow-controller.ts: -------------------------------------------------------------------------------- 1 | import { xdr } from '@stellar/stellar-base'; 2 | import { isFloodMessage } from './is-flood-message'; 3 | import MessageType = xdr.MessageType; 4 | import StellarMessage = xdr.StellarMessage; 5 | 6 | export class FlowController { 7 | private messagesReceivedInCurrentBatch = 0; 8 | private bytesReceivedInCurrentBatch = 0; 9 | 10 | /** 11 | * Uses param names from stellar-core config. Non bytes parameters are counted in number of messages. 12 | * The Reading capacity is the number of messages that can be processed simultaneously. 13 | * Everytime a batch of messages is processed, we request the capacity back through sendmore messages. 14 | * The bytes capacity should be higher than the batch size + the maximum message size, to avoid getting stuck. 15 | * @param peerFloodReadingCapacity 16 | * @param flowControlSendMoreBatchSize 17 | * @param peerFloodReadingCapacityBytes 18 | * @param flowControlSendMoreBatchSizeBytes 19 | */ 20 | constructor( 21 | //we use stellar-core defaults atm 22 | private peerFloodReadingCapacity = 200, 23 | private flowControlSendMoreBatchSize = 40, 24 | private peerFloodReadingCapacityBytes = 300000, 25 | private flowControlSendMoreBatchSizeBytes = 100000 26 | ) {} 27 | 28 | /* 29 | * Start by sending a send-more message with the _total_ capacity available. 30 | */ 31 | start(): null | StellarMessage { 32 | return xdr.StellarMessage.sendMoreExtended( 33 | new xdr.SendMoreExtended({ 34 | numMessages: this.peerFloodReadingCapacity, 35 | numBytes: this.peerFloodReadingCapacityBytes 36 | }) 37 | ); 38 | } 39 | 40 | sendMore( 41 | messageType: MessageType, 42 | stellarMessageSize: number 43 | ): null | xdr.StellarMessage { 44 | if (isFloodMessage(messageType)) { 45 | this.messagesReceivedInCurrentBatch++; 46 | this.bytesReceivedInCurrentBatch += stellarMessageSize; 47 | } 48 | 49 | let shouldSendMore = 50 | this.messagesReceivedInCurrentBatch === this.flowControlSendMoreBatchSize; 51 | shouldSendMore = 52 | shouldSendMore || 53 | this.bytesReceivedInCurrentBatch >= 54 | this.flowControlSendMoreBatchSizeBytes; 55 | 56 | //reclaim the capacity 57 | let sendMoreMessage: xdr.StellarMessage; 58 | if (shouldSendMore) { 59 | sendMoreMessage = xdr.StellarMessage.sendMoreExtended( 60 | new xdr.SendMoreExtended({ 61 | numMessages: this.messagesReceivedInCurrentBatch, //!request back the number of messages we received, not the total capacity like when starting! 62 | numBytes: this.bytesReceivedInCurrentBatch 63 | }) 64 | ); 65 | 66 | this.messagesReceivedInCurrentBatch = 0; 67 | this.bytesReceivedInCurrentBatch = 0; 68 | 69 | return sendMoreMessage; 70 | } 71 | 72 | return null; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/connection/handshake-message-creator.ts: -------------------------------------------------------------------------------- 1 | import { xdr } from '@stellar/stellar-base'; 2 | import { err, ok, Result } from 'neverthrow'; 3 | import AuthCert = xdr.AuthCert; 4 | import Hello = xdr.Hello; 5 | import { ConnectionAuthentication } from './connection-authentication'; 6 | 7 | export default { 8 | createAuthMessage: function ( 9 | flowControlInBytes = false 10 | ): Result { 11 | try { 12 | const auth = new xdr.Auth({ flags: flowControlInBytes ? 200 : 100 }); 13 | // @ts-ignore 14 | const authMessage = new xdr.StellarMessage.auth(auth) as StellarMessage; 15 | return ok(authMessage); 16 | } catch (error) { 17 | if (error instanceof Error) 18 | return err(new Error('Auth msg create failed: ' + error.message)); 19 | else return err(new Error('Auth msg create failed')); 20 | } 21 | }, 22 | 23 | createHelloMessage: function ( 24 | peerId: xdr.PublicKey, 25 | nonce: Buffer, 26 | authCert: xdr.AuthCert, 27 | stellarNetworkId: Buffer, 28 | ledgerVersion: number, 29 | overlayVersion: number, 30 | overlayMinVersion: number, 31 | versionStr: string, 32 | listeningPort: number 33 | ): Result { 34 | try { 35 | const hello = new xdr.Hello({ 36 | ledgerVersion: ledgerVersion, 37 | overlayVersion: overlayVersion, 38 | overlayMinVersion: overlayMinVersion, 39 | networkId: stellarNetworkId, 40 | versionStr: versionStr, 41 | listeningPort: listeningPort, 42 | peerId: peerId, 43 | cert: authCert, 44 | nonce: nonce 45 | }); 46 | 47 | //@ts-ignore 48 | return ok(new xdr.StellarMessage.hello(hello)); 49 | } catch (error) { 50 | let msg = 'CreateHelloMessage failed'; 51 | if (error instanceof Error) msg += ': ' + error.message; 52 | return err(new Error(msg)); 53 | } 54 | }, 55 | 56 | createAuthCert: function ( 57 | connectionAuthentication: ConnectionAuthentication 58 | ): Result { 59 | try { 60 | const curve25519PublicKey = new xdr.Curve25519Public({ 61 | key: connectionAuthentication.publicKeyECDH 62 | }); 63 | 64 | return ok( 65 | new xdr.AuthCert({ 66 | pubkey: curve25519PublicKey, 67 | expiration: connectionAuthentication.getAuthCert(new Date()) 68 | .expiration, 69 | sig: connectionAuthentication.getAuthCert(new Date()).signature 70 | }) 71 | ); 72 | } catch (error) { 73 | if (error instanceof Error) 74 | return err(new Error('createAuthCert failed: ' + error.message)); 75 | else return err(new Error('createAuthCert failed')); 76 | } 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/connection/is-flood-message.ts: -------------------------------------------------------------------------------- 1 | import { xdr } from '@stellar/stellar-base'; 2 | import MessageType = xdr.MessageType; 3 | 4 | export function isFloodMessage(messageType: MessageType): boolean { 5 | return [ 6 | MessageType.scpMessage(), 7 | MessageType.floodAdvert(), 8 | MessageType.transaction(), 9 | MessageType.floodDemand() 10 | ].includes(messageType); 11 | } 12 | -------------------------------------------------------------------------------- /src/connection/xdr-buffer-converter.ts: -------------------------------------------------------------------------------- 1 | import { xdr } from '@stellar/stellar-base'; 2 | import AuthenticatedMessage = xdr.AuthenticatedMessage; 3 | import { err, ok, Result } from 'neverthrow'; 4 | import StellarMessage = xdr.StellarMessage; 5 | 6 | export default { 7 | getMessageLengthFromXDRBuffer: function (buffer: Buffer): number { 8 | if (buffer.length < 4) return 0; 9 | 10 | buffer[0] = buffer[0] &= 0x7f; //clear xdr continuation bit 11 | return buffer.readUInt32BE(0); 12 | }, 13 | 14 | xdrBufferContainsCompleteMessage: function ( 15 | buffer: Buffer, 16 | messageLength: number 17 | ): boolean { 18 | return buffer.length - 4 >= messageLength; 19 | }, 20 | 21 | //returns next message and remaining buffer 22 | getMessageFromXdrBuffer: function ( 23 | buffer: Buffer, 24 | messageLength: number 25 | ): [Buffer, Buffer] { 26 | return [ 27 | buffer.slice(4, messageLength + 4), 28 | buffer.slice(4 + messageLength) 29 | ]; 30 | }, 31 | 32 | getXdrBufferFromMessage: function ( 33 | message: AuthenticatedMessage | StellarMessage 34 | ): Result { 35 | try { 36 | const lengthBuffer = Buffer.alloc(4); 37 | const xdrMessage = message.toXDR(); 38 | lengthBuffer.writeUInt32BE(xdrMessage.length, 0); 39 | 40 | return ok(Buffer.concat([lengthBuffer, xdrMessage])); 41 | } catch (error) { 42 | let msg: xdr.StellarMessage; 43 | if (message instanceof AuthenticatedMessage) 44 | msg = message.value().message(); 45 | else msg = message; 46 | 47 | let errorMsg = 'ToXDR of ' + msg.switch().name + ' failed'; 48 | if (error instanceof Error) errorMsg += ': ' + error.message; 49 | 50 | return err(new Error(errorMsg)); 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/connection/xdr-message-handler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Fast way to determine message type without parsing the whole xdr through the StellarBase xdr class 3 | */ 4 | import { ok, err, Result } from 'neverthrow'; 5 | 6 | export interface AuthenticatedMessageV0 { 7 | sequenceNumberXDR: Buffer; 8 | messageTypeXDR: Buffer; 9 | stellarMessageXDR: Buffer; 10 | macXDR: Buffer; 11 | } 12 | 13 | export function parseAuthenticatedMessageXDR( 14 | messageXDR: Buffer 15 | ): Result { 16 | const messageVersionXDR = messageXDR.slice(0, 4); 17 | if (messageVersionXDR.readInt32BE(0) !== 0) { 18 | //we only support v0 19 | return err(new Error('Unsupported message version')); 20 | } 21 | const sequenceNumberXDR = messageXDR.slice(4, 12); 22 | const messageTypeXDR = messageXDR.slice(12, 16); 23 | const stellarMessageXDR = messageXDR.slice(16, messageXDR.length - 32); 24 | //mac has length 32 bytes and is only remaining structure in xdr after stellar message 25 | //https://github.com/stellar/stellar-core/blob/7cf753cb37530d1ed372a7091fadd233d2f1604a/src/xdr/Stellar-overlay.x#L226 26 | //another approach would be to get the length by messageType 27 | const macXDR = messageXDR.slice(messageXDR.length - 32); 28 | 29 | return ok({ 30 | sequenceNumberXDR: sequenceNumberXDR, 31 | messageTypeXDR: messageTypeXDR, 32 | stellarMessageXDR: stellarMessageXDR, 33 | macXDR: macXDR 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/crypto-helper.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | import { 3 | crypto_sign_BYTES, 4 | crypto_sign_detached, 5 | crypto_sign_verify_detached 6 | } from 'sodium-native'; 7 | 8 | export function createSHA256Hmac(data: Buffer, macKey: Buffer): Buffer { 9 | return crypto.createHmac('SHA256', macKey).update(data).digest(); 10 | } 11 | 12 | export function verifyHmac(mac: Buffer, macKey: Buffer, data: Buffer): boolean { 13 | const calculatedMac = crypto 14 | .createHmac('SHA256', macKey) 15 | .update(data) 16 | .digest(); 17 | 18 | return crypto.timingSafeEqual(calculatedMac, mac); 19 | } 20 | 21 | export function verifySignature( 22 | publicKey: Buffer, 23 | signature: Buffer, 24 | message: Buffer 25 | ): boolean { 26 | return crypto_sign_verify_detached(signature, message, publicKey); 27 | } 28 | 29 | export function createSignature(secretKey: Buffer, message: Buffer): Buffer { 30 | const signature = Buffer.alloc(crypto_sign_BYTES); 31 | crypto_sign_detached(signature, message, secretKey); 32 | 33 | return signature; 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { NodeConfig } from './node-config'; 2 | import { Node } from './node'; 3 | import { FastSigning, hash, Keypair } from '@stellar/stellar-base'; 4 | import { ConnectionAuthentication } from './connection/connection-authentication'; 5 | import { pino } from 'pino'; 6 | 7 | export { Node } from './node'; 8 | export { Connection } from './connection/connection'; 9 | export { UniqueSCPStatementTransform } from './unique-scp-statement-transform'; 10 | export { 11 | StellarMessageRouter, 12 | MessageTypeName 13 | } from './stellar-message-router'; 14 | export { 15 | ScpBallot, 16 | SCPStatement, 17 | SCPStatementType, 18 | ScpStatementPledges, 19 | ScpStatementPrepare, 20 | ScpStatementConfirm, 21 | ScpStatementExternalize, 22 | ScpNomination 23 | } from './scp-statement-dto'; 24 | export { getConfigFromEnv } from './node-config'; 25 | export { ScpReader } from './scp-reader'; 26 | export { 27 | getPublicKeyStringFromBuffer, 28 | createSCPEnvelopeSignature, 29 | createStatementXDRSignature, 30 | getIpFromPeerAddress, 31 | verifySCPEnvelopeSignature, 32 | getQuorumSetFromMessage, 33 | QuorumSetDTO 34 | } from './stellar-message-service'; //todo: separate package? 35 | 36 | export function createNode(config: NodeConfig, logger?: pino.Logger): Node { 37 | if (!logger) { 38 | logger = pino({ 39 | level: process.env.LOG_LEVEL || 'info', 40 | base: undefined 41 | }); 42 | } 43 | 44 | logger = logger.child({ app: 'Connector' }); 45 | if (!FastSigning) { 46 | logger.debug('warning', 'FastSigning not enabled'); 47 | } 48 | 49 | let keyPair: Keypair; 50 | if (config.privateKey) { 51 | try { 52 | keyPair = Keypair.fromSecret(config.privateKey); 53 | } catch (error) { 54 | throw new Error('Invalid private key'); 55 | } 56 | } else { 57 | keyPair = Keypair.random(); 58 | } 59 | 60 | const networkId = hash(Buffer.from(config.network)); 61 | 62 | const connectionAuthentication = new ConnectionAuthentication( 63 | keyPair, 64 | networkId 65 | ); 66 | 67 | return new Node(config, keyPair, connectionAuthentication, logger); 68 | } 69 | -------------------------------------------------------------------------------- /src/map-unknown-to-error.ts: -------------------------------------------------------------------------------- 1 | export function mapUnknownToError(e: unknown): Error { 2 | if (e instanceof Error) { 3 | return e; 4 | } 5 | if (isString(e)) { 6 | return new Error(e); 7 | } 8 | 9 | return new Error('Unspecified error: ' + e); 10 | } 11 | 12 | function isString(param: unknown): param is string { 13 | return typeof param === 'string'; 14 | } 15 | -------------------------------------------------------------------------------- /src/node-config.ts: -------------------------------------------------------------------------------- 1 | import { Networks } from '@stellar/stellar-base'; 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | require('dotenv').config(); 4 | import * as yn from 'yn'; 5 | import { NodeInfo } from './node'; 6 | 7 | export type NodeConfig = { 8 | network: string; 9 | nodeInfo: NodeInfo; 10 | listeningPort: number; 11 | privateKey?: string; 12 | receiveTransactionMessages: boolean; 13 | receiveSCPMessages: boolean; 14 | peerFloodReadingCapacity: number; 15 | flowControlSendMoreBatchSize: number; 16 | peerFloodReadingCapacityBytes: number; 17 | flowControlSendMoreBatchSizeBytes: number; 18 | }; 19 | 20 | export function getConfigFromEnv(): NodeConfig { 21 | const ledgerVersion = getNumberFromEnv('LEDGER_VERSION', 17); 22 | const overlayVersion = getNumberFromEnv('OVERLAY_VERSION', 17); 23 | const overlayMinVersion = getNumberFromEnv('OVERLAY_MIN_VERSION', 16); 24 | const versionString = process.env['VERSION_STRING'] 25 | ? process.env['VERSION_STRING'] 26 | : 'sb'; 27 | const listeningPort = getNumberFromEnv('LISTENING_PORT', 11625); 28 | const privateKey = process.env['PRIVATE_KEY'] 29 | ? process.env['PRIVATE_KEY'] 30 | : undefined; 31 | const receiveTransactionMessages = yn(process.env['RECEIVE_TRANSACTION_MSG']); 32 | const receiveSCPMessages = yn(process.env['RECEIVE_SCP_MSG']); 33 | const networkString = process.env['NETWORK'] 34 | ? process.env['NETWORK'] 35 | : Networks.PUBLIC; 36 | 37 | const peerFloodReadingCapacity = getNumberFromEnv( 38 | 'PEER_FLOOD_READING_CAPACITY', 39 | 200 40 | ); 41 | const flowControlSendMoreBatchSize = getNumberFromEnv( 42 | 'FLOW_CONTROL_SEND_MORE_BATCH_SIZE', 43 | 40 44 | ); 45 | const peerFloodReadingCapacityBytes = getNumberFromEnv( 46 | 'PEER_FLOOD_READING_CAPACITY_BYTES', 47 | 300000 48 | ); 49 | const flowControlSendMoreBatchSizeBytes = getNumberFromEnv( 50 | 'FLOW_CONTROL_SEND_MORE_BATCH_SIZE_BYTES', 51 | 100000 52 | ); 53 | 54 | return { 55 | network: networkString, 56 | nodeInfo: { 57 | ledgerVersion: ledgerVersion, 58 | overlayMinVersion: overlayMinVersion, 59 | overlayVersion: overlayVersion, 60 | versionString: versionString 61 | }, 62 | listeningPort: listeningPort, 63 | privateKey: privateKey, 64 | receiveSCPMessages: 65 | receiveSCPMessages !== undefined ? receiveSCPMessages : true, 66 | receiveTransactionMessages: 67 | receiveTransactionMessages !== undefined 68 | ? receiveTransactionMessages 69 | : true, 70 | peerFloodReadingCapacity: peerFloodReadingCapacity, 71 | flowControlSendMoreBatchSize: flowControlSendMoreBatchSize, 72 | peerFloodReadingCapacityBytes: peerFloodReadingCapacityBytes, 73 | flowControlSendMoreBatchSizeBytes: flowControlSendMoreBatchSizeBytes 74 | }; 75 | } 76 | 77 | function getNumberFromEnv(key: string, defaultValue: number) { 78 | let value = defaultValue; 79 | const stringy = process.env[key]; 80 | if (stringy && !isNaN(parseInt(stringy))) { 81 | value = parseInt(stringy); 82 | } 83 | return value; 84 | } 85 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@stellar/stellar-base'; 2 | 3 | import * as net from 'net'; 4 | import { Connection } from './connection/connection'; 5 | 6 | import { ConnectionAuthentication } from './connection/connection-authentication'; 7 | import { NodeConfig } from './node-config'; 8 | import { EventEmitter } from 'events'; 9 | import { Server, Socket } from 'net'; 10 | import { pino } from 'pino'; 11 | import { FlowController } from './connection/flow-controller'; 12 | 13 | export type NodeInfo = { 14 | ledgerVersion: number; 15 | overlayVersion: number; 16 | overlayMinVersion: number; 17 | versionString: string; 18 | networkId?: string; 19 | }; 20 | 21 | /** 22 | * Supports two operations: connect to a node, and accept connections from other nodes. 23 | * In both cases it returns Connection instances that produce and consume StellarMessages 24 | */ 25 | export class Node extends EventEmitter { 26 | protected server?: Server; 27 | 28 | constructor( 29 | private config: NodeConfig, 30 | public keyPair: Keypair, 31 | private readonly connectionAuthentication: ConnectionAuthentication, 32 | private readonly logger: pino.Logger 33 | ) { 34 | super(); 35 | this.logger.info('Using public key: ' + this.keyPair.publicKey()); 36 | } 37 | 38 | /* 39 | * Connect to a node 40 | */ 41 | connectTo(ip: string, port: number): Connection { 42 | const socket = new net.Socket(); 43 | const flowController = new FlowController( 44 | this.config.peerFloodReadingCapacity, 45 | this.config.flowControlSendMoreBatchSize, 46 | this.config.peerFloodReadingCapacityBytes, 47 | this.config.flowControlSendMoreBatchSizeBytes 48 | ); 49 | 50 | const connection = new Connection( 51 | { 52 | ip: ip, 53 | port: port, 54 | keyPair: this.keyPair, 55 | localNodeInfo: { 56 | ledgerVersion: this.config.nodeInfo.ledgerVersion, 57 | overlayVersion: this.config.nodeInfo.overlayVersion, 58 | overlayMinVersion: this.config.nodeInfo.overlayMinVersion, 59 | versionString: this.config.nodeInfo.versionString 60 | }, 61 | listeningPort: this.config.listeningPort, 62 | remoteCalledUs: false, 63 | receiveTransactionMessages: this.config.receiveTransactionMessages, 64 | receiveSCPMessages: this.config.receiveSCPMessages 65 | }, 66 | socket, 67 | this.connectionAuthentication, 68 | flowController, 69 | this.logger 70 | ); 71 | 72 | this.logger.debug({ remote: connection.remoteAddress }, 'Connect'); 73 | connection.connect(); 74 | 75 | return connection; 76 | } 77 | 78 | /* 79 | * Start accepting connections from other nodes. 80 | * emits connection event with a Connection instance on a new incoming connection 81 | */ 82 | acceptIncomingConnections(port?: number, host?: string): void { 83 | if (!this.server) { 84 | this.server = new Server(); 85 | this.server.on('connection', (socket) => 86 | this.onIncomingConnection(socket) 87 | ); 88 | this.server.on('error', (err) => this.emit('error', err)); 89 | this.server.on('close', () => this.emit('close')); 90 | this.server.on('listening', () => this.emit('listening')); 91 | } 92 | 93 | if (!this.server.listening) this.server.listen(port, host); 94 | } 95 | 96 | stopAcceptingIncomingConnections(callback?: (err?: Error) => void): void { 97 | if (this.server) this.server.close(callback); 98 | else if (callback) callback(); 99 | } 100 | 101 | public get listening(): boolean { 102 | if (this.server) return this.server.listening; 103 | else return false; 104 | } 105 | 106 | protected onIncomingConnection(socket: Socket): void { 107 | if (socket.remoteAddress === undefined || socket.remotePort === undefined) 108 | return; //this can happen when socket is immediately destroyed 109 | 110 | const flowController = new FlowController( 111 | this.config.peerFloodReadingCapacity, 112 | this.config.flowControlSendMoreBatchSize, 113 | this.config.peerFloodReadingCapacityBytes, 114 | this.config.flowControlSendMoreBatchSizeBytes 115 | ); 116 | const connection = new Connection( 117 | { 118 | ip: socket.remoteAddress, 119 | port: socket.remotePort, 120 | keyPair: this.keyPair, 121 | localNodeInfo: { 122 | ledgerVersion: this.config.nodeInfo.ledgerVersion, 123 | overlayVersion: this.config.nodeInfo.overlayVersion, 124 | overlayMinVersion: this.config.nodeInfo.overlayMinVersion, 125 | versionString: this.config.nodeInfo.versionString 126 | }, 127 | listeningPort: this.config.listeningPort, 128 | remoteCalledUs: true, 129 | receiveTransactionMessages: this.config.receiveTransactionMessages, 130 | receiveSCPMessages: this.config.receiveSCPMessages 131 | }, 132 | socket, 133 | this.connectionAuthentication, 134 | flowController, 135 | this.logger 136 | ); 137 | this.emit('connection', connection); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/scp-reader.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'pino'; 2 | import { Node } from './node'; 3 | import { StrKey, xdr } from '@stellar/stellar-base'; 4 | 5 | type PublicKey = string; 6 | type Ledger = string; 7 | type Value = string; 8 | 9 | export class ScpReader { 10 | private nominateVotes: Map> = new Map(); 11 | private nominateAccepted: Map> = new Map(); 12 | 13 | constructor(private logger: Logger) {} 14 | 15 | private isNewNominateVote( 16 | ledger: Ledger, 17 | publicKey: PublicKey, 18 | value: Value[] 19 | ): boolean { 20 | if (value.length === 0) return false; 21 | const ledgerVotes = this.nominateVotes.get(ledger); 22 | if (!ledgerVotes) return true; 23 | 24 | const votesByNode = ledgerVotes.get(publicKey); 25 | if (!votesByNode) return true; 26 | 27 | return votesByNode.length !== value.length; 28 | } 29 | 30 | private registerNominateVotes( 31 | ledger: Ledger, 32 | publicKey: PublicKey, 33 | value: Value[] 34 | ) { 35 | let ledgerVotes = this.nominateVotes.get(ledger); 36 | if (!ledgerVotes) { 37 | ledgerVotes = new Map(); 38 | this.nominateVotes.set(ledger, ledgerVotes); 39 | } 40 | 41 | const votesByNode = ledgerVotes.get(publicKey); 42 | if (!votesByNode) { 43 | ledgerVotes.set(publicKey, value); 44 | } 45 | } 46 | 47 | private isNewNominateAccepted( 48 | ledger: Ledger, 49 | publicKey: PublicKey, 50 | value: Value[] 51 | ): boolean { 52 | if (value.length === 0) return false; 53 | 54 | const ledgerAccepted = this.nominateAccepted.get(ledger); 55 | if (!ledgerAccepted) return true; 56 | 57 | const acceptedByNode = ledgerAccepted.get(publicKey); 58 | if (!acceptedByNode) return true; 59 | 60 | return acceptedByNode.length !== value.length; 61 | } 62 | 63 | private registerNominateAccepted( 64 | ledger: Ledger, 65 | publicKey: PublicKey, 66 | value: Value[] 67 | ) { 68 | let ledgerAccepted = this.nominateAccepted.get(ledger); 69 | if (!ledgerAccepted) { 70 | ledgerAccepted = new Map(); 71 | this.nominateAccepted.set(ledger, ledgerAccepted); 72 | } 73 | 74 | const acceptedByNode = ledgerAccepted.get(publicKey); 75 | if (!acceptedByNode) { 76 | ledgerAccepted.set(publicKey, value); 77 | } 78 | } 79 | 80 | read( 81 | node: Node, 82 | ip: string, 83 | port: number, 84 | nodeNames: Map 85 | ): void { 86 | this.logger.info('Connecting to ' + ip + ':' + port); 87 | 88 | const connection = node.connectTo(ip, port); 89 | connection 90 | .on('connect', (publicKey, nodeInfo) => { 91 | console.log('Connected to Stellar Node: ' + publicKey); 92 | console.log(nodeInfo); 93 | }) 94 | .on('data', (stellarMessageJob) => { 95 | const stellarMessage = stellarMessageJob.stellarMessage; 96 | //console.log(stellarMessage.toXDR('base64')) 97 | 98 | switch (stellarMessage.switch()) { 99 | case xdr.MessageType.scpMessage(): 100 | this.translateSCPMessage(stellarMessage, nodeNames); 101 | break; 102 | default: 103 | console.log( 104 | 'rcv StellarMessage of type ' + stellarMessage.switch().name //+ 105 | //': ' + 106 | // stellarMessage.toXDR('base64') 107 | ); 108 | break; 109 | } 110 | stellarMessageJob.done(); 111 | }) 112 | .on('error', (err) => { 113 | console.log(err); 114 | }) 115 | .on('close', () => { 116 | console.log('closed connection'); 117 | }) 118 | .on('timeout', () => { 119 | console.log('timeout'); 120 | connection.destroy(); 121 | }); 122 | } 123 | 124 | private translateSCPMessage( 125 | stellarMessage: xdr.StellarMessage, 126 | nodeNames: Map 127 | ) { 128 | const publicKey = StrKey.encodeEd25519PublicKey( 129 | stellarMessage.envelope().statement().nodeId().value() 130 | ).toString(); 131 | const name = nodeNames.get(publicKey); 132 | const ledger = stellarMessage.envelope().statement().slotIndex().toString(); 133 | 134 | if ( 135 | stellarMessage.envelope().statement().pledges().switch() === 136 | xdr.ScpStatementType.scpStNominate() 137 | ) { 138 | this.translateNominate(stellarMessage, ledger, publicKey, name); 139 | } else if ( 140 | stellarMessage.envelope().statement().pledges().switch() === 141 | xdr.ScpStatementType.scpStPrepare() 142 | ) { 143 | this.translatePrepare(stellarMessage, ledger, name); 144 | } else if ( 145 | stellarMessage.envelope().statement().pledges().switch() === 146 | xdr.ScpStatementType.scpStConfirm() 147 | ) { 148 | this.translateCommit(stellarMessage, ledger, name); 149 | } else if ( 150 | stellarMessage.envelope().statement().pledges().switch() === 151 | xdr.ScpStatementType.scpStExternalize() 152 | ) { 153 | this.translateExternalize(stellarMessage, ledger, name); 154 | } 155 | } 156 | 157 | private translateCommit( 158 | stellarMessage: xdr.StellarMessage, 159 | ledger: string, 160 | name: string | undefined 161 | ) { 162 | const ballotValue = this.trimString( 163 | stellarMessage 164 | .envelope() 165 | .statement() 166 | .pledges() 167 | .confirm() 168 | .ballot() 169 | .value() 170 | .toString('hex') 171 | ); 172 | const cCounter = stellarMessage 173 | .envelope() 174 | .statement() 175 | .pledges() 176 | .confirm() 177 | .nCommit(); 178 | const hCounter = stellarMessage 179 | .envelope() 180 | .statement() 181 | .pledges() 182 | .confirm() 183 | .nH(); 184 | const preparedCounter = stellarMessage 185 | .envelope() 186 | .statement() 187 | .pledges() 188 | .confirm() 189 | .nPrepared(); 190 | console.log( 191 | ledger + 192 | ': ' + 193 | name + 194 | ':ACCEPT(COMMIT<' + 195 | cCounter + 196 | ' - ' + 197 | hCounter + 198 | ',' + 199 | ballotValue + 200 | '>)' 201 | ); 202 | console.log( 203 | ledger + ': ' + name + ':VOTE|ACCEPT(PREPARE)' 204 | ); 205 | console.log( 206 | ledger + 207 | ': ' + 208 | name + 209 | ':ACCEPT(PREPARE<' + 210 | preparedCounter + 211 | ',' + 212 | ballotValue + 213 | '>)' 214 | ); 215 | console.log( 216 | ledger + 217 | ': ' + 218 | name + 219 | ':CONFIRM(PREPARE<' + 220 | hCounter + 221 | ',' + 222 | ballotValue + 223 | '>)' 224 | ); 225 | console.log( 226 | ledger + 227 | ': ' + 228 | name + 229 | ':VOTE(COMMIT<' + 230 | cCounter + 231 | ' - Inf,' + 232 | ballotValue + 233 | '>)' 234 | ); 235 | } 236 | 237 | private translateExternalize( 238 | stellarMessage: xdr.StellarMessage, 239 | ledger: string, 240 | name: string | undefined 241 | ) { 242 | const ballotValue = this.trimString( 243 | stellarMessage 244 | .envelope() 245 | .statement() 246 | .pledges() 247 | .externalize() 248 | .commit() 249 | .value() 250 | .toString('hex') 251 | ); 252 | const ballotCounter = stellarMessage 253 | .envelope() 254 | .statement() 255 | .pledges() 256 | .externalize() 257 | .commit() 258 | .counter(); 259 | console.log( 260 | ledger + 261 | ': ' + 262 | name + 263 | ':ACCEPT(COMMIT<' + 264 | ballotCounter + 265 | ' - Inf,' + 266 | ballotValue + 267 | '>)' 268 | ); 269 | 270 | const hCounter = stellarMessage 271 | .envelope() 272 | .statement() 273 | .pledges() 274 | .externalize() 275 | .nH(); 276 | console.log( 277 | ledger + 278 | ': ' + 279 | name + 280 | ':CONFIRM(COMMIT<' + 281 | ballotCounter + 282 | ' - ' + 283 | hCounter + 284 | ',' + 285 | ballotValue + 286 | '>)' 287 | ); 288 | 289 | console.log( 290 | ledger + ': ' + name + ':ACCEPT(PREPARE)' 291 | ); 292 | console.log( 293 | ledger + 294 | ': ' + 295 | name + 296 | ':CONFIRM(PREPARE<' + 297 | hCounter + 298 | ',' + 299 | ballotValue + 300 | '>)' 301 | ); 302 | } 303 | 304 | private translatePrepare( 305 | stellarMessage: xdr.StellarMessage, 306 | ledger: string, 307 | name: string | undefined 308 | ) { 309 | const ballotValue = this.trimString( 310 | stellarMessage 311 | .envelope() 312 | .statement() 313 | .pledges() 314 | .prepare() 315 | .ballot() 316 | .value() 317 | .toString('hex') 318 | ); 319 | const ballotCounter = stellarMessage 320 | .envelope() 321 | .statement() 322 | .pledges() 323 | .prepare() 324 | .ballot() 325 | .counter() 326 | .toString(); 327 | const prepared = stellarMessage 328 | .envelope() 329 | .statement() 330 | .pledges() 331 | .prepare() 332 | .prepared(); 333 | console.log( 334 | ledger + 335 | ': ' + 336 | name + 337 | ':VOTE(PREPARE<' + 338 | ballotCounter + 339 | ',' + 340 | ballotValue + 341 | '>)' 342 | ); 343 | 344 | if (prepared) { 345 | const preparedBallotValue = this.trimString( 346 | prepared.value().toString('hex') 347 | ); 348 | const preparedBallotCounter = prepared.counter().toString(); 349 | console.log( 350 | ledger + 351 | ': ' + 352 | name + 353 | ':ACCEPT(PREPARE<' + 354 | preparedBallotCounter + 355 | ',' + 356 | preparedBallotValue + 357 | '>)' 358 | ); 359 | 360 | //if prepared.value changes, ABORT is implied for all indices smaller than aCounter. aCounter is computed (see doc). 361 | } 362 | 363 | const hCounter = stellarMessage 364 | .envelope() 365 | .statement() 366 | .pledges() 367 | .prepare() 368 | .nH(); 369 | if (hCounter !== 0 && hCounter !== undefined) { 370 | console.log( 371 | ledger + 372 | ': ' + 373 | name + 374 | ':CONFIRM(PREPARE<' + 375 | hCounter + 376 | ',' + 377 | ballotValue + 378 | '>)' 379 | ); 380 | } 381 | 382 | const cCounter = stellarMessage 383 | .envelope() 384 | .statement() 385 | .pledges() 386 | .prepare() 387 | .nC(); 388 | if (cCounter !== 0 && cCounter !== undefined) { 389 | console.log( 390 | ledger + 391 | ': ' + 392 | name + 393 | ':VOTE(COMMIT<' + 394 | cCounter + 395 | ' - ' + 396 | hCounter + 397 | ',' + 398 | ballotValue + 399 | '>)' 400 | ); 401 | } 402 | } 403 | 404 | private translateNominate( 405 | stellarMessage: xdr.StellarMessage, 406 | ledger: string, 407 | publicKey: string, 408 | name: string | undefined 409 | ) { 410 | const nominateVotes = stellarMessage 411 | .envelope() 412 | .statement() 413 | .pledges() 414 | .nominate() 415 | .votes() 416 | .map((vote: Buffer) => { 417 | return this.trimString(vote.toString('hex')); 418 | }); 419 | if (this.isNewNominateVote(ledger, publicKey, nominateVotes)) { 420 | console.log( 421 | ledger + ': ' + name + ':VOTE(NOMINATE([' + nominateVotes + ']))' 422 | ); 423 | this.registerNominateVotes(ledger, publicKey, nominateVotes); 424 | } 425 | 426 | const nominateAccepted = stellarMessage 427 | .envelope() 428 | .statement() 429 | .pledges() 430 | .nominate() 431 | .accepted() 432 | .map((accepted: Buffer) => { 433 | return this.trimString(accepted.toString('hex')); 434 | }); 435 | if (this.isNewNominateAccepted(ledger, publicKey, nominateAccepted)) { 436 | console.log( 437 | ledger + ': ' + name + ':ACCEPT(NOMINATE([' + nominateAccepted + ']))' 438 | ); 439 | this.registerNominateAccepted(ledger, publicKey, nominateAccepted); 440 | } 441 | } 442 | 443 | private trimString(str: string, lengthToShow = 5) { 444 | if (str.length <= lengthToShow * 2) { 445 | return str; 446 | } 447 | 448 | const start = str.substring(0, lengthToShow); 449 | const end = str.substring(str.length - lengthToShow); 450 | 451 | return `${start}...${end}`; 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/scp-statement-dto.ts: -------------------------------------------------------------------------------- 1 | import { StrKey, xdr } from '@stellar/stellar-base'; 2 | import { err, ok, Result } from 'neverthrow'; 3 | 4 | export type ScpStatementPledges = 5 | | ScpStatementPrepare 6 | | ScpStatementConfirm 7 | | ScpStatementExternalize 8 | | ScpNomination; 9 | 10 | export interface ScpStatementConfirm { 11 | ballot: ScpBallot; 12 | nPrepared: number; 13 | nCommit: number; 14 | nH: number; 15 | quorumSetHash: string; 16 | } 17 | 18 | export interface ScpStatementPrepare { 19 | quorumSetHash: string; 20 | ballot: ScpBallot; 21 | prepared: null | ScpBallot; 22 | preparedPrime: null | ScpBallot; 23 | nC: number; 24 | nH: number; 25 | } 26 | 27 | export interface ScpBallot { 28 | counter: number; 29 | value: string; //base64 30 | } 31 | 32 | export interface ScpStatementExternalize { 33 | quorumSetHash: string; 34 | nH: number; 35 | commit: ScpBallot; 36 | } 37 | 38 | export interface ScpNomination { 39 | quorumSetHash: string; 40 | votes: string[]; 41 | accepted: string[]; 42 | } 43 | 44 | export type SCPStatementType = 45 | | 'externalize' 46 | | 'nominate' 47 | | 'confirm' 48 | | 'prepare'; 49 | 50 | export class SCPStatement { 51 | nodeId: string; 52 | slotIndex: string; 53 | type: SCPStatementType; 54 | pledges: ScpStatementPledges; 55 | 56 | constructor( 57 | nodeId: string, 58 | slotIndex: string, 59 | type: SCPStatementType, 60 | pledges: ScpStatementPledges 61 | ) { 62 | this.nodeId = nodeId; 63 | this.slotIndex = slotIndex; 64 | this.type = type; 65 | this.pledges = pledges; 66 | } 67 | 68 | static fromXdr( 69 | xdrInput: string | xdr.ScpStatement 70 | ): Result { 71 | if (typeof xdrInput === 'string') { 72 | const buffer = Buffer.from(xdrInput, 'base64'); 73 | xdrInput = xdr.ScpStatement.fromXDR(buffer) as xdr.ScpStatement; 74 | } 75 | 76 | const nodeId = StrKey.encodeEd25519PublicKey( 77 | xdrInput.nodeId().value() 78 | ).toString(); //slow! cache! 79 | const slotIndex = xdrInput.slotIndex().toString(); 80 | const xdrType = xdrInput.pledges().switch(); 81 | let pledges: ScpStatementPledges; 82 | let type: SCPStatementType; 83 | 84 | if (xdrType === xdr.ScpStatementType.scpStExternalize()) { 85 | type = 'externalize'; 86 | const statement = xdrInput 87 | .pledges() 88 | .value() as xdr.ScpStatementExternalize; 89 | pledges = { 90 | quorumSetHash: statement.commitQuorumSetHash().toString('base64'), 91 | nH: statement.nH(), 92 | commit: { 93 | counter: statement.commit().counter(), 94 | value: statement.commit().value().toString('base64') 95 | } 96 | }; 97 | } else if (xdrType === xdr.ScpStatementType.scpStConfirm()) { 98 | const statement = xdrInput.pledges().value() as xdr.ScpStatementConfirm; 99 | type = 'confirm'; 100 | pledges = { 101 | quorumSetHash: statement.quorumSetHash().toString('base64'), 102 | nH: statement.nH(), 103 | nPrepared: statement.nPrepared(), 104 | nCommit: statement.nCommit(), 105 | ballot: { 106 | counter: statement.ballot().counter(), 107 | value: statement.ballot().value().toString('base64') 108 | } 109 | }; 110 | } else if (xdrType === xdr.ScpStatementType.scpStNominate()) { 111 | const statement = xdrInput.pledges().value() as xdr.ScpNomination; 112 | type = 'nominate'; 113 | pledges = { 114 | quorumSetHash: statement.quorumSetHash().toString('base64'), 115 | votes: statement.votes().map((vote: Buffer) => vote.toString('base64')), 116 | accepted: statement 117 | .votes() 118 | .map((vote: Buffer) => vote.toString('base64')) 119 | }; 120 | } else if (xdrType === xdr.ScpStatementType.scpStPrepare()) { 121 | type = 'prepare'; 122 | const statement = xdrInput.pledges().value() as xdr.ScpStatementPrepare; 123 | const prepared = statement.prepared(); 124 | const preparedPrime = statement.preparedPrime(); 125 | pledges = { 126 | quorumSetHash: statement.quorumSetHash().toString('base64'), 127 | ballot: { 128 | counter: statement.ballot().counter(), 129 | value: statement.ballot().value().toString('base64') 130 | }, 131 | prepared: prepared 132 | ? { 133 | counter: prepared.counter(), 134 | value: prepared.value().toString('base64') 135 | } 136 | : null, 137 | preparedPrime: preparedPrime 138 | ? { 139 | counter: preparedPrime.counter(), 140 | value: preparedPrime.value().toString('base64') 141 | } 142 | : null, 143 | nC: statement.nH(), 144 | nH: statement.nC() 145 | }; 146 | } else { 147 | return err(new Error('unknown type: ' + xdrType)); 148 | } 149 | 150 | return ok(new SCPStatement(nodeId, slotIndex, type, pledges)); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/stellar-message-router.ts: -------------------------------------------------------------------------------- 1 | import { Transform, TransformCallback } from 'stream'; 2 | import { xdr } from '@stellar/stellar-base'; 3 | import StellarMessage = xdr.StellarMessage; 4 | 5 | export type MessageTypeName = string; 6 | 7 | export class StellarMessageRouter extends Transform { 8 | streams: Map; 9 | 10 | constructor(streams: Map) { 11 | super({ 12 | readableObjectMode: true, 13 | writableObjectMode: true 14 | }); 15 | this.streams = streams; 16 | } 17 | 18 | _transform( 19 | stellarMessage: StellarMessage, 20 | encoding: string, 21 | next: TransformCallback 22 | ): void { 23 | const stream = this.streams.get(stellarMessage.switch().name); 24 | if (stream) { 25 | stream.write(stellarMessage); //use write, not push because we add to the writable side of the duplex stream. Push is for adding to the readable side. 26 | } 27 | 28 | return next(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/stellar-message-service.ts: -------------------------------------------------------------------------------- 1 | import { StrKey, xdr } from '@stellar/stellar-base'; 2 | import { ok, err, Result } from 'neverthrow'; 3 | import { createSignature, verifySignature } from './crypto-helper'; 4 | import ScpEnvelope = xdr.ScpEnvelope; 5 | import ScpStatement = xdr.ScpStatement; 6 | 7 | export interface QuorumSetDTO { 8 | validators: string[]; 9 | innerQuorumSets: QuorumSetDTO[]; 10 | threshold: number; 11 | } 12 | 13 | export function verifyStatementXDRSignature( 14 | statementXDR: Buffer, 15 | peerId: Buffer, 16 | signature: Buffer, 17 | network: Buffer 18 | ): Result { 19 | try { 20 | const body = Buffer.concat([ 21 | network, 22 | Buffer.from([0, 0, 0, 1]), 23 | statementXDR 24 | ]); 25 | return ok(verifySignature(peerId, signature, body)); 26 | } catch (error) { 27 | if (error instanceof Error) return err(error); 28 | else return err(new Error('Error verifying statement xdr signature')); 29 | } 30 | } 31 | 32 | export function createStatementXDRSignature( 33 | scpStatementXDR: Buffer, 34 | publicKey: Buffer, 35 | secretKey: Buffer, 36 | network: Buffer 37 | ): Result { 38 | try { 39 | const body = Buffer.concat([ 40 | network, 41 | Buffer.from([0, 0, 0, 1]), 42 | scpStatementXDR 43 | ]); 44 | const secret = Buffer.concat([secretKey, publicKey]); 45 | return ok(createSignature(secret, body)); 46 | } catch (error) { 47 | if (error instanceof Error) return err(error); 48 | else return err(new Error('Error creating statement xdr signature')); 49 | } 50 | } 51 | 52 | export function getPublicKeyStringFromBuffer( 53 | buffer: Buffer 54 | ): Result { 55 | try { 56 | return ok(StrKey.encodeEd25519PublicKey(buffer).toString()); 57 | } catch (error) { 58 | if (error instanceof Error) return err(error); 59 | else return err(new Error('error parsing public key string from buffer')); 60 | } 61 | } 62 | 63 | export function createSCPEnvelopeSignature( 64 | scpStatement: ScpStatement, 65 | publicKey: Buffer, 66 | secretKey: Buffer, 67 | network: Buffer 68 | ): Result { 69 | try { 70 | return createStatementXDRSignature( 71 | scpStatement.toXDR(), 72 | publicKey, 73 | secretKey, 74 | network 75 | ); 76 | } catch (error) { 77 | if (error instanceof Error) return err(error); 78 | else return err(new Error('Error creating scp envelope signature')); 79 | } 80 | } 81 | 82 | export function verifySCPEnvelopeSignature( 83 | scpEnvelope: ScpEnvelope, 84 | network: Buffer 85 | ): Result { 86 | try { 87 | return verifyStatementXDRSignature( 88 | scpEnvelope.statement().toXDR(), 89 | scpEnvelope.statement().nodeId().value(), 90 | scpEnvelope.signature(), 91 | network 92 | ); 93 | } catch (error) { 94 | if (error instanceof Error) return err(error); 95 | else return err(new Error('Error verifying scp envelope signature')); 96 | } 97 | } 98 | 99 | export function getQuorumSetFromMessage( 100 | scpQuorumSetMessage: xdr.ScpQuorumSet 101 | ): Result { 102 | try { 103 | return ok(getQuorumSetFromMessageRecursive(scpQuorumSetMessage)); 104 | } catch (error) { 105 | if (error instanceof Error) return err(error); 106 | else return err(new Error('Error getting quorumSet from message')); 107 | } 108 | } 109 | 110 | function getQuorumSetFromMessageRecursive( 111 | scpQuorumSetMessage: xdr.ScpQuorumSet 112 | ): QuorumSetDTO { 113 | return { 114 | threshold: scpQuorumSetMessage.threshold(), 115 | validators: scpQuorumSetMessage 116 | .validators() 117 | .map((validator) => StrKey.encodeEd25519PublicKey(validator.value())), 118 | innerQuorumSets: scpQuorumSetMessage 119 | .innerSets() 120 | .map((innerSet) => getQuorumSetFromMessageRecursive(innerSet)) 121 | }; 122 | } 123 | 124 | export function getIpFromPeerAddress( 125 | peerAddress: xdr.PeerAddress 126 | ): Result { 127 | try { 128 | const peerAddressIp = peerAddress.ip().value(); 129 | return ok( 130 | peerAddressIp[0] + 131 | '.' + 132 | peerAddressIp[1] + 133 | '.' + 134 | peerAddressIp[2] + 135 | '.' + 136 | peerAddressIp[3] 137 | ); 138 | } catch (error) { 139 | if (error instanceof Error) return err(error); 140 | else return err(new Error('Error getting ip from peer address')); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/unique-scp-statement-transform.ts: -------------------------------------------------------------------------------- 1 | import { Transform, TransformCallback } from 'stream'; 2 | import { LRUCache } from 'lru-cache'; 3 | import { hash, Networks, xdr } from '@stellar/stellar-base'; 4 | import StellarMessage = xdr.StellarMessage; 5 | import MessageType = xdr.MessageType; 6 | import { verifySCPEnvelopeSignature } from './stellar-message-service'; 7 | 8 | export class UniqueSCPStatementTransform extends Transform { 9 | protected cache = new LRUCache({ max: 5000 }); 10 | 11 | constructor() { 12 | super({ 13 | objectMode: true, 14 | readableObjectMode: true, 15 | writableObjectMode: true 16 | }); 17 | } 18 | 19 | _transform( 20 | stellarMessage: StellarMessage, 21 | encoding: string, 22 | next: TransformCallback 23 | ): void { 24 | if (stellarMessage.switch() !== MessageType.scpMessage()) return next(); 25 | 26 | if (this.cache.has(stellarMessage.envelope().signature().toString())) { 27 | console.log('cache hit'); 28 | return next(); 29 | } 30 | 31 | this.cache.set(stellarMessage.envelope().signature().toString(), 1); 32 | 33 | //todo: if we use worker pool and 'async' next call, will the internal buffer fill up too fast and block reading? 34 | if ( 35 | verifySCPEnvelopeSignature( 36 | stellarMessage.envelope(), 37 | hash(Buffer.from(Networks.PUBLIC)) 38 | ) 39 | ) 40 | return next(null, stellarMessage.envelope().statement().toXDR('base64')); 41 | 42 | return next(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/worker/crypto-worker.ts: -------------------------------------------------------------------------------- 1 | import { worker } from 'workerpool'; 2 | import { Result } from 'neverthrow'; 3 | import { verifyStatementXDRSignature } from '../stellar-message-service'; 4 | 5 | function verifyStatementXDRSignatureWorker( 6 | statementXDR: Buffer, 7 | peerId: Buffer, 8 | signature: Buffer, 9 | network: Buffer 10 | ): boolean { 11 | return handleResult( 12 | verifyStatementXDRSignature(statementXDR, peerId, signature, network) 13 | ); 14 | } 15 | 16 | function handleResult(result: Result): Type { 17 | if (result.isErr()) throw result.error; 18 | else return result.value; 19 | } 20 | 21 | worker({ 22 | verifyStatementXDRSignature: verifyStatementXDRSignatureWorker 23 | }); 24 | -------------------------------------------------------------------------------- /test/connection-authentication.test.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionAuthentication } from '../src/connection/connection-authentication'; 2 | import { hash, Keypair, Networks, xdr } from '@stellar/stellar-base'; 3 | import xdrMessageCreator from '../src/connection/handshake-message-creator'; 4 | import BigNumber from 'bignumber.js'; 5 | import { createSHA256Hmac, verifyHmac } from '../src/crypto-helper'; 6 | 7 | it('should create a shared key', () => { 8 | const keyPair = Keypair.fromSecret( 9 | 'SCV6Q3VU4S52KVNJOFXWTHFUPHUKVYK3UV2ISGRLIUH54UGC6OPZVK2D' 10 | ); 11 | const connectionAuth = new ConnectionAuthentication( 12 | keyPair, 13 | hash(Buffer.from(Networks.PUBLIC)) 14 | ); 15 | expect( 16 | connectionAuth.getSharedKey( 17 | Buffer.from('SaINZpCTl6KO8xMLvDkE2vE3knQz0Ma1RmJySOFqsWk=', 'base64') 18 | ) 19 | ).toBeDefined(); 20 | }); 21 | 22 | it('should create a valid auth cert', () => { 23 | const keyPair = Keypair.fromSecret( 24 | 'SCV6Q3VU4S52KVNJOFXWTHFUPHUKVYK3UV2ISGRLIUH54UGC6OPZVK2D' 25 | ); 26 | const connectionAuth = new ConnectionAuthentication( 27 | keyPair, 28 | hash(Buffer.from(Networks.PUBLIC)) 29 | ); 30 | const authCertMessage = xdrMessageCreator.createAuthCert(connectionAuth); 31 | if (authCertMessage.isOk()) 32 | expect( 33 | connectionAuth.verifyRemoteAuthCert( 34 | new Date(), 35 | keyPair.rawPublicKey(), 36 | authCertMessage.value 37 | ) 38 | ).toBeTruthy(); 39 | }); 40 | 41 | it('should create a new Authcert if expiration/2 has passed', function () { 42 | const keyPair = Keypair.fromSecret( 43 | 'SCV6Q3VU4S52KVNJOFXWTHFUPHUKVYK3UV2ISGRLIUH54UGC6OPZVK2D' 44 | ); 45 | const connectionAuth = new ConnectionAuthentication( 46 | keyPair, 47 | hash(Buffer.from(Networks.PUBLIC)) 48 | ); 49 | 50 | const authCert = connectionAuth.getAuthCert(new Date()); 51 | const authCertInsideValidRange = connectionAuth.getAuthCert( 52 | new Date(new Date().getTime() + 1000) 53 | ); 54 | const newAuthCert = connectionAuth.getAuthCert( 55 | new Date( 56 | new Date().getTime() + 57 | ConnectionAuthentication.AUTH_EXPIRATION_LIMIT / 2 + 58 | 100 59 | ) 60 | ); 61 | 62 | expect(authCert).toEqual(authCertInsideValidRange); 63 | expect(newAuthCert).toBeDefined(); 64 | expect(newAuthCert === authCert).toBeFalsy(); 65 | }); 66 | 67 | test('mac', () => { 68 | const keyPair = Keypair.random(); 69 | const peerKeyPair = Keypair.random(); 70 | 71 | //@ts-ignore 72 | const connectionAuth = new ConnectionAuthentication( 73 | keyPair, 74 | hash(Buffer.from(Networks.PUBLIC)) 75 | ); 76 | //@ts-ignore 77 | const peerConnectionAuth = new ConnectionAuthentication( 78 | peerKeyPair, 79 | hash(Buffer.from(Networks.PUBLIC)) 80 | ); 81 | 82 | //@ts-ignore 83 | const ourNonce = hash(BigNumber.random().toString()); 84 | //@ts-ignore 85 | const peerNonce = hash(BigNumber.random().toString()); 86 | 87 | const receivingMacKey = connectionAuth.getReceivingMacKey( 88 | ourNonce, 89 | peerNonce, 90 | peerConnectionAuth.publicKeyECDH 91 | ); 92 | const peerSendingMacKey = peerConnectionAuth.getSendingMacKey( 93 | peerNonce, 94 | ourNonce, 95 | connectionAuth.publicKeyECDH, 96 | false 97 | ); 98 | 99 | const peerSequence = xdr.Uint64.fromString('10').toXDR(); 100 | 101 | const msg = Buffer.from( 102 | 'AAAAAAAAAAAAAAE3AAAACwAAAACslTOENMyaVlaiRvFAjiP6s8nFVIHDgWGbncnw+ziO5gAAAAACKbcUAAAAAzQaCq4p6tLHpdfwGhnlyX9dMUP70r4Dm98Td6YvKnhoAAAAAQAAAJijLxoAW1ZSaVphczIXU0XT7i46Jla6OZxkm9mEUfan3gAAAABg6Ee9AAAAAAAAAAEAAAAA+wsSteGzmcH88GN69FRjGLfxMzFH8tsJTaK+8ERePJMAAABAOiGtC3MiMa3LVn8f6SwUpKOmSMAJWQt2vewgt8T9WkRUPt2UdYac7vzcisXnmiusHldZcjVMF3vS03QhzaxdDQAAAAEAAACYoy8aAFtWUmlaYXMyF1NF0+4uOiZWujmcZJvZhFH2p94AAAAAYOhHvQAAAAAAAAABAAAAAPsLErXhs5nB/PBjevRUYxi38TMxR/LbCU2ivvBEXjyTAAAAQDohrQtzIjGty1Z/H+ksFKSjpkjACVkLdr3sILfE/VpEVD7dlHWGnO783IrF55orrB5XWXI1TBd70tN0Ic2sXQ0AAABA0ZiyH9AGgPR/d3h+94s6+iU5zhZbKM/5DIOYeKgxwEOotUveGfHLN5IQk7VlTW2arDkk+ekzjRQfBoexrkJrBMsQ30YpI1R/uY9npg0Fpt1ScyZ+yhABs6x1sEGminNh', 103 | 'base64' 104 | ); 105 | 106 | const macPeerUsesToSendUsTheMessage = createSHA256Hmac( 107 | Buffer.concat([peerSequence, msg]), 108 | peerSendingMacKey 109 | ); 110 | 111 | const result = verifyHmac( 112 | macPeerUsesToSendUsTheMessage, 113 | receivingMacKey, 114 | Buffer.concat([peerSequence, msg]) 115 | ); 116 | expect(result).toBeTruthy(); 117 | }); 118 | -------------------------------------------------------------------------------- /test/flow-controller.test.ts: -------------------------------------------------------------------------------- 1 | import { FlowController } from '../src/connection/flow-controller'; 2 | import { xdr } from '@stellar/stellar-base'; 3 | import MessageType = xdr.MessageType; 4 | 5 | describe('FlowController', function () { 6 | it('sendMore should return true if there is no peer flood reading capacity left in batch', function () { 7 | const flowController = new FlowController(2, 2); 8 | flowController.start(); 9 | 10 | expect(flowController.sendMore(MessageType.transaction(), 100)).toBeNull(); 11 | expect( 12 | flowController.sendMore(MessageType.scpMessage(), 100) 13 | ).toBeInstanceOf(xdr.StellarMessage); 14 | expect(flowController.sendMore(MessageType.transaction(), 100)).toBeNull(); 15 | expect( 16 | flowController.sendMore(MessageType.scpMessage(), 100) 17 | ).toBeInstanceOf(xdr.StellarMessage); 18 | }); 19 | 20 | it('sendMore should return true if there is no peer flood reading capacity in bytes left in batch', function () { 21 | const flowController = new FlowController(20, 20, 200, 200); 22 | flowController.start(); 23 | 24 | expect(flowController.sendMore(MessageType.transaction(), 150)).toBeNull(); 25 | expect( 26 | flowController.sendMore(MessageType.scpMessage(), 150) 27 | ).toBeInstanceOf(xdr.StellarMessage); 28 | expect(flowController.sendMore(MessageType.transaction(), 150)).toBeNull(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/integration/node.test.ts: -------------------------------------------------------------------------------- 1 | import { createNode, Node } from '../../src'; 2 | import { Connection } from '../../src'; 3 | import { xdr } from '@stellar/stellar-base'; 4 | import StellarMessage = xdr.StellarMessage; 5 | import { getConfigFromEnv } from '../../src'; 6 | import { StellarMessageWork } from '../../src/connection/connection'; 7 | import { createDummyExternalizeMessage } from '../../fixtures/stellar-message.fixtures'; 8 | 9 | let nodeA: Node; 10 | let nodeB: Node; //we don't want to connect the node to itself 11 | 12 | let connectionToNodeA: Connection; 13 | 14 | beforeAll(() => { 15 | const configA = getConfigFromEnv(); 16 | configA.peerFloodReadingCapacity = 2; 17 | configA.flowControlSendMoreBatchSize = 2; 18 | configA.nodeInfo.overlayVersion = 30; 19 | const configB = getConfigFromEnv(); 20 | configB.peerFloodReadingCapacity = 2; 21 | configB.flowControlSendMoreBatchSize = 2; 22 | configB.nodeInfo.overlayVersion = 30; 23 | nodeA = createNode(configA); //random public key 24 | nodeB = createNode(configB); //other random public key 25 | nodeA.acceptIncomingConnections(11623, '127.0.0.1'); 26 | connectionToNodeA = nodeB.connectTo('127.0.0.1', 11623); 27 | }); 28 | 29 | afterAll((done) => { 30 | connectionToNodeA.destroy(); 31 | nodeA.stopAcceptingIncomingConnections(done); 32 | }); 33 | 34 | test('connect', (done) => { 35 | let pingPongCounter = 0; 36 | let myConnectionToNodeB: Connection; 37 | nodeA.on('connection', (connectionToNodeB) => { 38 | connectionToNodeB.on('connect', () => { 39 | myConnectionToNodeB = connectionToNodeB; 40 | return; 41 | }); 42 | connectionToNodeB.on('data', () => { 43 | //pong 44 | connectionToNodeB.sendStellarMessage( 45 | createDummyExternalizeMessage(nodeA.keyPair) 46 | ); 47 | }); 48 | connectionToNodeB.on('error', (error: Error) => console.log(error)); 49 | }); 50 | 51 | connectionToNodeA 52 | .on('connect', () => { 53 | //ping 54 | connectionToNodeA.sendStellarMessage( 55 | StellarMessage.getScpQuorumset(Buffer.alloc(32)) 56 | ); 57 | }) 58 | .on('data', (data: StellarMessageWork) => { 59 | data.done(); 60 | pingPongCounter++; 61 | if ( 62 | pingPongCounter === 100 && 63 | myConnectionToNodeB.sendMoreMsgReceivedCounter === 50 64 | ) { 65 | done(); 66 | } else 67 | connectionToNodeA.sendStellarMessage( 68 | StellarMessage.getScpQuorumset(Buffer.alloc(32)) 69 | ); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/is-flood-message.test.ts: -------------------------------------------------------------------------------- 1 | import { xdr } from '@stellar/stellar-base'; 2 | import { isFloodMessage } from '../src/connection/is-flood-message'; 3 | import MessageType = xdr.MessageType; 4 | 5 | test('isFloodMessage', () => { 6 | expect(isFloodMessage(MessageType.getScpQuorumset())).toBeFalsy(); 7 | expect(isFloodMessage(MessageType.scpMessage())).toBeTruthy(); 8 | expect(isFloodMessage(MessageType.transaction())).toBeTruthy(); 9 | }); 10 | -------------------------------------------------------------------------------- /test/stellar-message-service.test.ts: -------------------------------------------------------------------------------- 1 | import { getQuorumSetFromMessage, verifySCPEnvelopeSignature } from '../src'; 2 | import { hash, Keypair, Networks, xdr } from '@stellar/stellar-base'; 3 | import { createDummyExternalizeMessage } from '../fixtures/stellar-message.fixtures'; 4 | 5 | it('should create and verify envelope signatures correctly', () => { 6 | const scpMessage = createDummyExternalizeMessage(Keypair.random()); 7 | 8 | const result = verifySCPEnvelopeSignature( 9 | scpMessage.envelope(), 10 | hash(Buffer.from(Networks.PUBLIC)) 11 | ); 12 | 13 | expect(result.isOk()).toBeTruthy(); 14 | 15 | if (result.isOk()) expect(result.value).toBeTruthy(); 16 | }); 17 | it('should transform quorumset correctly', function () { 18 | const quorumSetBuffer = Buffer.from( 19 | 'AAAACgAAAAcAAAAAAAAACQAAAAIAAAADAAAAAAFdGFUq2t7rTo0wWu9k/6rxa0T+pf6CHmBj2vO56O1XAAAAACtw/kFMcHkbypQPj/G/SAsV8eWuRNtbQIo9kimhTJklAAAAADF88fn7/Qdf5AnejN22xxQnBxZIgcsq0VC/dFJbol8/AAAAAAAAAAIAAAADAAAAAALFJZ5Gu3RxX6evZlFupBENfCKbFy8HGJOsqYaXrFMrAAAAAIwdS0o2ARfVAN/PjN6xZrGaEuD0t7zToaDF6Z5B9peZAAAAAJnoMfsa4vmDNtUy8T76WXcr2up7H7MouQjkXcMmro3YAAAAAAAAAAMAAAAFAAAAAANLAi7Z6GdWn+x4zmK4IspnZakCCcfQZEyflPbKll7aAAAAACxvBigpTgmzVqp4CsCq0rZsbpwng2M7MHI38IGhKKrlAAAAAFaWt2CucvNouAYniCiCbacMYpTGlVK3TqKyoNF3RcOeAAAAAJF6qaV6rVYpKqq1XjVfHyYvW2GjLyQnfuwoC9gxe9N0AAAAAKqeZ9NHs6k4xGyfccNU5FR9ibBWKHbKT8Fe201ml4gTAAAAAAAAAAIAAAADAAAAAAOzZQT1wYtc2aDZwzClWkJWLJpI30Fambscye2nuM5LAAAAAEMIT2RFJXikXpmoI1QbLV965WY23JwAVgktgJ6c9ZK7AAAAAO9FFMb/rJF8AiH2mj1qZ1vZf9CTGjC5sSUu8WcVe5ZbAAAAAAAAAAIAAAADAAAAAAaweClXqq3sjNIHBm/r6o1RY6yR5HqkHJCaZtEEdMUfAAAAADPN/Tz0hFfkg11MNisvWV3/3TyFQUjJNY5VqfZfvQp7AAAAAP0swruxE0PvumzqtRK6SzUT696ryM+Q/YLWj+cgAti2AAAAAAAAAAIAAAADAAAAABXs9pU1KyIrzyOEDCUSnPpEhDvj27a5ZIG1KirxrXRfAAAAAFMKIvR3Lya8FJ+cNeaJjXTX3wCSqBxw3WYftnPvFVHeAAAAALsrraBmxcawytPurkd8D0zc8+XENcygJRlcKnh8UFvHAAAAAAAAAAIAAAADAAAAADEP2bl6LBZeQDc+D0s21t3EWLRE8cFv+Nm4LnuXojWuAAAAAGSOjdK+CEZXk+NquAzFdel7ao9eLVNWGuH1IhUhYgPFAAAAAGoDBgM8mjoqhyF2+omrrrN2fuD5JXRY/DidtZfRdw53AAAAAAAAAAIAAAADAAAAADfZ7/rdH6ulYtH+xq/VYrOUFXqtv3OwpUqUgGipFia1AAAAAKyVM4Q0zJpWVqJG8UCOI/qzycVUgcOBYZudyfD7OI7mAAAAANViLMmkYquQRtnOU92Rv7mLQfQ6hViSTr7J17PDZzF1AAAAAAAAAAMAAAAFAAAAADsphZX0B3KMw/aYQ61nzN40f/TbyuHqRF07FIzwsGPWAAAAAD8yPEEcFI+mC4b5mkYsVzg64Eg1bwhZsrAJCPlB4VggAAAAAIrmk1sAf48LuMuYqijEm9AEg5B2Fy7VsuJOEyo5E/BxAAAAAO8A7cxkPMMbm32KHNglUEI6ZRWVBgzmcQWhlRJaEOROAAAAAPsLErXhs5nB/PBjevRUYxi38TMxR/LbCU2ivvBEXjyTAAAAAA==', 20 | 'base64' 21 | ); 22 | const stellarMessage = xdr.StellarMessage.fromXDR(quorumSetBuffer); 23 | const result = getQuorumSetFromMessage(stellarMessage.qSet()); 24 | expect(result.isOk()).toBeTruthy(); 25 | if (!result.isOk()) return; 26 | expect(result.value.innerQuorumSets).toHaveLength(9); 27 | expect(result.value.threshold).toEqual(7); 28 | expect(result.value.validators).toHaveLength(0); 29 | }); 30 | -------------------------------------------------------------------------------- /test/xdr-buffer-service.test.ts: -------------------------------------------------------------------------------- 1 | import xdrService from '../src/connection/xdr-buffer-converter'; 2 | 3 | const xdrStringWithNextMessage = 'gAAANAAAAAAAAAAAAAAAAAAAAAIAAAAAv2qE3dixC3UHHZmFXQGPliZ90ghxAiO5C4fYG/G4EeqAAANUAAAAAAAAAAAAAAABAAAABQAAADIAAAAAqTm9CAAALWkAAAAAAAAAAKkvb34AAC1pAAAAAAAAAAA23YxJAAAtaQAAAAAAAAAANDdVmQAALWkAAAAAAAAAAKkzSDUAAC1pAAAAAAAAAACer1MIAAAtaQAAAAAAAAAAI8ZHBAAALWkAAAAAAAAAACPGQFcAAC1pAAAAAAAAAACCxkWYAAAtaQAAAAAAAAAAaxSf6AAALWkAAAAAAAAAACv/s4IAAC1pAAAAAAAAAAA2TpjFAAAtaQAAAAAAAAAAsj7l8gAALWkAAAAAAAAAALhIZ40AAC1pAAAAAAAAAACeQEz9AAAtaQAAAAAAAAAAq2DFJwAALWkAAAAAAAAAADZKcDEAAC1pAAAAAAAAAAAnPPzbAAAtaQAAAAAAAAAAcyEZJgAALWkAAAAAAAAAACm+Dn4AAC1pAAAAAAAAAAB04vMyAAAtaQAAAAAAAAAANk4tVQAALWkAAAAAAAAAANRcdnwAAC1pAAAAAAAAAAB9J5EIAAAtaQAAAAAAAAAApeOhygAALWkAAAAAAAAAAK4kOPYAAC1pAAAAAAAAAABnC1mkAAAtaQAAAAAAAAAANqo7KwAALWkAAAAAAAAAADRO0w0AAC1pAAAAAAAAAAABtNQAAAAtaQAAAAAAAAAAcm+nvgAALWkAAAAAAAAAALS/SCYAAC1pAAAAAAAAAAA2SvPwAAAtaQAAAAAAAAAANpKzmAAALWkAAAAAAAAAAJ5VSogAAC1pAAAAAAAAAACyotgHAAAtaQAAAAAAAAAANkoQ2wAALWkAAAAAAAAAADZOAFMAAC1pAAAAAAAAAAA2qm9GAAAtaQAAAAAAAAAANpvTAgAALWkAAAAAAAAAAG/GQiEAAC1pAAAAAAAAAAAju7lmAAAtaQAAAAAAAAAANk7rHwAALWkAAAAAAAAAABfyLQ0AAC1pAAAAAAAAAAA2TjaRAAAtaQAAAAAAAAAANk5J7gAALWkAAAAAAAAAAHRm84QAAC1pAAAAAAAAAAAr/7KvAAAtaQAAAAAAAAAAufEGsgAALWkAAAAAAAAAADZOzt4AAC1pAAAAADKjiRSHBdyaVK1C+7UoMAGGyLJ5D1CjOi7gsns2GEFBgAABaAAAAAAAAAAAAAAAAgAAAAsAAAAAAsUlnka7dHFfp69mUW6kEQ18IpsXLwcYk6yphpesUysAAAAAAULT7wAAAAN1tE4FkHboorc8QsJU7+LkIN2zbNK9MrkY49OpVcEzDwAAAAIAAAAw/0TiDQ=='; 4 | const xdrBufferWithNextMessage = Buffer.from(xdrStringWithNextMessage, 'base64'); 5 | 6 | test("getMessageLength", () =>{ 7 | expect(xdrService.getMessageLengthFromXDRBuffer(xdrBufferWithNextMessage)).toEqual(52); 8 | } ) 9 | 10 | test("xdr", () => { 11 | expect(xdrService.xdrBufferContainsCompleteMessage(xdrBufferWithNextMessage, xdrService.getMessageLengthFromXDRBuffer(xdrBufferWithNextMessage))).toEqual(true); 12 | 13 | const xdrStringWithoutNextMessage = 'gAABaAAAAAAAAAAAAAAAAgAAAAsAAAAAAsUlnka7dHFfp69mUW6kEQ18IpsXLwcYk6yphpesUysAAAAAAULT7wAAAAN1tE4FkHboorc8QsJU7+LkIN2zbNK9MrkY49OpVcEzDwAAAAIAAAAw/0TiDQ=='; 14 | const xdrBufferWithoutNextMessage = Buffer.from(xdrStringWithoutNextMessage, 'base64'); 15 | expect(xdrService.xdrBufferContainsCompleteMessage(xdrBufferWithoutNextMessage, xdrService.getMessageLengthFromXDRBuffer(xdrBufferWithoutNextMessage))).toEqual(false); 16 | 17 | let nextMessage = undefined; 18 | let remainingBuffer = undefined; 19 | 20 | [nextMessage, remainingBuffer] = xdrService.getMessageFromXdrBuffer(xdrBufferWithNextMessage, xdrService.getMessageLengthFromXDRBuffer(xdrBufferWithNextMessage)); 21 | expect(nextMessage.toString('base64')).toEqual('AAAAAAAAAAAAAAAAAAAAAgAAAAC/aoTd2LELdQcdmYVdAY+WJn3SCHECI7kLh9gb8bgR6g==') 22 | 23 | let nextNextMessage = undefined; 24 | [nextNextMessage, remainingBuffer] = xdrService.getMessageFromXdrBuffer(remainingBuffer, xdrService.getMessageLengthFromXDRBuffer(remainingBuffer)); 25 | expect(nextNextMessage.toString('base64')).toEqual('AAAAAAAAAAAAAAABAAAABQAAADIAAAAAqTm9CAAALWkAAAAAAAAAAKkvb34AAC1pAAAAAAAAAAA23YxJAAAtaQAAAAAAAAAANDdVmQAALWkAAAAAAAAAAKkzSDUAAC1pAAAAAAAAAACer1MIAAAtaQAAAAAAAAAAI8ZHBAAALWkAAAAAAAAAACPGQFcAAC1pAAAAAAAAAACCxkWYAAAtaQAAAAAAAAAAaxSf6AAALWkAAAAAAAAAACv/s4IAAC1pAAAAAAAAAAA2TpjFAAAtaQAAAAAAAAAAsj7l8gAALWkAAAAAAAAAALhIZ40AAC1pAAAAAAAAAACeQEz9AAAtaQAAAAAAAAAAq2DFJwAALWkAAAAAAAAAADZKcDEAAC1pAAAAAAAAAAAnPPzbAAAtaQAAAAAAAAAAcyEZJgAALWkAAAAAAAAAACm+Dn4AAC1pAAAAAAAAAAB04vMyAAAtaQAAAAAAAAAANk4tVQAALWkAAAAAAAAAANRcdnwAAC1pAAAAAAAAAAB9J5EIAAAtaQAAAAAAAAAApeOhygAALWkAAAAAAAAAAK4kOPYAAC1pAAAAAAAAAABnC1mkAAAtaQAAAAAAAAAANqo7KwAALWkAAAAAAAAAADRO0w0AAC1pAAAAAAAAAAABtNQAAAAtaQAAAAAAAAAAcm+nvgAALWkAAAAAAAAAALS/SCYAAC1pAAAAAAAAAAA2SvPwAAAtaQAAAAAAAAAANpKzmAAALWkAAAAAAAAAAJ5VSogAAC1pAAAAAAAAAACyotgHAAAtaQAAAAAAAAAANkoQ2wAALWkAAAAAAAAAADZOAFMAAC1pAAAAAAAAAAA2qm9GAAAtaQAAAAAAAAAANpvTAgAALWkAAAAAAAAAAG/GQiEAAC1pAAAAAAAAAAAju7lmAAAtaQAAAAAAAAAANk7rHwAALWkAAAAAAAAAABfyLQ0AAC1pAAAAAAAAAAA2TjaRAAAtaQAAAAAAAAAANk5J7gAALWkAAAAAAAAAAHRm84QAAC1pAAAAAAAAAAAr/7KvAAAtaQAAAAAAAAAAufEGsgAALWkAAAAAAAAAADZOzt4AAC1pAAAAADKjiRSHBdyaVK1C+7UoMAGGyLJ5D1CjOi7gsns2GEFB'); 26 | }); -------------------------------------------------------------------------------- /test/xdr-message-handler.test.ts: -------------------------------------------------------------------------------- 1 | import { xdr } from '@stellar/stellar-base'; 2 | import { parseAuthenticatedMessageXDR } from '../src/connection/xdr-message-handler'; 3 | 4 | test('parseAuthenticatedMessageXDR', () => { 5 | const xdrBuffer = Buffer.from( 6 | 'AAAAAAAAAAAAAAA7AAAACwAAAACMHUtKNgEX1QDfz4zesWaxmhLg9Le806GgxemeQfaXmQAAAAACKDOuAAAAAzQaCq4p6tLHpdfwGhnlyX9dMUP70r4Dm98Td6YvKnhoAAAAAQAAAJg1D82tsvx59BI2BldZq12xYzdrhUkIflWnRwbiJsoMUgAAAABg4A0jAAAAAAAAAAEAAAAAUwoi9HcvJrwUn5w15omNdNffAJKoHHDdZh+2c+8VUd4AAABAB5/NoeG4iJJitcTDJvdhDLaLL9FSUHodRXvMEjbGKeDSkSXDgl+q+VvDXenwQNOOhLg112bsviGwh61ci4HnAgAAAAEAAACYNQ/NrbL8efQSNgZXWatdsWM3a4VJCH5Vp0cG4ibKDFIAAAAAYOANIwAAAAAAAAABAAAAAFMKIvR3Lya8FJ+cNeaJjXTX3wCSqBxw3WYftnPvFVHeAAAAQAefzaHhuIiSYrXEwyb3YQy2iy/RUlB6HUV7zBI2xing0pElw4Jfqvlbw13p8EDTjoS4Nddm7L4hsIetXIuB5wIAAABAyN92d7osuHXtUWHoEQzSRH5f9h6oEQAGK02b4CO4bQchmpbwbqGQLdbD9psFpamuLrDK+QJiBuKw3PVnMNlMDA9Ws6xvU3NyJ/OBsg2EZicl61zCYxrQXQ4Qq/eXI+wT', 7 | 'base64' 8 | ); 9 | 10 | const result = parseAuthenticatedMessageXDR(xdrBuffer); 11 | expect(result.isOk()).toBeTruthy(); 12 | if (result.isOk()) { 13 | //@ts-ignore 14 | const messageType = xdr.MessageType.fromXDR(result.value.messageTypeXDR); 15 | expect(messageType).toEqual(xdr.MessageType.scpMessage()); 16 | expect( 17 | xdr.ScpEnvelope.fromXDR(result.value.stellarMessageXDR) 18 | ).toBeDefined(); 19 | } 20 | }); 21 | 22 | test('handleInvalidXdr', () => { 23 | const xdrBuffer = Buffer.from( 24 | '3a4VJCH5Vp0cG4ibKDFIAAAAAYOANIwAAAAAAAAABAAAAAFMKIvR3Lya8FJ+cNeaJjXTX3wCSqBxw3WYftnPvFVHeAAAAQAefzaHhuIiSYrXEwyb3YQy2iy/RUlB6HUV7zBI2xing0pElw4Jfqvlbw13p8EDTjoS4Nddm7L4hsIetXIuB5wIAAABAyN92d7osuHXtUWHoEQzSRH5f9h6oEQAGK02b4CO4bQchmpbwbqGQLdbD9psFpamuLrDK+QJiBuKw3PVnMNlMDA9Ws6xvU3NyJ/OBsg2EZicl61zCYxrQXQ4Qq/eXI+wT', 25 | 'base64' 26 | ); 27 | 28 | expect(parseAuthenticatedMessageXDR(xdrBuffer).isErr()).toBeTruthy(); 29 | }); 30 | 31 | /*test('handleNominateSCPMessageXDR', () => { 32 | let xdr = Buffer.from('AAAAAAFdGFUq2t7rTo0wWu9k/6rxa0T+pf6CHmBj2vO56O1XAAAAAAIpQW4AAAADNBoKrinq0sel1/AaGeXJf10xQ/vSvgOb3xN3pi8qeGgAAAABAAAAmDfNPDC76wNcNIfI9Kh/sIZzyLSqM+/2Q7ynrnWNb75gAAAAAGDlyDEAAAAAAAAAAQAAAACMHUtKNgEX1QDfz4zesWaxmhLg9Le806GgxemeQfaXmQAAAEAO4K/dJZruXyv6ypWMXhk9fy8W5Ujq8znMPy8EncZQepTRzYvqyUU4PFamzp99lly+yDt4nqgov4VZvYVVDXsPAAAAAAAAAEBpJ3HZ9TunMXViASRj5RrWlNSjA6hZZeClRGo+SYHRwq8STmObzvUvOKfgF8VTfvyqZ/LCM9FPD+iQoG2gHssB', 'base64'); 33 | 34 | //@ts-ignore 35 | let result = handleSCPMessageXD(xdr, hash(Networks.PUBLIC)) ; 36 | expect(result.isOk()).toBeTruthy(); 37 | if(result.isOk()){ 38 | expect(result.value.type).toEqual('nominate'); 39 | expect(result.value.nodeId).toEqual('GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU'); 40 | expect(result.value.slotIndex.toString()).toEqual('36258158'); 41 | expect((result.value.pledges as ScpNomination).quorumSetHash).toEqual('NBoKrinq0sel1/AaGeXJf10xQ/vSvgOb3xN3pi8qeGg='); 42 | expect((result.value.pledges as ScpNomination).votes).toEqual([ 43 | "N808MLvrA1w0h8j0qH+whnPItKoz7/ZDvKeudY1vvmAAAAAAYOXIMQAAAAAAAAABAAAAAIwdS0o2ARfVAN/PjN6xZrGaEuD0t7zToaDF6Z5B9peZAAAAQA7gr90lmu5fK/rKlYxeGT1/LxblSOrzOcw/LwSdxlB6lNHNi+rJRTg8VqbOn32WXL7IO3ieqCi/hVm9hVUNew8=" 44 | ]); 45 | expect((result.value.pledges as ScpNomination).accepted).toEqual([ 46 | "N808MLvrA1w0h8j0qH+whnPItKoz7/ZDvKeudY1vvmAAAAAAYOXIMQAAAAAAAAABAAAAAIwdS0o2ARfVAN/PjN6xZrGaEuD0t7zToaDF6Z5B9peZAAAAQA7gr90lmu5fK/rKlYxeGT1/LxblSOrzOcw/LwSdxlB6lNHNi+rJRTg8VqbOn32WXL7IO3ieqCi/hVm9hVUNew8=" 47 | ]); 48 | } 49 | }) 50 | test('handleConfirmSCPMessageXDR', () => { 51 | let xdr = Buffer.from('AAAAADPN/Tz0hFfkg11MNisvWV3/3TyFQUjJNY5VqfZfvQp7AAAAAAIpSk4AAAABAAAAAQAAAJhDlpNWjI0kZ2RCow2qCtM0XCBeAzcd81xKMpGnrYm/4AAAAABg5fhQAAAAAAAAAAEAAAAAM839PPSEV+SDXUw2Ky9ZXf/dPIVBSMk1jlWp9l+9CnsAAABAG46KDK74Y05yGtNqWKoogWBYsfc3OcIdJ49F/BV6OvN5ADZiiuPoZF1Dweo2XN3BxazSDe1u/X8TRPznHxRuDAAAAAEAAAABAAAAATQaCq4p6tLHpdfwGhnlyX9dMUP70r4Dm98Td6YvKnhoAAAAQC4eOEcKrC5gm8nt9cLITZ9XynAybzBc1TviBHoJEfVCV9ewGjvGJ4jPTZhARCGpukVQ/2qepWjG9kf96WAsDgo=', 'base64'); 52 | 53 | //@ts-ignore 54 | let result = handleSCPMessageXDR(xdr, hash(Networks.PUBLIC)) ; 55 | expect(result.isOk()).toBeTruthy(); 56 | if(result.isOk()){ 57 | expect(result.value.type).toEqual('confirm'); 58 | expect(result.value.nodeId).toEqual('GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z'); 59 | expect(result.value.slotIndex).toEqual('36260430'); 60 | expect((result.value.pledges as ScpStatementConfirm).quorumSetHash).toEqual('NBoKrinq0sel1/AaGeXJf10xQ/vSvgOb3xN3pi8qeGg='); 61 | expect((result.value.pledges as ScpStatementConfirm).nH).toEqual(1); 62 | expect((result.value.pledges as ScpStatementConfirm).nCommit).toEqual(1); 63 | expect((result.value.pledges as ScpStatementConfirm).nPrepared).toEqual(1); 64 | expect((result.value.pledges as ScpStatementConfirm).ballot.value).toEqual('Q5aTVoyNJGdkQqMNqgrTNFwgXgM3HfNcSjKRp62Jv+AAAAAAYOX4UAAAAAAAAAABAAAAADPN/Tz0hFfkg11MNisvWV3/3TyFQUjJNY5VqfZfvQp7AAAAQBuOigyu+GNOchrTaliqKIFgWLH3NznCHSePRfwVejrzeQA2Yorj6GRdQ8HqNlzdwcWs0g3tbv1/E0T85x8Ubgw='); 65 | expect((result.value.pledges as ScpStatementConfirm).ballot.counter).toEqual(1); 66 | } 67 | }) 68 | test('handleExternalizeSCPMessageXDR', () => { 69 | let xdr = Buffer.from('AAAAAAaweClXqq3sjNIHBm/r6o1RY6yR5HqkHJCaZtEEdMUfAAAAAAIpSk4AAAACAAAAAQAAAJhDlpNWjI0kZ2RCow2qCtM0XCBeAzcd81xKMpGnrYm/4AAAAABg5fhQAAAAAAAAAAEAAAAAM839PPSEV+SDXUw2Ky9ZXf/dPIVBSMk1jlWp9l+9CnsAAABAG46KDK74Y05yGtNqWKoogWBYsfc3OcIdJ49F/BV6OvN5ADZiiuPoZF1Dweo2XN3BxazSDe1u/X8TRPznHxRuDAAAAAE0GgquKerSx6XX8BoZ5cl/XTFD+9K+A5vfE3emLyp4aAAAAEBuChnRV0BBbiJe2dwhkMF+hXW6Nrq9ODUBUSHEq0wOvUnNgrVkLpvP0QTBana8Oscw2xXWMVwR/86ae3VuMXAE', 'base64'); 70 | 71 | //@ts-ignore 72 | let result = handleSCPMessageXDR(xdr, hash(Networks.PUBLIC)) ; 73 | expect(result.isOk()).toBeTruthy(); 74 | if(result.isOk()){ 75 | expect(result.value.type).toEqual('externalize'); 76 | expect(result.value.nodeId).toEqual('GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T'); 77 | expect(result.value.slotIndex.toString()).toEqual('36260430'); 78 | expect((result.value.pledges as ScpStatementExternalize).quorumSetHash).toEqual('NBoKrinq0sel1/AaGeXJf10xQ/vSvgOb3xN3pi8qeGg='); 79 | expect((result.value.pledges as ScpStatementExternalize).nH).toEqual(1); 80 | expect((result.value.pledges as ScpStatementExternalize).commit).toEqual({ 81 | "counter": 1, 82 | "value": "Q5aTVoyNJGdkQqMNqgrTNFwgXgM3HfNcSjKRp62Jv+AAAAAAYOX4UAAAAAAAAAABAAAAADPN/Tz0hFfkg11MNisvWV3/3TyFQUjJNY5VqfZfvQp7AAAAQBuOigyu+GNOchrTaliqKIFgWLH3NznCHSePRfwVejrzeQA2Yorj6GRdQ8HqNlzdwcWs0g3tbv1/E0T85x8Ubgw=" 83 | }); 84 | } 85 | }) 86 | test('handlePrepareSCPMessageXDR', () => { 87 | let xdr = Buffer.from('AAAAANViLMmkYquQRtnOU92Rv7mLQfQ6hViSTr7J17PDZzF1AAAAAAIpSk4AAAAANBoKrinq0sel1/AaGeXJf10xQ/vSvgOb3xN3pi8qeGgAAAABAAAAmEOWk1aMjSRnZEKjDaoK0zRcIF4DNx3zXEoykaetib/gAAAAAGDl+FAAAAAAAAAAAQAAAAAzzf089IRX5INdTDYrL1ld/908hUFIyTWOVan2X70KewAAAEAbjooMrvhjTnIa02pYqiiBYFix9zc5wh0nj0X8FXo683kANmKK4+hkXUPB6jZc3cHFrNIN7W79fxNE/OcfFG4MAAAAAQAAAAEAAACYQ5aTVoyNJGdkQqMNqgrTNFwgXgM3HfNcSjKRp62Jv+AAAAAAYOX4UAAAAAAAAAABAAAAADPN/Tz0hFfkg11MNisvWV3/3TyFQUjJNY5VqfZfvQp7AAAAQBuOigyu+GNOchrTaliqKIFgWLH3NznCHSePRfwVejrzeQA2Yorj6GRdQ8HqNlzdwcWs0g3tbv1/E0T85x8UbgwAAAAAAAAAAQAAAAEAAABAc+O1i4Gf0bAMZGoUEzj4aGsLLgsrA0G4LrtqXBKiSpg6QT7LKFkBDS0XURpkQspXauM+fzdOV9NY708RpFd0BA==', 'base64'); 88 | 89 | //@ts-ignore 90 | let result = handleSCPMessageXDR(xdr, hash(Networks.PUBLIC)) ; 91 | expect(result.isOk()).toBeTruthy(); 92 | if(result.isOk()){ 93 | expect(result.value.type).toEqual('prepare'); 94 | expect(result.value.nodeId).toEqual('GDKWELGJURRKXECG3HHFHXMRX64YWQPUHKCVRESOX3E5PM6DM4YXLZJM'); 95 | expect(result.value.slotIndex.toString()).toEqual('36260430'); 96 | expect((result.value.pledges as ScpStatementPrepare).quorumSetHash).toEqual('NBoKrinq0sel1/AaGeXJf10xQ/vSvgOb3xN3pi8qeGg='); 97 | expect((result.value.pledges as ScpStatementPrepare).nH).toEqual(1); 98 | expect((result.value.pledges as ScpStatementPrepare).nC).toEqual(1); 99 | expect((result.value.pledges as ScpStatementPrepare).prepared).toEqual({ 100 | "counter": 1, 101 | "value": "Q5aTVoyNJGdkQqMNqgrTNFwgXgM3HfNcSjKRp62Jv+AAAAAAYOX4UAAAAAAAAAABAAAAADPN/Tz0hFfkg11MNisvWV3/3TyFQUjJNY5VqfZfvQp7AAAAQBuOigyu+GNOchrTaliqKIFgWLH3NznCHSePRfwVejrzeQA2Yorj6GRdQ8HqNlzdwcWs0g3tbv1/E0T85x8Ubgw=" 102 | }); 103 | expect((result.value.pledges as ScpStatementPrepare).preparedPrime).toEqual(null); 104 | expect((result.value.pledges as ScpStatementPrepare).ballot.counter).toEqual(1); 105 | expect((result.value.pledges as ScpStatementPrepare).ballot.value).toEqual('Q5aTVoyNJGdkQqMNqgrTNFwgXgM3HfNcSjKRp62Jv+AAAAAAYOX4UAAAAAAAAAABAAAAADPN/Tz0hFfkg11MNisvWV3/3TyFQUjJNY5VqfZfvQp7AAAAQBuOigyu+GNOchrTaliqKIFgWLH3NznCHSePRfwVejrzeQA2Yorj6GRdQ8HqNlzdwcWs0g3tbv1/E0T85x8Ubgw='); 106 | } 107 | })*/ 108 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "outDir": "lib", 6 | "sourceMap": true, 7 | "types": ["node", "jest"], 8 | "strict": true 9 | }, 10 | "include": [ 11 | "src/**/*.ts", 12 | "decs.d.ts", 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } --------------------------------------------------------------------------------