Написание блокчейна на JavaScript

JavaScript модульный тест биткойн блокчейн

оригинал:Writing a tiny blockchain in JavaScript

Добавить Автора

Переводчик: Дже Левин

Почти все слышали о криптовалютах, таких как Биткойн и Эфириум, но очень немногие понимают лежащую в их основе технологию. В этом блоге я буду использовать JavaScript для создания простой цепочки блоков, чтобы продемонстрировать, как они работают внутри. Я назову его SavjeeCoin!

Полный текст разделен на три части:

  1. часть 1: внедрить базовую цепочку блоков
  2. часть 2: реализация POW
  3. часть 3: Награды за торговлю и майнинг

Часть 1: Внедрение базовой цепочки блоков

блокчейн

Блокчейн — это общедоступная база данных блоков, к которой может получить доступ любой желающий. Может показаться, что в этом нет ничего особенного, но у них есть интересное свойство: они неизменны. Как только блок добавлен в цепочку блоков, его нельзя изменить, если только остальные блоки не будут признаны недействительными.

Вот почему криптовалюты основаны на блокчейне. Вы определенно не хотите, чтобы люди меняли сделки после того, как они закончились!

создать блок

Блокчейны связаны друг с другом множеством блоков (это звучит прекрасно). Блоки в цепочке каким-то образом позволяют нам определить, манипулировал ли кто-нибудь какими-либо предыдущими блоками.

Так как же обеспечить целостность данных? Каждый блок содержит хэш, вычисляемый на основе его содержимого. Он также содержит хэш предыдущего блока.

Вот как класс блока может выглядеть в JavaScript:

const SHA256 = require("crypto-js/sha256");
class Block {
  constructor(index, timestamp, data, previousHash = '') {
    this.index = index;
    this.previousHash = previousHash;
    this.timestamp = timestamp;
    this.data = data;
    this.hash = this.calculateHash();
  }

  calculateHash() {
    return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.data)).toString();
  }
}

Поскольку sha256 не поддерживается в JavaScript, я представилcrypto-jsбиблиотека. Затем я определяю конструктор для инициализации свойств моего блока. назначается каждому блокуindexсвойства, чтобы сообщить нам, где находится этот блок во всей цепочке. Мы также генерируем временную метку и некоторые данные, которые необходимо сохранить в блоке. Последний — это хэш предыдущего блока.

создать цепочку

Теперь мы можем связывать блоки вместе в классе Blockchain! Вот код, реализованный в JavaScript:

class Blockchain{
  constructor() {
    this.chain = [this.createGenesisBlock()];
  }

  createGenesisBlock() {
    return new Block(0, "01/01/2017", "Genesis block", "0");
  }

  getLatestBlock() {
    return this.chain[this.chain.length - 1];
  }

  addBlock(newBlock) {
    newBlock.previousHash = this.getLatestBlock().hash;
    newBlock.hash = newBlock.calculateHash();
    this.chain.push(newBlock);
  }

  isChainValid() {
    for (let i = 1; i < this.chain.length; i++){
      const currentBlock = this.chain[i];
      const previousBlock = this.chain[i - 1];

      if (currentBlock.hash !== currentBlock.calculateHash()) {
        return false;
      }

      if (currentBlock.previousHash !== previousBlock.hash) {
        return false;
      }
    }
    return true;
  }
}

В конструкторе я инициализирую всю цепочку, создавая массив, содержащий блоки генезиса. Первый блок особенный, потому что он не может указывать на предыдущий блок. Я также добавил следующие два метода:

  • getLatestBlock()Возвращает последний блок в нашей цепочке блоков.
  • addBlock()Отвечает за добавление новых блоков в нашу цепочку. Для этого мы добавляем хэш предыдущего блока в наш новый блок. Таким образом, мы можем поддерживать целостность всей цепочки. Потому что всякий раз, когда мы меняем содержимое последнего блока, нам нужно пересчитывать его хэш. Когда вычисления будут завершены, я вставлю блок в цепочку (массив).

