Skip to content

Commit

Permalink
added scp reader class that translates and logs scp messages to the
Browse files Browse the repository at this point in the history
actual scp protocol statements. Early stage.
  • Loading branch information
pieterjan84 committed Dec 4, 2023
1 parent 9c4de37 commit 178bf8f
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 0 deletions.
71 changes: 71 additions & 0 deletions examples/scp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const { xdr, StrKey } = require('stellar-base');
const { createNode } = require('../lib');
const getConfigFromEnv = require('../lib').getConfigFromEnv;
const http = require('http');
const https = require('https');
const {ScpReader} = require("../lib/scp-reader");
const pino = require('pino')()
let node = createNode(getConfigFromEnv());

connect();

async function connect() {
if (process.argv.length <= 2) {
console.log(
'Parameters: ' + 'NODE_IP(required) ' + 'NODE_PORT(default: 11625) '
);
process.exit(-1);
}

let ip = process.argv[2];
let port = 11625;

let portArg = process.argv[3];
if (portArg) {
port = parseInt(portArg);
}

const nodes = await fetchData('https://api.stellarbeat.io/v1/nodes');
const nodeNames = new Map(nodes.map((node) => {
return [node.publicKey, node.name ?? node.publicKey];
}));

const scpReader = new ScpReader(pino)
scpReader.read(node, ip, port, nodeNames)
}

function fetchData(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;

const request = client.get(url, (response) => {
let data = '';

// A chunk of data has been received.
response.on('data', (chunk) => {
data += chunk;
});

// The whole response has been received.
response.on('end', () => {
resolve(JSON.parse(data));
});
});

// Handle errors during the request.
request.on('error', (error) => {
reject(error);
});
});
}

