https://testnet1.symbol-mikun.net:3001/network/fees/transaction
{
"averageFeeMultiplier": 137,
"medianFeeMultiplier": 100,
"highestFeeMultiplier": 10000,
"lowestFeeMultiplier": 0,
"minFeeMultiplier": 10
}
これを計算してみます。
どういう計算なのか
デフォルト設定のRESTでは、直近300ブロックから求めています。ただ、トランザクションがない場合、手数料乗数がゼロになるので平均と中央値は、ゼロの場合はネットワークプロパティにあるdefaultDynamicFeeMultiplier
の値(100)に置き換えて計算しています。minFeeMultiplier
はノードプロパティで設定しているノード固有値です。
計算する
RestじゃなくてPeerに直接聞いてみます。Peerからブロックを取得するとトランザクションまで付いてくるのでサイズが大きくなりますが、今のスカスカのブロックなら大丈夫でしょう。きっと。
PacketBuffer
受信したパケットを読むためのクラスです。
export class PacketBuffer {
private _length: number
private _index: number
constructor(private readonly _buffer: Buffer) {
this._length = _buffer.length
this._index = 0
}
addOffset(addOffset: number): number {
this._index += addOffset
return this._index
}
readUInt8(addOffset: number = 0): number {
this.addOffset(addOffset)
const readData = this._buffer.readUInt8(this._index)
this._index += 1
return readData
}
readUInt16LE(addOffset: number = 0): number {
this.addOffset(addOffset)
const readData = this._buffer.readUInt16LE(this._index)
this._index += 2
return readData
}
readUInt32LE(addOffset: number = 0): number {
this.addOffset(addOffset)
const readData = this._buffer.readUInt32LE(this._index)
this._index += 4
return readData
}
readBigUInt64LE(addOffset: number = 0): bigint {
this.addOffset(addOffset)
const readData = this._buffer.readBigUInt64LE(this._index)
this._index += 8
return readData
}
readString(length: number, addOffset: number = 0): string {
this.addOffset(addOffset)
const readData = this._buffer.toString('utf8', this._index, this._index + length)
this._index += length
return readData
}
readHexString(length: number, addOffset: number = 0): string {
this.addOffset(addOffset)
const readData = this._buffer.toString('hex', this._index, this._index + length)
this._index += length
return readData
}
get index(): number {
return this._index
}
get length(): number {
return this._length
}
}
SslSocket
SSLソケット通信する抽象クラスです。後述するCatapultクラスで継承して使用します。
import { X509Certificate } from 'node:crypto'
import { readFileSync } from 'node:fs'
import tls, { ConnectionOptions } from 'node:tls'
export abstract class SslSocket {
private _connectionOptions: ConnectionOptions
protected _x509Certificate: X509Certificate | undefined
private tlsSocket: tls.TLSSocket | undefined
private readonly SYMBOL_HEADER_SIZE = 8
constructor(
caCertPath: string,
nodeCertPath: string,
nodeKeyPath: string,
host: string = '127.0.0.1',
port: number = 7900,
timeout: number = 3000
) {
this._connectionOptions = {
host,
port,
timeout,
cert: Buffer.concat([
readFileSync(nodeCertPath),
readFileSync(caCertPath),
]),
key: readFileSync(nodeKeyPath),
rejectUnauthorized: false,
}
}
protected async connect(): Promise<void> {
// console.debug('socket connected.')
this.tlsSocket = await new Promise<tls.TLSSocket>((resolve) => {
const sock = tls.connect(this._connectionOptions, () => {
resolve(sock)
})
})
}
protected async request(
packetType: number,
payload?: Uint8Array,
isResponse = true
): Promise<Uint8Array | undefined> {
// Symbolパケット生成
const payloadSize = payload ? payload.length : 0
const packetSize = this.SYMBOL_HEADER_SIZE + payloadSize
const symbolPacketBuffer = new ArrayBuffer(packetSize)
// Symbolヘッダー編集
const symbolHeader = new DataView(symbolPacketBuffer)
symbolHeader.setUint32(0, packetSize, true)
symbolHeader.setUint32(4, packetType, true)
// Symbolペイロード編集
if (payload) {
const symbolPayload = new Uint8Array(
symbolPacketBuffer,
this.SYMBOL_HEADER_SIZE,
payloadSize
)
symbolPayload.set(payload)
}
// 接続確認
if (!this.tlsSocket || this.tlsSocket.closed) await this.connect()
if (!this.tlsSocket) throw Error('Failed to connect socket.')
// Symbolパケット送信
this.tlsSocket.write(new Uint8Array(symbolPacketBuffer))
if (!isResponse) return // レスポンスなしの場合
return await this.receiver(this.tlsSocket, packetType) // レスポンスありの場合
}
private async receiver(
socket: tls.TLSSocket,
packetType: number
): Promise<Uint8Array | undefined> {
return new Promise<Uint8Array | undefined>((resolve, reject) => {
if (this.tlsSocket === undefined) reject('tlsSocket undefined.')
let responseSize = this.SYMBOL_HEADER_SIZE // ヘッダ分のサイズを前もって付与
let responseData: Uint8Array | undefined = undefined
// SSL接続
socket.on(
'secureConnect',
() => (this._x509Certificate = socket.getPeerX509Certificate())
)
// データ受信
socket.once('data', (data) => {
// レスポンスデータ(ヘッダ)取得
const nodeBufferView = Buffer.from(new Uint8Array(data).buffer)
// レスポンスサイズチェック
const responseDataSize = nodeBufferView.readUInt32LE(0)
if (responseDataSize === 0) {
socket.destroy()
reject('Received data is empty.')
}
// レスポンスパケットタイプチェック
const responsePacketType = nodeBufferView.readUInt32LE(4)
if (responsePacketType !== packetType) {
socket.destroy()
reject(
`Mismatch packet type: expect: ${packetType} actual: ${responsePacketType}`
)
}
// ヘッダが問題なければデータ部取得
socket.on('data', (data) => {
const tempResponseData = new Uint8Array(data)
responseSize += tempResponseData.length
if (!responseData) {
// 初回
responseData = tempResponseData
} else {
// 連結
const merged = new Uint8Array(
responseData.length + tempResponseData.length
)
merged.set(responseData)
merged.set(tempResponseData, responseData.length)
responseData = merged
}
if (responseDataSize <= responseSize) {
resolve(responseData)
}
})
})
// タイムアウト
socket.on('timeout', () => {
socket.destroy()
console.debug(`socket timeout: ${packetType}`)
reject('timeout')
})
// エラー
socket.on('error', (error) => {
socket.destroy()
console.debug(`socket error: ${packetType}`)
reject(error)
})
// 切断
socket.on('close', () => {
// console.debug(`socket close: ${packetType}`)
})
})
}
protected close() {
if (this.tlsSocket && !this.tlsSocket.closed) {
this.tlsSocket.end(new Uint8Array())
}
}
}
Catapult
SSLソケット通信で実際にデータを取得するクラスです。パケットタイプは色々ありますが、今回使用するブロック高とブロック(レンジ読み)を取得する所だけ書いてます。
import { models } from 'symbol-sdk/symbol'
import { PacketBuffer } from './PacketBuffer.js'
import { SslSocket } from './SslSocket.js'
class ChainStatistics {
constructor(
public height: bigint,
public finalizedHeight: bigint,
public scoreHigh: bigint,
public scoreLow: bigint
) {}
static deserialize(payload: Uint8Array) {
const bufferView = new PacketBuffer(Buffer.from(payload))
const height = bufferView.readBigUInt64LE()
const finalizedHeight = bufferView.readBigUInt64LE()
const scoreHigh = bufferView.readBigUInt64LE()
const scoreLow = bufferView.readBigUInt64LE()
return new ChainStatistics(height, finalizedHeight, scoreHigh, scoreLow)
}
}
export class Catapult extends SslSocket {
private PacketType = {
CHAIN_STATISTICS: 0x0_05,
PULL_BLOCKS: 0x0_08,
}
async getChainStatistics(isSocketClose: boolean = true) {
let chainStatistics: ChainStatistics | undefined
try {
const socketData = await this.request(this.PacketType.CHAIN_STATISTICS)
if (socketData) chainStatistics = ChainStatistics.deserialize(socketData)
if (isSocketClose) this.close()
} catch (e) {
console.error(e)
}
return chainStatistics
}
async getBlocks(
startHeight: bigint,
count: number,
maxSize: number,
isSocketClose: boolean = true
) {
const blocks: models.Block[] = []
try {
const payloadBuffer = new ArrayBuffer(16)
const payloadDataView = new DataView(payloadBuffer)
payloadDataView.setBigUint64(0, startHeight, true)
payloadDataView.setInt32(8, count, true)
payloadDataView.setInt32(12, maxSize, true)
const socketData = await this.request(
this.PacketType.PULL_BLOCKS,
Buffer.from(payloadBuffer)
)
if (socketData) {
let offset = 0
do {
const block = models.BlockFactory.deserialize(
socketData.slice(offset)
) as models.Block
blocks.push(block)
offset += block.size
} while (offset < socketData.length)
}
if (isSocketClose) this.close()
} catch (e) {
console.error(e)
}
return blocks
}
}
証明書準備
SSLソケット通信のための証明書を作成します。
npm install -g simple-symbol-node-cert-cli
simple-symbol-node-cert-cli generate
パスワードは秘密鍵を暗号化して保存するためのものです。
main
Catapultのコンストラクタに渡すノードホスト名は公開されているノードであれば、Peerでも何でも良いです。ちなみに、sakiaはPeerのみです。
現在ブロック高を取得して、そこから過去300ブロック分取得し、平均値、中央値、最大値、最長値を算出します。
import { Catapult } from './catapult/Catapult.js'
const catapult = new Catapult(
'cert/ca.crt.pem',
'cert/node.crt.pem',
'cert/node.key.pem',
'sakia.harvestasya.com'
)
// 平均
const ave = (arr: number[]) => arr.reduce((pre, cur) => pre + cur) / arr.length
// 中央値
const mdn = (arr: number[]) => {
arr.sort()
const mid = arr.length / 2
return mid % 1 ? arr[mid - 0.5] : (arr[mid - 1] + arr[mid]) / 2
}
// 最大値
const max = (arr: number[]) => arr.reduce((pre, cur) => Math.max(pre, cur))
// 最小値
const min = (arr: number[]) => arr.reduce((pre, cur) => Math.min(pre, cur))
// Peerから現在のブロック高取得
const chainStatistics = await catapult.getChainStatistics(false) // ソケット開けたままにする
if (!chainStatistics) throw Error('no data.')
// Peerから300ブロック取得
const startHeight = chainStatistics.height - BigInt(299)
const blocks = await catapult.getBlocks(startHeight, 300, 1_048_576) // 1MiBで打ち切り
// 手数料乗数取得
const defaultDynamicFeeMultiplier = 100 // ネットワークプロパティにあるデフォルト値
const feeMultipliers = []
for (const block of blocks) {
feeMultipliers.push(block.feeMultiplier.value as number)
}
// 0はdefaultDynamicFeeMultiplierの値に置き換え
const defaultedFeeMultipliers = feeMultipliers.map((val) =>
val === 0 ? defaultDynamicFeeMultiplier : val
)
// 各種値算出
const averageFeeMultiplier = Math.floor(ave(defaultedFeeMultipliers))
const medianFeeMultiplier = Math.floor(mdn(defaultedFeeMultipliers))
const highestFeeMultiplier = max(feeMultipliers)
const lowestFeeMultiplier = min(feeMultipliers)
console.log(`averageFeeMultiplier: ${averageFeeMultiplier}`)
console.log(`medianFeeMultiplier : ${medianFeeMultiplier}`)
console.log(`highestFeeMultiplier: ${highestFeeMultiplier}`)
console.log(`lowestFeeMultiplier : ${lowestFeeMultiplier}`)