Наконец, я создаюisChainValid()чтобы убедиться, что никто не вмешался в блокчейн. Он проходит через все блоки, чтобы проверить правильность хэша каждого блока. это будет сравниватьpreviousHashчтобы убедиться, что каждый блок указывает на правильный предыдущий блок. Если все хорошо, то вернетсяtrueВ противном случае он вернетсяfalse.

Используйте блокчейн

Наш класс блокчейна готов, и мы можем начать его использовать!

let savjeeCoin = new Blockchain();
savjeeCoin.addBlock(new Block(1, "20/07/2017", { amount: 4 }));
savjeeCoin.addBlock(new Block(2, "20/07/2017", { amount: 8 }));

Здесь я только что создал экземпляр блокчейна и назвал его SavjeeCoin! После этого я добавил несколько блоков в цепочку. Блок может содержать любые данные, которые вы хотите, но в приведенном выше коде я решил добавитьamountсвойства объекта.

Попробуйте!

Во введении я сказал, что блокчейн неизменен. После добавления блоки не могут быть изменены. Давай попробуем!

// 检查是否有效(将会返回true)
console.log('Blockchain valid? ' + savjeeCoin.isChainValid());

// 现在尝试操作变更数据
savjeeCoin.chain[1].data = { amount: 100 };

// 再次检查是否有效 (将会返回false)
console.log("Blockchain valid? " + savjeeCoin.isChainValid());

я бы начал с бегаisChainValid()проверить целостность всей цепи. Мы манипулировали любым блоком, поэтому он вернет true.

После этого я изменил данные первого (индекс 1) блока в цепочке. После этого я еще раз проверил целостность всей цепочки и обнаружил, что она вернула false. Вся наша цепочка больше не действует.

В заключение

Этот маленький каштан далеко не закончен. Он еще не внедрил POW (Proof of Work) или сеть P2P для связи с другими майнерами.

Но он доказал, что блокчейн работает. Многие люди думали, что принципы будут очень сложными, но эта статья доказывает, что основные концепции блокчейна очень просты для понимания и реализации.

Часть 2: Внедрение POW (доказательство работы: доказательство работы)

В части 1 мы создали простой блокчейн на JavaScript, чтобы продемонстрировать, как работает блокчейн. Однако эта реализация не завершена, и многие люди считают, что систему все еще можно взломать. Вот так! Нашему блокчейну нужен еще один механизм для защиты от атак. Итак, давайте посмотрим, как мы можем это сделать!

вопрос

Теперь мы можем очень быстро создавать блоки и очень быстро добавлять их в нашу цепочку блоков. Однако это приводит к трем проблемам:

  • Во-первых: люди могут быстро создавать блоки, а затем заполнять нашу цепочку мусором. Большое количество блоков может перегрузить нашу цепочку блоков и сделать ее непригодной для использования.
  • Во-вторых: поскольку создать действительный блок очень просто, можно изменить блок в цепочке, а затем пересчитать хэш всех блоков. Даже если они подделали блок, они все равно могут получить действительный блок.
  • Третье: вы можете эффективно контролировать блокчейн, комбинируя два вышеупомянутых недостатка. Блокчейны питаются от сети p2p, где узлы добавляют блоки в самую длинную доступную цепочку. Таким образом, вы можете вмешиваться в блок, затем подсчитывать все остальные блоки и, наконец, добавлять столько блоков, сколько хотите. Вы получите самую длинную цепочку, и все остальные узлы примут ее и добавят свои собственные блоки.

Очевидно, нам нужно решение этих проблем: POW.

Что такое военнопленный

POW — это механизм, предшествующий созданию первой существовавшей цепочки блоков. Это простой метод предотвращения злоупотреблений путем вычисления определенного числа. Рабочая нагрузка является ключом к предотвращению взлома и заполнения мусором. Если это требует большой вычислительной мощности, то заливать мусором уже не стоит.

Биткойн реализует POW, требуя, чтобы хеш состоял из определенного количества нулей. это также называетсятрудность

