Як і обіцяв у минулій статті, Я напишу невеликі рекомендації щодо використання модуля ChainSyncer.
Максимальна узгодженість
Через асинхронні характеристики модуля та надмірної складності RPC серверів,при взаємодії з модулем потрібні деякі хитрощі,для того щоб підтримувати узгодженність з блокчейном.
Напариклад: Ми маємо абстрактний контракт на стейкінг.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
/**
* Some very-very abstract staking contract without any sense
*/
contract Staking {
struct Pool {
uint256 total_staked;
}
mapping(uint256 => Pool) pools;
event Staked(
uint256 indexed pool_id,
address staked_by,
uint256 current_total_staked
);
function stake(uint256 pool_id_, uint256 amount_) external {
pools[pool_id_].total_staked += amount_;
emit Staked(pool_id_, msg.sender, pools[pool_id_].total_staked);
}
}
Завдання полягає в тому щоб відстежити кількість токенів у пулі в умовах високого навантаження контракту (припустимо, дуже популярного).
Робитимемо це за допомогою Staked
події.
// Example using Mongoose
// This is just an abstract example, dont try to copy-paste it
const Mongoose = require('mongoose');
const { ChainSyncer } = require('chain-syncer');
const { MongoDBAdapter } = require('@chainsyncer/mongodb-adapter');
// where will we store the pulled events
await Mongoose.connect(process.env['MONGO_SRV']);
const adapter = new MongoDBAdapter(Mongoose.connection.db);
const syncer = new ChainSyncer(adapter, { /* ... some options ... */ })
syncer.on('Staking.Staked', async (
{ global_index }, // will need in the next example
pool_id,
staked_by, // actually dont need
current_total_staked,
) => {
await StakingPool.updateOne({
_id: pool_id,
}, {
$set: {
total_staked: Number(current_total_staked),
},
});
});
Обов'язково потрібно переконатись, що наші обробники є ідемпотентними. Це можливо тільки у тому випадку, якщо ви проєктуете події таким чином,щоб вони зберігали поточний моментальний знімок даних: у нашому випадку total_staked
.Навпаки, якщо ми збережемо amount_
, то нам доведеться продивитись усі події тільки для того щоб дізнатись, скільки зараз застейкано. Це дуже ускладнить код.
У наданому вище коді ховалася доволі серйозна помилка. Оскільки модуль обробляє події паралельно пакетами (для максимальної продуктивності), цілком ймовірно, що більш рання подія буде оброблена після більш пізньої. У результаті, total_staked
на вашому сервері буде менше, ніж повинно бути.
Тут у гру вступає параметр global_index
. З його допомогою ми можемо визначити найбільш актуальний стан total_staked
пула.
Додаючи деякі правки у наш обробник, ми можемо прибрати ризики помилок синхронізації:
syncer.on('Staking.Staked', async (
{ global_index }, // we need only global_index
pool_id,
staked_by, // actually dont need
current_total_staked,
) => {
await StakingPool.updateOne({
_id: pool_id,
// we need to update total_staked only if global_index is greater than the last one saved
last_synced_at: { $lt: global_index },
}, {
$set: {
total_staked: Number(current_total_staked),
// dont't forget to save
last_synced_at: global_index,
},
});
});
Ви, напевно, спитаєте: Звідки береться цей параметр?
Він збирається ChainSyncer'ом для кожної події шляхом поєднання blockNumber
та logIndex
.
Таким чином, ми отримуємо щось на зразок додаткового унікального ID для кожної події.
Перенесення подій
Іншою функцією, призначеною для вирішення проблем асинхронності модуля, є збереження послідовності обробки подій.
Я наведу вам приклад. У вас є об’єкт меча для вашої крипто-гри, який повністю зберігається в контракті.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Swords {
struct Sword {
uint256 damage;
bool is_enchanted;
}
Sword[] public swords;
event Created(
uint256 sword_id,
uint256 damage
);
event Enchanted(
uint256 sword_id
);
function _create(uint256 damage_) internal returns (uint256) {
uint256 sword_id = swords.length;
swords.push(Sword(damage_, false));
return sword_id;
}
function create(uint256 damage_) external {
uint256 sword_id = _create(damage_);
emit Created(sword_id, damage_);
}
function createAndEnchant(uint256 damage_) external {
uint256 sword_id = _create(damage_);
swords[sword_id].is_enchanted = true;
emit Created(sword_id, damage_);
emit Enchanted(sword_id);
}
}
У нас також є backend який відстежує статус кожного меча в контракті.
syncer.on('Swords.Created', async (
{ global_index, block_timestamp }, // don't need
sword_id,
damage
) => {
// uint256 that comes from event syncer converts to string, but we need a number
sword_id = Number(sword_id);
damage = Number(damage);
// getting the sword by id
const sword = await Sword.findOne({ _id: sword_id });
// will create a new one, if no sword found in our DB
if(!sword) {
await Sword.create({
_id: sword_id,
damage: damage,
is_enchanted: false,
});
}
});
syncer.on('Swords.Enchanted', async (
{ global_index, block_timestamp }, // don't need
sword_id,
) => {
sword_id = Number(sword_id);
await Sword.updateOne({
_id: sword_id,
}, {
$set: {
is_enchanted: true, // updating owner address
}
});
});
Як я описав вище, події обробляються паралельно, і в цьому прикладі нам потрібна чітка послідовність виконання обробників. Оскільки Enchanted
і Created
створюються в одній транзакції, існує надзвичайно високий ризик того, що подія Enchanted
буде оброблено раніше події Created
, що призведе до втрати узгодженості.
Щоб усунути ймовірність цієї помилки, ChainSyncer має цивільний механізм для відкладення подій: якщо false
повертається з обробника, то обробка події відкладається на наступну ітерацію.
syncer.on('Swords.Enchanted', async (
{ global_index, block_timestamp }, // don't need
sword_id,
) => {
sword_id = Number(sword_id);
// getting the sword by id
const sword = await Sword.findOne({ _id: sword_id });
// if no sword found in our DB,
// we will postpone this event till the sword will be created
if(!sword) {
return false;
}
await Sword.updateOne({
_id: sword_id,
}, {
$set: {
is_enchanted: true, // updating owner address
}
});
});
Використовуючи цей механізм, ми отримуємо щось на зразок графіка залежності подій.
На даний момент це все. Використовуючи ці практики, ви отримаєте не тільки високу ефективність, а й надійну узгодженість, щоб ви могли міцно спати.
У наступній статті я розповім вам про більш глибоку роботу з модулем — сканування окремих блоків, режим сканера і, можливо, щось більше.
Друзі, не забуваємо донатити на 🇺🇦 ЗСУ. Залишу посилання для тих хто в танку.