libp2p-middleware-evm
Version:
EVM middleware for libp2p
269 lines • 12.2 kB
JavaScript
/* eslint-disable no-console */
import {} from '@libp2p/interface';
import { verifyMessage } from 'ethers';
import { ruleDefinitionSchema } from 'evm-rule-engine';
import { lpStream } from 'it-length-prefixed-stream';
import { v7 as uuidv7 } from 'uuid';
import { z } from 'zod';
import { MAX_INBOUND_STREAMS, MAX_OUTBOUND_STREAMS, PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION, TIMEOUT } from './constants.js';
export class MiddlewareEVM {
protocol;
components;
started;
timeout;
maxInboundStreams;
maxOutboundStreams;
runOnLimitedConnection;
log;
decoratedConnections;
evmRuleEngine;
signer;
constructor(components, init) {
this.components = components;
this.log = components.logger.forComponent('libp2p:middleware-evm');
this.started = false;
this.protocol = `/${init.protocolPrefix ?? PROTOCOL_PREFIX}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}`;
this.timeout = init.timeout ?? TIMEOUT;
this.maxInboundStreams = init.maxInboundStreams ?? MAX_INBOUND_STREAMS;
this.maxOutboundStreams = init.maxOutboundStreams ?? MAX_OUTBOUND_STREAMS;
this.runOnLimitedConnection = init.runOnLimitedConnection ?? true;
this.signer = init.signer;
if (init.evmRuleEngine == null) {
throw new Error('EVM engine required');
}
this.evmRuleEngine = init.evmRuleEngine;
this.decoratedConnections = new Set();
this.handle = this.handle.bind(this);
this.handler = this.handler.bind(this);
}
[Symbol.toStringTag] = 'libp2p-middleware-evm';
async start() {
if (this.started)
return;
// Check if the protocol is already registered before trying to register it
try {
// Try to get existing handler first
this.components.registrar.getHandler(this.protocol);
// If we get here, the protocol is already registered
this.log(`Protocol ${this.protocol} already registered, skipping`);
}
catch (err) {
// handle registering protocol
if (err.name === 'UnhandledProtocolError') {
await this.components.registrar.handle(this.protocol, this.handler, {
maxInboundStreams: this.maxInboundStreams,
maxOutboundStreams: this.maxOutboundStreams,
runOnLimitedConnection: this.runOnLimitedConnection
});
this.log(`Registered handler for ${this.protocol}`);
}
else {
throw err;
}
}
this.log(`Started evm middleware with protocol ${this.protocol}`);
this.started = true;
}
async stop() {
if (!this.started)
return;
// Unregister the protocol handler
try {
// Make sure the protocol is registered before trying to unregister it
this.components.registrar.getHandler(this.protocol);
await this.components.registrar.unhandle(this.protocol);
this.log(`Unregistered handler for ${this.protocol}`);
}
catch (err) {
// If it's an UnhandledProtocolError, the protocol is already unregistered
if (err.name === 'UnhandledProtocolError') {
this.log(`Protocol ${this.protocol} already unregistered, skipping`);
}
else {
// Unexpected error, log but don't throw (allow cleanup to continue)
this.log.error(`Error unregistering protocol ${this.protocol}: ${err.message}`);
}
}
this.decoratedConnections.clear();
this.started = false;
this.log('Stopped evm middleware');
}
isStarted() {
return this.started;
}
isDecorated(connection) {
if (!this.started)
return false;
return this.decoratedConnections.has(connection.id);
}
wrappedRulesToSign() {
const toSign = {
rules: this.evmRuleEngine.getRuleDefinitions(),
timestamp: Date.now(),
peerId: this.components.peerId.toString(),
nonce: uuidv7()
};
return JSON.stringify(toSign);
}
handler({ stream, connection }) {
try {
const promise = this.handle(stream, connection);
promise.catch(err => {
this.log('Error handling request', err);
});
}
catch (err) {
this.log('Synchronous error in handler', err);
}
}
// Handle inbound EVM challenge-response requests from clients
async handle(stream, connection) {
this.log('Received evm middleware connection request', connection.id, connection.remotePeer.toString());
try {
const lp = lpStream(stream);
this.log('created lpstream');
const wrappedRules = this.wrappedRulesToSign();
this.log('ping2: wrappedRules', wrappedRules);
try {
this.log('Sending EVM challenge to client');
await lp.write(new TextEncoder().encode(wrappedRules), { signal: AbortSignal.timeout(this.timeout) });
this.log('EVM Challenge sent successfully to client');
}
catch (err) {
this.log.error('Error sending EVM challenge to client:', err.message);
connection.abort(new Error('Error sending EVM challenge to client'));
return;
}
try {
this.log('Reading response to challenge');
const res = await lp.read({ signal: AbortSignal.timeout(this.timeout) });
this.log('Read response', res);
const responseSig = new TextDecoder().decode(res.slice());
const recoveredAddress = verifyMessage(wrappedRules, responseSig);
this.log('Recovered address:', recoveredAddress);
const evalRes = await this.evmRuleEngine.evaluate(recoveredAddress);
if (!evalRes.result) {
this.log('Failed EVM rule evaluation', evalRes);
connection.abort(new Error('Failed EVM rule evaluation'));
return;
}
}
catch (err) {
this.log.error('Error reading response:', err.message);
connection.abort(new Error('Error reading response'));
return;
}
try {
this.decoratedConnections.add(connection.id);
this.log(`Connection ${connection.id} middleware negotiated successfully, sending OK`);
await lp.write(new TextEncoder().encode('OK'), { signal: AbortSignal.timeout(this.timeout) });
this.log('Sent OK to client, closing stream');
// await stream.close()
}
catch (err) {
this.log.error('Error sending challenge to client:', err.message);
connection.abort(new Error('Error sending challenge to client'));
}
}
catch (err) {
this.log.error('Error handling request', err);
}
}
// Authentication methods
async decorate(stream, connection, abortOptions) {
this.log.trace('Decorate attempt for connection:', connection.id);
if (!this.started) {
this.log.error('middleware not started');
return false;
}
// If already authenticated, return true
if (this.isDecorated(connection)) {
this.log('Connection middleware already applied:', connection.id);
return true;
}
// We're going to initiate middleware with the server
// The server will send us a challenge that we need to respond to
this.log(`Initiating middleware for connection ${connection.id}`);
try {
// Open a stream to the remote peer using the EVM protocol
this.log('Opening EVM stream to peer', connection.remotePeer.toString(), 'on protocol', this.protocol);
const authStream = await connection.newStream(this.protocol, { signal: AbortSignal.timeout(this.timeout) });
const lp = lpStream(authStream);
try {
this.log('Waiting to receive challenge from server...');
const challengeBytes = await lp.read({ signal: AbortSignal.timeout(this.timeout) });
const challenge = new TextDecoder().decode(challengeBytes.slice());
this.log(`Received challenge from server: [${challenge}] (length: ${challenge.length})`);
const toSignSchema = z.object({
rules: z.array(ruleDefinitionSchema),
timestamp: z.number().refine((val) => {
const now = Date.now();
return Math.abs(now - val) <= 60000; // within 1 minute (60000 ms)
}, {
message: 'Timestamp must be within ±1 minute of the current time.'
}),
peerId: z.string().refine((val) => {
return val === connection.remotePeer.toString();
}, {
message: 'Peer ID must be a valid PeerId'
}),
nonce: z.string().uuid()
});
const validateRes = toSignSchema.safeParse(JSON.parse(challenge));
if (!validateRes.success) {
this.log.error(validateRes.error);
connection.abort(new Error('challenge failed validation'));
return false;
}
const toSign = validateRes.data;
if (!this.evmRuleEngine.validateRules(toSign.rules)) {
this.log.error('Error validation challenge');
connection.abort(new Error('Error validating challenge'));
return false;
}
const sig = await this.signer.signMessage(challenge);
this.log(`Signed message : ${sig}`);
// Send response to server
try {
await lp.write(new TextEncoder().encode(sig), { signal: AbortSignal.timeout(this.timeout) });
this.log('Response sent successfully to server');
}
catch (err) {
this.log.error('Error sending response to server:', err.message);
connection.abort(new Error('Error sending response to server'));
return false;
}
try {
const isOK = await lp.read({ signal: AbortSignal.timeout(this.timeout) });
this.log('Read challenge ok');
// eslint-disable-next-line max-depth
if (new TextDecoder().decode(isOK.slice()) !== 'OK') {
this.log.error('reading challenge ok failed');
connection.abort(new Error('reading challenge ok failed'));
return false;
}
this.decoratedConnections.add(connection.id);
await authStream.close();
return true;
}
catch (err) {
this.log.error('Error reading response from server:', err.message);
connection.abort(new Error('Error reading response from server'));
return false;
}
}
catch (err) {
this.log.error('Error sending response to server:', err.message);
connection.abort(new Error('Error sending response to server'));
return false;
}
}
catch (err) {
// eslint-disable-next-line no-console
this.log.error('Middleware error for connection', connection.id, err);
connection.abort(err);
return false;
}
}
}
//# sourceMappingURL=middleware-evm.js.map