Но подождите минутку! Как изменить хэш блока? В случае Биткойна блок содержит различную информацию о финансовых транзакциях. Мы определенно не хотим запутывать эти данные, чтобы получить правильный хэш.

Чтобы решить эту проблему, блокчейн добавляетnonceстоимость. Nonce — это количество раз, используемое для поиска действительного хэша. Более того, поскольку вывод хеш-функции нельзя предсказать, можно попробовать только большое количество комбинаций, прежде чем будет получен хэш, удовлетворяющий условию сложности. Поиск действительного хэша (создание нового блока) называется майнингом в круге.

В случае Биткойна POW гарантирует, что каждые 10 минут можно добавлять только один блок. Вы можете себе представить, сколько вычислительной мощности требуется спамерам для создания нового блока, им будет очень сложно обмануть сеть, не говоря уже о том, чтобы вмешаться во всю цепочку.

Реализовать POW

Как мы можем достичь этого? Давайте сначала изменим наш класс блока и добавьте переменную NONE в своем конструкторе. Я бы инициализирую его и установил его значение 0.

constructor(index, timestamp, data, previousHash = '') {
  this.index = index;
  this.previousHash = previousHash;
  this.timestamp = timestamp;
  this.data = data;
  this.hash = this.calculateHash();
  this.nonce = 0;
}

Нам также нужен новый способ увеличения значения Nonce до тех пор, пока у нас не будет корректного хэша. Опять же, это определяется сложностью. Таким образом, мы получим сложность в качестве параметра.

mineBlock(difficulty) {
    while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
        this.nonce++;
        this.hash = this.calculateHash();
    }
    console.log("BLOCK MINED: " + this.hash);
}

Наконец, нам также нужно изменитьcalculateHash()функция. Потому что он еще не использовал Nonce для вычисления хеша.

calculateHash() {
  return SHA256(this.index +
    this.previousHash +
    this.timestamp +
    JSON.stringify(this.data) +
    this.nonce
  ).toString();
}

Объединив их вместе, вы получите такой блок-класс:

class Block {
  constructor(index, timestamp, data, previousHash = '') {
    this.index = index;
    this.previousHash = previousHash;
    this.timestamp = timestamp;
    this.data = data;
    this.hash = this.calculateHash();
    this.nonce = 0;
  }

  calculateHash() {
    return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).toString();
  }

  mineBlock(difficulty) {
    while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
      this.nonce++;
      this.hash = this.calculateHash();
    }
    console.log("BLOCK MINED: " + this.hash);
  }
}

Изменить блокчейн

Теперь, когда у нашего блока есть одноразовый номер и его можно майнить, нам также нужно убедиться, что наш блокчейн поддерживает это новое поведение. Давайте начнем с добавления нового свойства в блокчейн для отслеживания сложности всей цепочки. Я бы установил его на 2 (что означает, что хэш блока должен начинаться с 2 0).

constructor() {
  this.chain = [this.createGenesisBlock()];
  this.difficulty = 2;
}

Теперь все, что осталось сделать, это изменитьaddBlock()чтобы убедиться, что блок действительно добыт, прежде чем добавить его в цепочку. Далее мы передаем сложность блоку.

addBlock(newBlock) {
  newBlock.previousHash = this.getLatestBlock().hash;
  newBlock.mineBlock(this.difficulty);
  this.chain.push(newBlock);
}

Готово! Наш блокчейн теперь имеет POW для защиты от атак.

тестовое задание

Теперь давайте протестируем наш блокчейн и посмотрим, какой эффект даст добавление нового блока под POW. Я буду использовать предыдущий код. Мы создадим новый экземпляр блокчейна и добавим в него 2 блока.

let savjeeCoin = new Blockchain();

console.log('Mining block 1');
savjeeCoin.addBlock(new Block(1, "20/07/2017", { amount: 4 }));

console.log('Mining block 2');
savjeeCoin.addBlock(new Block(2, "20/07/2017", { amount: 8 }));

Если вы запустите приведенный выше код, вы обнаружите, что добавление новых блоков по-прежнему происходит очень быстро. Это потому, что текущая сложность всего 2 (или ваш компьютер очень мощный).