function trimString(str, lengthToShow = 5) {
if (str.length <= lengthToShow * 2) {
return str;
}

const start = str.substring(0, lengthToShow);
const end = str.substring(str.length - lengthToShow);

return `${start}...${end}`;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preversion": "yarn run build",
"build": "tsc --declaration",
"examples:connect": "npm run build; node examples/connect",
"examples:scp": "npm run build; node examples/scp",
"test": "jest"
},
"types": "lib/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
ScpNomination
} from './scp-statement-dto';
export { getConfigFromEnv } from './node-config';
export { ScpReader } from "./scp-reader";
export {
getPublicKeyStringFromBuffer,
createSCPEnvelopeSignature,
Expand Down
207 changes: 207 additions & 0 deletions src/scp-reader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {Logger} from "pino";
import {Node} from "./node";
import {StrKey, xdr} from "stellar-base";

type PublicKey = string;
type Ledger = string;
type Value = string;

export class ScpReader {
private nominateVotes: Map<Ledger, Map<PublicKey, Value[]>> = new Map();
private nominateAccepted: Map<Ledger, Map<PublicKey, Value[]>> = new Map();

constructor(private logger: Logger) {

}

private isNewNominateVote(ledger: Ledger, publicKey: PublicKey, value: Value[]): boolean {
if (value.length === 0)
return false;
const ledgerVotes = this.nominateVotes.get(ledger);
if (!ledgerVotes)
return true;

const votesByNode = ledgerVotes.get(publicKey);
if (!votesByNode)
return true;

return votesByNode.length !== value.length;
}

private registerNominateVotes(ledger: Ledger, publicKey: PublicKey, value: Value[]) {
let ledgerVotes = this.nominateVotes.get(ledger);
if (!ledgerVotes) {
ledgerVotes = new Map();
this.nominateVotes.set(ledger, ledgerVotes);
}

const votesByNode = ledgerVotes.get(publicKey);
if (!votesByNode) {
ledgerVotes.set(publicKey, value);
}
}

private isNewNominateAccepted(ledger: Ledger, publicKey: PublicKey, value: Value[]): boolean {
if (value.length === 0)
return false;

const ledgerAccepted = this.nominateAccepted.get(ledger);
if (!ledgerAccepted)
return true;

const acceptedByNode = ledgerAccepted.get(publicKey);
if (!acceptedByNode)
return true;

return acceptedByNode.length !== value.length;
}

private registerNominateAccepted(ledger: Ledger, publicKey: PublicKey, value: Value[]) {
let ledgerAccepted = this.nominateAccepted.get(ledger);
if (!ledgerAccepted) {
ledgerAccepted = new Map();
this.nominateAccepted.set(ledger, ledgerAccepted);
}

const acceptedByNode = ledgerAccepted.get(publicKey);
if (!acceptedByNode) {
ledgerAccepted.set(publicKey, value);
}
}

read(node: Node, ip: string, port: number, nodeNames: Map<string, string>): void {
this.logger.info('Connecting to ' + ip + ':' + port);

const connection = node.connectTo(ip, port);
connection
.on('connect', (publicKey, nodeInfo) => {
console.log('Connected to Stellar Node: ' + publicKey);
console.log(nodeInfo);
})
.on('data', (stellarMessageJob) => {
const stellarMessage = stellarMessageJob.stellarMessage;
//console.log(stellarMessage.toXDR('base64'))

switch (stellarMessage.switch()) {
case xdr.MessageType.scpMessage():
this.translateSCPMessage(stellarMessage, nodeNames);
break;
default:
console.log(
'rcv StellarMessage of type ' + stellarMessage.switch().name //+
//': ' +
// stellarMessage.toXDR('base64')
);
break;
}
stellarMessageJob.done();
})
.on('error', (err) => {
console.log(err);
})
.on('close', () => {
console.log('closed connection');
})
.on('timeout', () => {
console.log('timeout');
connection.destroy();
});
}

private translateSCPMessage(stellarMessage: xdr.StellarMessage, nodeNames: Map<string, string>) {
const publicKey = StrKey.encodeEd25519PublicKey(
stellarMessage.envelope().statement().nodeId().value()
).toString();
const name = nodeNames.get(publicKey);
const ledger = stellarMessage.envelope().statement().slotIndex().toString();

if (stellarMessage.envelope().statement().pledges().switch() === xdr.ScpStatementType.scpStNominate()) {
this.translateNominate(stellarMessage, ledger, publicKey, name);
} else if (stellarMessage.envelope().statement().pledges().switch() === xdr.ScpStatementType.scpStPrepare()) {
this.translatePrepare(stellarMessage, ledger, name);
} else if (stellarMessage.envelope().statement().pledges().switch() === xdr.ScpStatementType.scpStConfirm()) {
this.translateCommit(stellarMessage, ledger, name);
} else if (stellarMessage.envelope().statement().pledges().switch() === xdr.ScpStatementType.scpStExternalize()) {
this.translateExternalize(stellarMessage, ledger, name);
}
}

private translateCommit(stellarMessage: xdr.StellarMessage, ledger: string, name: string | undefined) {
const ballotValue = this.trimString(stellarMessage.envelope().statement().pledges().confirm().ballot().value().toString('hex'));
const cCounter = stellarMessage.envelope().statement().pledges().confirm().nCommit();
const hCounter = stellarMessage.envelope().statement().pledges().confirm().nH();
const preparedCounter = stellarMessage.envelope().statement().pledges().confirm().nPrepared();
console.log(ledger + ': ' + name + ':ACCEPT(COMMIT<' + cCounter + ' - ' + hCounter + ',' + ballotValue + '>)')
console.log(ledger + ': ' + name + ':VOTE|ACCEPT(PREPARE<Inf,' + ballotValue + '>)')
console.log(ledger + ': ' + name + ':ACCEPT(PREPARE<' + preparedCounter + ',' + ballotValue + '>)')
console.log(ledger + ': ' + name + ':CONFIRM(PREPARE<' + hCounter + ',' + ballotValue + '>)')
console.log(ledger + ': ' + name + ':VOTE(COMMIT<' + cCounter + ' - Inf,' + ballotValue + '>)')
}

private translateExternalize(stellarMessage: xdr.StellarMessage, ledger: string, name: string | undefined) {
const ballotValue = this.trimString(stellarMessage.envelope().statement().pledges().externalize().commit().value().toString('hex'));
const ballotCounter = stellarMessage.envelope().statement().pledges().externalize().commit().counter();
console.log(ledger + ': ' + name + ':ACCEPT(COMMIT<' + ballotCounter + ' - Inf,' + ballotValue + '>)')

const hCounter = stellarMessage.envelope().statement().pledges().externalize().nH();
console.log(ledger + ': ' + name + ':CONFIRM(COMMIT<' + ballotCounter + ' - ' + hCounter + ',' + ballotValue + '>)')

console.log(ledger + ': ' + name + ':ACCEPT(PREPARE<Inf,' + ballotValue + '>)')
console.log(ledger + ': ' + name + ':CONFIRM(PREPARE<' + hCounter + ',' + ballotValue + '>)')
}

private translatePrepare(stellarMessage: xdr.StellarMessage, ledger: string, name: string | undefined) {
const ballotValue = this.trimString(stellarMessage.envelope().statement().pledges().prepare().ballot().value().toString('hex'));
const ballotCounter = stellarMessage.envelope().statement().pledges().prepare().ballot().counter().toString();
const prepared = stellarMessage.envelope().statement().pledges().prepare().prepared();
console.log(ledger + ': ' + name + ':VOTE(PREPARE<' + ballotCounter + ',' + ballotValue + '>)')

if (prepared) {
const preparedBallotValue = this.trimString(prepared.value().toString('hex'));
const preparedBallotCounter = prepared.counter().toString();
console.log(ledger + ': ' + name + ':ACCEPT(PREPARE<' + preparedBallotCounter + ',' + preparedBallotValue + '>)')

//if prepared.value changes, ABORT is implied for all indices smaller than aCounter. aCounter is computed (see doc).
}

const hCounter = stellarMessage.envelope().statement().pledges().prepare().nH();
if (hCounter !== 0 && hCounter !== undefined) {
console.log(ledger + ': ' + name + ':CONFIRM(PREPARE<' + hCounter + ',' + ballotValue + '>)')
}

const cCounter = stellarMessage.envelope().statement().pledges().prepare().nC();
if (cCounter !== 0 && cCounter !== undefined) {
console.log(ledger + ': ' + name + ':VOTE(COMMIT<' + cCounter + ' - ' + hCounter + ',' + ballotValue + '>)')
}
}

private translateNominate(stellarMessage: xdr.StellarMessage, ledger: string, publicKey: string, name: string | undefined) {
const nominateVotes = stellarMessage.envelope().statement().pledges().nominate().votes().map((vote: Buffer) => {
return this.trimString(vote.toString('hex'));
});
if (this.isNewNominateVote(ledger, publicKey, nominateVotes)) {
console.log(ledger + ': ' + name + ':VOTE(NOMINATE([' + nominateVotes + ']))')
this.registerNominateVotes(ledger, publicKey, nominateVotes)
}

const nominateAccepted = stellarMessage.envelope().statement().pledges().nominate().accepted().map((accepted: Buffer) => {
return this.trimString(accepted.toString('hex'));
});
if (this.isNewNominateAccepted(ledger, publicKey, nominateAccepted)) {
console.log(ledger + ': ' + name + ':ACCEPT(NOMINATE([' + nominateAccepted + ']))')
this.registerNominateAccepted(ledger, publicKey, nominateAccepted)
}
}

private trimString(str: string, lengthToShow = 5) {
if (str.length <= lengthToShow * 2) {
return str;
}

const start = str.substring(0, lengthToShow);
const end = str.substring(str.length - lengthToShow);

return `${start}...${end}`;
}

}

0 comments on commit 178bf8f

Please sign in to comment.