Skill Branches

Blockchain

Traceable

Non-repudiation

Distribution

Publicity

Mentor / Pro

Student

Signature

Verify

Signature

Story

Newbie

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.

Implementation

Specification

Personal Card

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

Specification

Skill chain (512 bytes)

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

Skill Defined

We have more than 86K space to store skill...

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 ....

Transmission Protocol

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

Source Code

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;

UDP Server

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();
  }
}

PING/PONG

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);
}

PING/PONG

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,
    };
  }
}

Neighbors

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)),
    ]);
  }
}

... to be continued

Skill Branches

By Chia Yu Pai

Skill Branches

  • 406