Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"homepage": "https://github.com/ripple/validator-history-service#readme",
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/create-hash": "^1.2.2",
"@types/express": "4.17.21",
"@types/jest": "^26.0.19",
"@types/nconf": "^0.10.0",
Expand Down Expand Up @@ -62,7 +61,6 @@
"@types/bunyan": "^1.8.7",
"axios": "^0.21.1",
"bunyan": "^1.8.15",
"create-hash": "^1.2.0",
"dotenv": "^16.3.1",
"express": "4.21.2",
"knex": "2.5.1",
Expand Down
10 changes: 4 additions & 6 deletions src/connection-manager/wsHandling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
saveAmendmentStatus,
saveAmendmentsStatus,
} from '../shared/database'
import { deleteAmendmentStatus } from '../shared/database/amendments'
import {
NETWORKS_HOSTS,
deleteAmendmentStatus,
} from '../shared/database/amendments'
import {
AmendmentStatus,
DatabaseValidator,
Expand All @@ -32,11 +35,6 @@ const LEDGER_HASHES_SIZE = 10
const GOT_MAJORITY_FLAG = 65536
const LOST_MAJORITY_FLAG = 131072
const FOURTEEN_DAYS_IN_MILLISECONDS = 14 * 24 * 60 * 60 * 1000
const NETWORKS_HOSTS = new Map([
['main', 'ws://s2.ripple.com:51233'],
['test', 'wss://s.altnet.rippletest.net:51233'],
['dev', 'wss://s.devnet.rippletest.net:51233'],
])

const log = logger({ name: 'connections' })

Expand Down
173 changes: 121 additions & 52 deletions src/shared/database/amendments.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import axios from 'axios'
import createHash from 'create-hash'
import { Client, ErrorResponse } from 'xrpl'
import {
FeatureAllResponse,
FeatureOneResponse,
} from 'xrpl/dist/npm/models/methods/feature'

import { AmendmentInfo } from '../types'
import logger from '../utils/logger'
Expand All @@ -9,78 +13,142 @@ import { query } from './utils'
const log = logger({ name: 'amendments' })

const amendmentIDs = new Map<string, { name: string; deprecated: boolean }>()
const votingAmendments = new Set<string>()
const rippledVersions = new Map<string, string>()

const ACTIVE_AMENDMENT_REGEX =
/^\s*REGISTER_F[A-Z]+\s*\((?<amendmentName>\S+),\s*.*$/u
const RETIRED_AMENDMENT_REGEX =
/^ .*retireFeature\("(?<amendmentName>\S+)"\)[,;].*$/u
// TODO: Use feature RPC instead when this issue is fixed and released:
// https://github.com/XRPLF/rippled/issues/4730
const RETIRED_AMENDMENTS = [
'MultiSign',
'TrustSetAuth',
'FeeEscalation',
'PayChan',
'CryptoConditions',
'TickSize',
'fix1368',
'Escrow',
'fix1373',
'EnforceInvariants',
'SortedDirectories',
'fix1201',
'fix1512',
'fix1523',
'fix1528',
]

const AMENDMENT_VERSION_REGEX =
/\| \[(?<amendmentName>[a-zA-Z0-9_]+)\][^\n]+\| (?<version>v[0-9]*\.[0-9]*\.[0-9]*|TBD) *\|/u

// TODO: Clean this up when this PR is merged:
// https://github.com/XRPLF/rippled/pull/4781
export const NETWORKS_HOSTS = new Map([
['main', 'ws://s2.ripple.com:51233'],
['test', 'wss://s.altnet.rippletest.net:51233'],
['dev', 'wss://s.devnet.rippletest.net:51233'],
])

/**
* Fetch a list of amendments names from rippled file.
* Fetch amendments information including id, name, and deprecated status.
*
* @returns The list of amendment names.
* @returns Void.
*/
async function fetchAmendmentNames(): Promise<Map<string, boolean> | null> {
try {
const response = await axios.get(
'https://raw.githubusercontent.com/XRPLF/rippled/develop/src/libxrpl/protocol/Feature.cpp',
)
const text = response.data
const amendmentNames: Map<string, boolean> = new Map()
text.split('\n').forEach((line: string) => {
const name = ACTIVE_AMENDMENT_REGEX.exec(line)
if (name) {
amendmentNames.set(name[1], name[0].includes('VoteBehavior::Obsolete'))
} else {
const name2 = RETIRED_AMENDMENT_REGEX.exec(line)
if (name2) {
amendmentNames.set(name2[1], true)
}
}
})
return amendmentNames
} catch (err) {
log.error('Error getting amendment names', err)
return null
async function fetchAmendmentsList(): Promise<void> {
for (const [network, url] of NETWORKS_HOSTS) {
await fetchNetworkAmendments(network, url)
}
}

/**
* Extracts Amendment ID from Amendment name inside a buffer.
* Fetch amendments information including id, name, and deprecated status of a network.
*
* @param buffer -- The buffer containing the amendment name.
* @param network - The network being retrieved.
* @param url - The Faucet URL of the network.
*
* @returns The amendment ID string.
* @returns Void.
*/
function sha512Half(buffer: Buffer): string {
return createHash('sha512')
.update(buffer)
.digest('hex')
.toUpperCase()
.slice(0, 64)
async function fetchNetworkAmendments(
network: string,
url: string,
): Promise<void> {
try {
log.info(`Updating amendment info for ${network}...`)
const client = new Client(url)
await client.connect()
const featureAllResponse: FeatureAllResponse = await client.request({
command: 'feature',
})

const featuresAll = featureAllResponse.result.features

for (const id of Object.keys(featuresAll)) {
amendmentIDs.set(id, {
name: featuresAll[id].name,
deprecated: RETIRED_AMENDMENTS.includes(featuresAll[id].name),
})
votingAmendments.delete(id)
}

// Some amendments in voting are not available in feature all request.
// This loop tries to fetch them in feature one.
for (const amendment_id of votingAmendments) {
await fetchSingleAmendment(amendment_id, client)
}

await client.disconnect()

log.info(`Finished updating amendment info for ${network}...`)
} catch (error) {
log.error(
`Failed to update amendment info for ${network} due to error: ${String(
error,
)}`,
)
}
}

/**
* Maps the id of Amendments to its corresponding names.
* Fetch an amendment info from a network and add to current map.
*
* @param amendment_id - The id of the amendment to fetch.
* @param client - The Client with a websocket connection to a rippled server.
* @returns Void.
*/
async function nameOfAmendmentID(): Promise<void> {
// The Amendment ID is the hash of the Amendment name
const amendmentNames = await fetchAmendmentNames()
if (amendmentNames !== null) {
amendmentNames.forEach((deprecated, name) => {
amendmentIDs.set(sha512Half(Buffer.from(name, 'ascii')), {
name,
deprecated,
})
async function fetchSingleAmendment(
amendment_id: string,
client: Client,
): Promise<void> {
const featureResponse: FeatureOneResponse | ErrorResponse =
await client.request({
command: 'feature',
feature: amendment_id,
})

if ('result' in featureResponse) {
const feature = featureResponse.result[amendment_id]
amendmentIDs.set(amendment_id, {
name: feature.name,
deprecated: RETIRED_AMENDMENTS.includes(feature.name),
})
votingAmendments.delete(amendment_id)
}
}

/**
* Fetch amendments in voting.
*
* @returns Void.
*/
async function fetchVotingAmendments(): Promise<void> {
const votingDb = await query('ballot')
.select('amendments')
.then(async (res) =>
res.map((vote: { amendments: string | null }) => vote.amendments),
)
for (const amendmentsDb of votingDb) {
if (!amendmentsDb) {
continue
}
const amendments = amendmentsDb.split(',')
for (const amendment of amendments) {
votingAmendments.add(amendment)
}
}
}

Expand Down Expand Up @@ -145,7 +213,8 @@ export async function deleteAmendmentStatus(

export async function fetchAmendmentInfo(): Promise<void> {
log.info('Fetch amendments info from data sources...')
await nameOfAmendmentID()
await fetchVotingAmendments()
await fetchAmendmentsList()
await fetchMinRippledVersions()
amendmentIDs.forEach(async (value, id) => {
const amendment: AmendmentInfo = {
Expand Down
Loading