Если вы создадите экземпляр блокчейна со сложностью 5, вы обнаружите, что вашему компьютеру потребуется около десяти секунд для майнинга. Чем выше сложность, тем более защищены ваши защитные атаки.

Отказ от ответственности

Как было сказано ранее: это ни в коем случае не полный блокчейн. Ему по-прежнему не хватает многих функций (например, сети P2P). Это просто для иллюстрации того, как работает блокчейн.

И еще: майнинг с помощью JavaScript не быстрый из-за однопоточности.

Часть 3 Награды за торговлю и майнинг

В первых двух частях мы создали простую цепочку блоков и добавили POW для защиты от атак. Однако по пути мы также обленились: наш блокчейн может хранить только одну транзакцию в блоке, а вознаграждения для майнеров нет. Теперь давайте это исправим!

Класс блока рефакторинга

Теперь блок имеетindex,previousHash,timestamp,data,hashа такжеnonceАтрибуты. этоindexСвойства не очень полезны, на самом деле я даже не знаю, зачем я их добавил. Поэтому я удалил его и добавилdataпереименовать вtransactionsбыть более смысловым.

class Block{
  constructor(timestamp, transactions, previousHash = '') {
    this.previousHash = previousHash;
    this.timestamp = timestamp;
    this.transactions = transactions;
    this.hash = this.calculateHash();
    this.nonce = 0;
  }
}

Когда мы меняем класс блока, мы также должны изменитьcalculateHash()функция. Он по-прежнему использует старыйindexа такжеdataАтрибуты.

calculateHash() {
  return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();
}

Класс транзакции

Внутри блока мы сможем хранить несколько транзакций. Так что нам также нужно определить класс транзакции, при этом мы можем заблокировать свойства, которые должны быть у транзакции:

class Transaction{
  constructor(fromAddress, toAddress, amount){
    this.fromAddress = fromAddress;
    this.toAddress = toAddress;
    this.amount = amount;
  }
}

Этот пример транзакции очень прост и включает только инициатора (fromAddress) и получатель (toAddress) и количество. Вы также можете добавить дополнительные поля, если это необходимо, но это только для минимальной реализации.

Настройте наш блокчейн

Самая большая задача на данный момент: адаптировать наш блокчейн к этим новым изменениям. Первое, что нам нужно, это место для хранения ожидающих транзакций.

Как вы знаете, блокчейн может стабильно создавать блоки благодаря POW. В случае Биткойна сложность устанавливается таким образом, чтобы создавать новый блок примерно каждые 10 минут. Однако можно отправлять новые транзакции между созданием двух блоков.

Для этого нам сначала нужно изменить конструктор нашего блокчейна, чтобы он мог хранить отложенные транзакции. Мы также создадим новое свойство, которое определяет, сколько денег майнеры получают в качестве вознаграждения:

class Blockchain{
  constructor() {
    this.chain = [this.createGenesisBlock()];
    this.difficulty = 5;

    // 在区块产生之间存储交易的地方
    this.pendingTransactions = [];

    // 挖矿回报
    this.miningReward = 100;
  }
}

Далее мы настроим нашaddBlock()метод. Но моя настройка означает удаление и перезапись! Мы больше не позволим людям добавлять блоки напрямую в цепочку. Вместо этого они должны добавить транзакцию в следующий блок. и мы будемaddBlock()Переименовать вcreateTransaction(), что выглядит более семантически:

createTransaction(transaction) {
  // 这里应该有一些校验!

  // 推入待处理交易数组
  this.pendingTransactions.push(transaction);
}

добыча

Теперь люди могут добавлять новые транзакции в список ожидающих транзакций. Но в любом случае нам нужно очистить их и переместить в настоящие блоки. Для этого создадимminePendingTransactions()метод. Этот метод не только добывает все новые блоки, ожидающие транзакции, но и отправляет вознаграждение майнерам.

