Traceable
Non-repudiation
Distribution
Publicity
Mentor / Pro
Student
Signature
Verify
Signature
1. Create avatar with email with description.
2. Find someone with skills to signature.
That's all.
In network, every user has:
1. Personal card with email and desc (signed).
2. Skill history chain.
Total Length: ushort (0-65535)
Email Address: email with ASCII encoding
Timestamp: uint millisecond before
Public Address: secp256k1 public key
Name: name with utf8mb4 encoding
Description: description with utf8mb4 encoding
Signature: ecdsa signature with keccak256 hash
Skills: store bool with pre-defined skills (over 86K types)
Signature Address: skill signer secp256k1 public key
Receiver Address: skill receiver secp256k1 public key
Previous Block Hash: last block payload keccak256 hash
Action: signer action like add(0)
Signature: ecdsa signature with keccak256 hash
0: HTML5
1: CSS Layout (block element, inline element)
2: CSS Flexbox
3: CSS Grid Layout
4: JavaScript Prototype
5: JavaScript Class
6: Go - Goroutine
7: Python - Tuple
.
.
.
0x4F 0x03 0xD2 ....
1. UDP based
2. Kademlia based network
3. Broadcast updates
4. TTL based data query
5. LRU cache for timeout management
6. Local cache with level db
export class Block {
static VERSION = Buffer.from('01000000', 'hex');
static ROOT_PUBLIC_KEY = Buffer.from('8c1c9cdbe4768df88a5a40adf6d0caa30adac5b7', 'hex');
static isValid(buffer: Buffer, version?: BlockVersion): boolean {
if (!version) {
return Block.isValid(buffer, buffer.readUInt32LE());
}
switch (version) {
case BlockVersion.ONE:
default: {
const publicKey = buffer.slice(344, 377);
const signature = buffer.slice(448);
const payload = createKeccakHash('keccak256').update(buffer.slice(0, 448)).digest();
return secp256k1.ecdsaVerify(new Uint8Array(signature), new Uint8Array(payload), new Uint8Array(publicKey));
}
}
}
public prevBlock: Buffer;
public timestamp: number;
public skills: Buffer;
public signKey?: Buffer;
public receiver: Buffer;
public action: BlockAction = BlockAction.SIGNATURE;
constructor(options: BlockInitArgs | Buffer) {
if (options instanceof Buffer) {
if (!Block.isValid(options)) throw new Error('Invalid Buffer');
this.receiver = options.slice(377, 410);
this.skills = options.slice(4, 340);
this.timestamp = options.readUInt32LE(340);
this.prevBlock = options.slice(410, 442);
this.action = options.readUInt32LE(442);
} else {
this.signKey = options.signKey;
this.receiver = options.receiver;
this.action = options.action || this.action;
this.prevBlock = options.prevBlock || Buffer.alloc(32);
this.timestamp = Math.round(Date.now() / 1000);
this.skills = options.skills;
}
}
// Total: 512 bytes
// | Version (4) | skills (336) | timestamp (4)
// | signerAddress (33) | receiverAddress (33) | previousBlockHash (32)
// | action (4) | reserved (2) | Signature (64)
toBuffer() {
if (!this.signKey) throw new Error('No Private Key to Sign');
const payloadBuffer = Buffer.alloc(448);
Block.VERSION.copy(payloadBuffer);
this.skills.copy(payloadBuffer, 4);
const timeBuffer = Buffer.alloc(4);
timeBuffer.writeUInt32LE(this.timestamp);
timeBuffer.copy(payloadBuffer, 340);
Buffer.from(secp256k1.publicKeyCreate(this.signKey)).copy(payloadBuffer, 344);
this.receiver.copy(payloadBuffer, 377);
this.prevBlock.copy(payloadBuffer, 410);
const actionBuffer = Buffer.alloc(4);
actionBuffer.writeUInt32LE(this.timestamp);
actionBuffer.copy(payloadBuffer, 442);
const checksumBuffer = createKeccakHash('keccak256').update(payloadBuffer).digest();
const { signature } = secp256k1.ecdsaSign(checksumBuffer, this.signKey);
return Buffer.concat([payloadBuffer, Buffer.from(signature)]);
}
}
export default Block;
class Server {
start() {
this._kBucket = new KBucket({
localNodeId: this._publicKey,
});
this._kBucket.on('added', (newPeer) => {
const peer = newPeer as PeerContact;
const neighbours = this._kBucket?.closest(peer.id, Server.KBUCKET_SIZE);
neighbours?.forEach((neighbour) => {
const neighbourPeer = neighbour as PeerContact;
this._send(neighbourPeer, RequestType.FOUND_NEIGHBOURS, [
Buffer.from([Server.VERSION]),
Server.encodePeer(this.getPeerInfo()),
[Server.encodePeer(peer)],
]);
});
this._debug(this._kBucket?.toArray().length);
});
this._socket = createSocket({ type: 'udp4' });
this._socket.once('listening', () => {
this._debug(`Socket is listening on port ${this._socket?.address().port}`);
this._kBucket?.add(this.getPeerInfo() as PeerContact);
this.emit('listening');
if (options?.initPeers?.length) {
options.initPeers.forEach(async (peer) => {
await this.ping(peer);
await this.findNeighbours(peer);
});
}
});
this._socket.on('message', (message: Buffer, info: RemoteInfo) => {
this._onMessage(message, info);
});
this._socket.bind();
}
}
class Server {
async ping(peer: PeerInfo): Promise<any> {
const cacheKey = `${peer.address}:${peer.udpPort}`;
const ongoingRequest = this._requestsCache.get(cacheKey);
if (ongoingRequest) return ongoingRequest;
const messageId = this._send(peer, RequestType.PING, [
Buffer.from([Server.VERSION]),
Server.encodePeer(this.getPeerInfo()),
Server.encodePeer(peer),
]);
const messageKey = messageId.toString('hex');
const promise = new Promise((resolve, reject) => {
this._requests.set(messageKey, {
peer,
done: resolve,
timeoutId: setTimeout(() => {
if (this._requests.get(messageKey)) {
this._requests.delete(messageKey);
reject(new Error(`Ping timeout: ${peer.address}:${peer.udpPort}`));
}
}, Server.TIMEOUT_MS),
});
});
this._requestsCache.set(cacheKey, promise);
}
}
private _encode(type: RequestType, data: any) {
const fullData = Buffer.concat([Buffer.from([type]), rlpEncode(data)]);
const fullDataHash = keccak256(fullData);
const { recid, signature } = secp256k1.ecdsaSign(fullDataHash, this._signKey);
const signedData = Buffer.concat([Buffer.from(signature), Buffer.from([recid]), fullData]);
const completeHash = keccak256(signedData);
return Buffer.concat([completeHash, signedData]);
}
private _send(peer: PeerInfo, type: RequestType, data: any) {
const message = this._encode(type, data);
this._socket?.send(message, 0, message.length, peer.udpPort!, peer.address);
return message.slice(0, 32);
}
class Server {
private _onPing(data: any, messageId: Buffer, info: RemoteInfo) {
const version = data[0][0];
const peer = Server.decodePeer(data[1]);
if (version !== Server.VERSION) throw new Error('Version not matched!');
if (!this._kBucket?.get(peer.id)) {
this._kBucket?.add({
id: peer.id,
address: info.address,
tcpPort: null,
udpPort: info.port,
} as PeerContact);
}
this._send({
id: peer.id,
address: info.address,
tcpPort: null,
udpPort: info.port,
}, RequestType.PONG, [
Buffer.from([Server.VERSION]),
Server.encodePeer(this.getPeerInfo()),
Server.encodePeer({
id: peer.id,
address: info.address,
tcpPort: null,
udpPort: info.port,
}),
messageId,
]);
}
static decodePeer(buffers: Buffer[]): PeerInfo {
const address = ip.toString(buffers[1]);
const tcpPort = buffer2int(buffers[2]);
const udpPort = buffer2int(buffers[3]);
return {
id: buffers[0],
address,
tcpPort: Number.isNaN(tcpPort) ? null : tcpPort,
udpPort: Number.isNaN(udpPort) ? null : udpPort,
};
}
private _decode(data: Buffer) {
const completeHash = keccak256(data.slice(32));
if (!completeHash.equals(data.slice(0, 32))) throw new Error('Incompleted Data');
const fullData = data.slice(97);
const type = fullData[0] as RequestType;
const fullDataHash = keccak256(fullData);
const signature = data.slice(32, 96);
const recid = data[96];
const publicKey = Buffer.from(secp256k1.ecdsaRecover(signature, recid, fullDataHash, false));
return {
type,
data: rlpDecode(fullData.slice(1)),
publicKey,
};
}
}
class Server {
private _onFindNeighbours(data: any, info: RemoteInfo) {
const version = data[0][0];
if (version !== Server.VERSION) throw new Error('Version not matched!');
const remotePeer = {
id: data[1],
address: info.address,
tcpPort: null,
udpPort: info.port,
};
this._send(remotePeer, RequestType.FOUND_NEIGHBOURS, [
Buffer.from([Server.VERSION]),
Server.encodePeer(this.getPeerInfo()),
this._kBucket?.closest(remotePeer.id, Server.KBUCKET_SIZE)
.map(peer => Server.encodePeer(peer as PeerContact)),
]);
}
}