Cookbook
Здесь собраны типовые задачи, которые возникают почти в каждом проекте на hlf-core, и короткие решения к ним — каждое читается за минуту и опирается на уже разобранные концепции, а не вводит новые. Если полный путь сборки команды нужен целиком, он описан в туториале; тут же — отдельные приёмы, к которым возвращаешься по мере роста проекта.
Свой алгоритм подписи
Задача — chaincode должен принимать подпись алгоритмом, которого нет среди готовых пакетов. Поскольку
подпись в hlf-core вынесена на уровень приложения и спрятана за ITransportCryptoManager, добавление
алгоритма не трогает ни команды, ни обработчики — нужен лишь новый менеджер и одна строка регистрации.
Менеджер отвечает за три вещи — имя алгоритма, подпись и проверку. Каноническую строку, которую
подписывают обе стороны, собирает базовый TransportCryptoManager.toSign, так что менеджеру остаётся
приложить к ней свой криптографический примитив:
import { ITransportCommand, ITransportCryptoManager, ISignature, TransportCryptoManager } from '@ts-core/common';
export class TransportCryptoManagerMyAlgorithm extends TransportCryptoManager implements ITransportCryptoManager {
public static ALGORITHM = 'MyAlgorithm';
public get algorithm(): string {
return TransportCryptoManagerMyAlgorithm.ALGORITHM;
}
public async sign<U>(command: ITransportCommand<U>, nonce: string, privateKey: string): Promise<string> {
let message = TransportCryptoManager.toSign(command, nonce);
return MyPrimitive.sign(message, privateKey);
}
public async verify<U>(command: ITransportCommand<U>, signature: ISignature): Promise<boolean> {
let message = TransportCryptoManager.toSign(command, signature.nonce);
return MyPrimitive.verify(message, signature.value, signature.publicKey);
}
}
Дальше менеджер добавляется в массив cryptoManagers приёмника, и этого достаточно — chaincode
начинает принимать команды, подписанные этим алгоритмом, а незнакомый алгоритм по-прежнему
отклоняется:
const transport = new TransportFabricChaincodeReceiver(logger, {
cryptoManagers: [new TransportCryptoManagerEd25519(), new TransportCryptoManagerMyAlgorithm()],
});
Выбор менеджера при проверке идёт по полю signature.algorithm, поэтому на клиенте достаточно
подписать команду тем же менеджером — больше ничего согласовывать не нужно. Подробно про каноническую
строку и готовые алгоритмы — в разделе «Криптография».
Новый токен с первичной эмиссией
Задача — выпустить монету и сразу начислить начальный объём на счета. Прикладной код обычно не трогает менеджер балансов напрямую, а идёт через сервис, который валидирует вход, выполняет операцию и диспетчеризует события. Сама монета создаётся фабрикой с нулевым балансом, затем на неё начисляется первичная эмиссия:
let item = CoinFactory.transform({ uid, balance: { held: '0', inUse: '0', burned: '0', emitted: '0' } });
let manager = new CoinManager(logger, holder.stub);
await manager.add(item);
await holder.stub.dispatch(new CoinAddedEvent({ coin: item, initiatorUid: params.initiatorUid }));
for (let { object, value } of params.emission ?? []) {
if (MathUtil.greaterThan(value, '0')) {
await manager.emitInitial(item, object, value);
await holder.stub.dispatch(new CoinEmittedEvent({ coinUid: item.uid, value, object, initiatorUid: params.initiatorUid }));
}
}
Два момента, которые легко упустить. Во-первых, монета создаётся с нулевым балансом, а начальный объём
начисляется отдельным шагом emitInitial, и каждое начисление сопровождается своим событием.
Во-вторых, суммы везде строками и через MathUtil, поэтому проверка greaterThan(value, '0') это
строковое сравнение, а не числовое. Полная модель счетов и балансов — в разделе
«Токены».
Guard со связкой publicKey и userId
Задача — убедиться, что команду прислал именно тот пользователь, за которого он себя выдаёт.
Транспорт проверяет, что подпись математически верна и соответствует publicKey, но он не знает, чей
это ключ — связка «ключ принадлежит этому userId» живёт в прикладном состоянии, и замыкать её нужно
guard'ом перед бизнес-логикой. Канонический рецепт грузит пользователя по stub.user.id, проверяет
статус и сверяет записанный в ledger ключ с ключом подписи:
async function validateUserStubHolder<U>(logger: ILogger, item: IUserStubHolder<U>, options: IUserGuardOptions): Promise<void> {
if (_.isEmpty(options) || !options.isNeedCheck) {
return;
}
let { id } = item.stub.user;
let { user } = item;
if (_.isNil(user)) {
item.manager = new UserManager(logger, item.stub);
item.user = user = await item.manager.get(id, ['cryptoKey']);
}
if (_.isNil(user)) {
throw new UserNotFoundError(id);
}
let { status, cryptoKey } = user;
if (status !== UserStatus.ACTIVE) {
throw new UserStatusForbiddenError({ value: status, expected: UserStatus.ACTIVE });
}
if (_.isNil(cryptoKey)) {
throw new UserCryptoKeyNotFoundError(id);
}
if (cryptoKey.value !== item.stub.user.publicKey) {
throw new UserCryptoKeyInvalidError(id);
}
}
Ключевая строка — последняя: cryptoKey.value !== item.stub.user.publicKey. Именно здесь ключ,
которым подписана команда, сверяется с ключом, который записан за пользователем в состоянии. Без этой
сверки чужой валидной подписью можно было бы выдать себя за другого userId. Для читающих команд,
которым пользователь не нужен, проверка выключается опцией @UserGuard({ isNeedCheck: false }), а
проверка ролей выносится отдельно — rolesCheck(holder, ...roles) прямо в execute. Разбор целиком —
в разделах «Обработчики команд» и «Безопасность».
Перенос операции в batch
Задача — команд много, каждая мелкая, и накладные расходы на отдельную transaction начинают преобладать над самой работой. Batch собирает несколько команд и проводит их одной transaction'ой; переключение делается на двух сторонах и не требует переписывать сами команды.
На стороне chaincode режим задаётся выбором приёмника — при наличии настроек batch берётся batch-приёмник:
useFactory: (logger: Logger) => {
if (_.isNil(settings.batch)) {
let item = new TransportFabricChaincodeReceiver(logger, settings);
item.createStubFactory = (logger, stub, payload, transport) =>
new TransportFabricStubStateProxy(logger, stub, payload.id, payload.options, transport);
return item;
}
return new TransportFabricChaincodeReceiverBatch(logger, settings);
}
На стороне клиента batch-режиму отвечает TransportFabricBatch, а чтобы не гадать, включён ли он,
chaincode отдаёт свои метаданные readonly-командой — по ним клиент и выбирает транспорт:
const metadata = await transport.sendListen(new ChaincodeMetadataGetCommand());
// по metadata.mode и metadata.batch клиент берёт TransportFabric или TransportFabricBatch
Главное помнить про цену решения — ответ на изменяющую команду приходит не сразу, а позже, из блока, как карта «хэш команды → ответ», и readonly-команды в пакет не попадают вовсе. Это сознательный размен мгновенности ответа на пропускную способность. Подробнее — в разделе «Batch-режим».