minePendingTransactions(miningRewardAddress) {
  // 用所有待交易来创建新的区块并且开挖..
  let block = new Block(Date.now(), this.pendingTransactions);
  block.mineBlock(this.difficulty);

  // 将新挖的看矿加入到链上
  this.chain.push(block);

  // 重置待处理交易列表并且发送奖励
  this.pendingTransactions = [
      new Transaction(null, miningRewardAddress, this.miningReward)
  ];
}

Обратите внимание, что метод принимает параметрminingRewardAddress. Если вы начнете майнить, вы можете передать адрес своего кошелька этому методу. После успешного майнинга система создаст новую транзакцию, чтобы дать вам вознаграждение за майнинг (в данном случае 100 монет).

Следует отметить, что в этом примере мы добавляем все ожидающие транзакции в блок. Но на практике, поскольку размер блока ограничен, это не работает. В биткойне размер блока составляет около 2 МБ. Если есть больше транзакций, которые могут втиснуться в блок, то майнеры могут выбирать, какие транзакции совершать, а какие нет (обычно выигрывает более высокая комиссия).

Баланс адреса

После тестирования нашего кода деньги давайте сделаем еще одну вещь! Было бы лучше иметь возможность проверять баланс адресов в нашем блокчейне.

getBalanceOfAddress(address){
  let balance = 0; // you start at zero!

  // 遍历每个区块以及每个区块内的交易
  for(const block of this.chain){
    for(const trans of block.transactions){

      // 如果地址是发起方 -> 减少余额
      if(trans.fromAddress === address){
        balance -= trans.amount;
      }

      // 如果地址是接收方 -> 增加余额
      if(trans.toAddress === address){
        balance += trans.amount;
      }
    }
  }

  return balance;
}

тестовое задание

Что ж, мы закончили и наконец-то можем посмотреть, все ли работает! Для этого создадим несколько транзакций:

let savjeeCoin = new Blockchain();

console.log('Creating some transactions...');
savjeeCoin.createTransaction(new Transaction('address1', 'address2', 100));
savjeeCoin.createTransaction(new Transaction('address2', 'address1', 50));

Эти транзакции в настоящее время находятся на рассмотрении, и для их подтверждения мы должны начать майнинг:

console.log('Starting the miner...');
savjeeCoin.minePendingTransactions('xaviers-address');

Когда мы начинаем майнить, мы также передаем адрес, по которому хотим получить вознаграждение за майнинг. В данном случае мой адресxaviers-address(очень сложно!).

После этого проверимxaviers-addressостаток на счету:

console.log('Balance of Xaviers address is', savjeeCoin.getBalanceOfAddress('xaviers-address'));
// 输出: 0

Вывод моего аккаунта оказался 0? ! Подождите, почему? Разве я не должен получать вознаграждение за майнинг? Что ж, если вы внимательно посмотрите на код, вы увидите, что система создает транзакцию, а затем добавляет вознаграждение за майнинг в качестве новой отложенной транзакции. Эта транзакция будет включена в следующий блок. Так что, если мы снова начнем майнить, мы получим награду в 100 монет!

console.log('Starting the miner again!');
savjeeCoin.minePendingTransactions("xaviers-address");

console.log('Balance of Xaviers address is', savjeeCoin.getBalanceOfAddress('xaviers-address'));
// 输出: 100

Ограничения и выводы

Теперь наша цепочка блоков может хранить несколько транзакций в блоке и генерировать вознаграждение для майнеров.

Тем не менее, есть некоторые недостатки: отправка валюты заключается в том, что мы не проверяем, достаточно ли у отправителя баланса для фактического совершения транзакции. Однако на самом деле сделать это несложно. Мы также не создавали новый кошелек и не подписывали транзакции (традиционно это делается с помощью шифрования с открытым/закрытым ключом).

Отказ от ответственности и исходный код

Я хотел бы отметить, что это ни в коем случае не полная реализация блокчейна! Ему по-прежнему не хватает многих функций. Это всего лишь доказательство концепции, которое поможет вам понять, как работает блокчейн.

Исходный код проекта размещен у меняGitHub