Quorum blockchain: интеграция в Java код +6


Здраствуй, Хабр!


Для тех кто интересуется темой блокчейна давно не секрет, что помимо публичных блокчейнов, таких как Ethereum, Bitcoin, Zcash и т.д., существуют также их "enterprise (private)" "братья", которые в кое чем лучше чем публичные сети, а в кое чем проигрывают им. Среди найболее известных сетей, я думаю, можно назвать Quorum (вендор — J.P. Morgan Chase), Pantheon (вендор — PegaSys) и Hyperledger (находиться под управлением The Linux Foundation). Несмотря на то, что существует довольно много публичных решений, бизнес все более интересуеться именно приватными блокчейнами благодаря тому, что они способны обеспечить нужный уровень приватности, транзакции выполняються быстрее и так далее. Отличия приватных от публичных блокчейнов, а также их преимущества и недостатки — не тема даной статьи. Если вам интересно прочитать об этом то есть, например, такая статья на Medium.


В этой статье я хотел бы расказать вам как можно испольовать Quorum блокчейн для разработки своих приложений с поддержкой приватных и публичных транзакций. Для демонстрации возможностей мы напишем небольшое Java/Spring приложение, которое будет принимать запросы на загрузку (deploy) смарт-контрактов, выполнение транзакций и чтения данных из смарт-контракта. Собственно, вот стек технологий который будет использоваться в статье:


  • Java 8
  • Gradle 5.2.1
  • Spring Framework
  • Web3j (библиотека для работы с публичными транзакциями в Ethereum подобных сетях)
  • Web3j-quorum (библиотека для работы с приватными транзакциями в Quorum сети)

Немного общей информации о Quorum


Quorum — проект с окрытым исходным кодом на GitHub, целью которого является предоставление блокчейна который давал бы возможность выполнять транзакции не только публично но и в приватном режиме тоже. С технической точки зрения Quorum это модернизирований Ethereum, у него есть также и свой модифицированый Geth-клиент чтобы иметь возможность делать приватные транзакции.


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


  1. Constellation — написан на Haskell, первый вариант enclave, однако сейчас уже не развиваеться, и скорее всего в будущем от него откажуться в пользу нового;
  2. Tessera — новый сервис, написан на Java, поддерживаеться разработчиками из J.P. Morgan Chase, имеет больше возможностей по интеграции с БД и управлением "чуствительной" информацией (например, есть вариант итеграции с HashiCorp Vault для управления секретами).

Что касаеться транзакций, то с точки зрения интерфейса обычной Ethereum поменялось не многое (и это хорошо). Для того что бы отправить приватную транзакцию, помимо обычной информации о транзакции, нужно ещё указать параметр privateFor — это масив строк, а строки эти — публичные ключи enclave нод. С помощью этих ключей шифруеться payload транзакции и выполняеться распространение payoad'a между Tessera-нодами внутри блокчейна.


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


Разработка Java приложения


Как пример, я покажу небольшой RESTful API, написаный на Java/Spring, с Gradle в качестве инструмента сборки и управления зависимостями, который будет загружать смарт-контракт в блокчейн, выполнять функцию изменения состояния контракта и считывать состояние из смарт-контракта.


Перед тем, как начать непосредствено разработку, я должен кое что прояснить. Несмотря на то, что в Quorum официально есть 2 варианта транзакций, я предпочитаю делить их на 3 вида:


  1. Public транзакции — транзакции полностью видны всем учасникам сети (в том числе и payload), enclave-нода не принимает участвие ни в оброботке, ни в хранении транзакции. Публичные транзакции в Quorum не отличаються от транзакций в Ethereum сети;
  2. Permissioned транзакции — транзакции по сути являються приватными, но для нескольких учасников сети, то есть в публичной сети мы имеем информацию о транзакции и о статусе ее выполнения, но вместо реального payload в публичной сети мы имеет лишь 64-битную строку-хеш, которая являеться идентификатором на реальный payload в enclave ноде, сама enclave нода отвествена за подпись, шифрование, хранение и распространение payload'a между указаными учасниками транзакции;
  3. Private транзакции — отличаються от permissioned тем что транзакция доступна только для ноды, которая эту транзакцию создала, другие учасники сети не могут увидеть payload транзакции.
    Этой класификацией я буду пользоваться на протяжении всей статьи.

Для начала я покажу, как будет выглядеть файл сборки — gradle.build:


plugins {
    id 'org.springframework.boot' version '2.1.6.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.github'
version = '1.0'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

test {
    testLogging.showStandardStreams = true
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'

    implementation group: 'org.web3j', name: 'quorum', version: '4.0.6'
    implementation group: 'org.web3j', name: 'core', version: '4.1.0'
    implementation group: 'org.web3j', name: 'codegen', version: '4.1.0'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

task generateWrappers(type: JavaExec) {
    group 'Demo'
    description 'Generates wrappers for smart-contracts'

    classpath = sourceSets.main.runtimeClasspath
    main = 'com.github.quorum.utils.WrappersGenerator'
}

Немного обьяснений:


  1. org.web3j.core — зависимость для работы с транзакциями в сети Ethereum и публичными транзакциями в сети Quorum
  2. org.web3j.quorum — зависимость для работы с приватными транзакциями в сети Quorum
  3. org.web3j.codegen — зависимость для генерации wrapper'ов для Solidity смарт-контрактов
  4. generateWrappers — Gradle-task для генерации Java-wrapper'ов из Solidity смарт котрактов

Далее я покажу вам код смарт-контракта, который будет использоваться в этой статье: файл QuorumDemo.sol:


pragma solidity 0.5.0;

/**
 * @dev Smart-Contract for demonstration purposes.
 */
contract QuorumDemo {

    string public user;

    /**
     * @dev Rewrite user name in storage.
     */
    function writeUser(string calldata _user) public {
        user = _user;
    }
}

Контракт намерено сделан простым, но его достаточно для целей нашей статьи. Если вы знаете Solidity, то вы можете пропустить объяснения:


  • string public user — публичная переменая типа string и названием user. В отличии от Java, Solidity автоматически генерирует getter для публичных переменных, поэтому реализововать вручную его не нужно.
  • function writeUser(...) — функция смены значения переменой, по сути — setter.

Для того, что бы из смарт-контракта создать Java-wrapper, нужно положить файл в папку src/main/solidity/contracts с любым названием, например QuorumDemo.sol.
Далее нужно запустить Gradle-task generateWrappers командой:


gradle generateWrappers

после выполнения этой задачи по адресу src/main/java/com/github/quorum/component/wrappers будет создан Java-wrapper, с которым уже можно работать в Java коде.


Для того, чтобы бекенд имел возможность подписывать транзакции, нам нужно иметь возможность получить payload транзакции до того, как мы пошлём ее. Для этого было бы хорошо получить его прямо из Java-wrapper класса. Здесь я создал 2 метода внутри врапера. Первый метод просто возращает ABI контракта, который можно использовать для загрузки нового смарт-контракта. Второй метод — это формирование транзакции на изменение состояния смарт-контракта. Вот код этих методов:


public static String getBinary() {
    return BINARY;
}

public static String getDataOnWriteUser(final String user) {
    final Function function = new Function(
            FUNC_WRITEUSER,
            Arrays.asList(new Utf8String(user)),
            Collections.emptyList()
    );
    return FunctionEncoder.encode(function);
}

Вставив их в сгенерированый Java-wrapper, вы сможете получать payload для транзакций.


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


  1. TesseraTransactionManager, для отправки приватных транзакций
  2. GethTransactionManager, для отправки публичных транзакций

Давайте разберём их. Код TesseraTransactionManager:


@Slf4j
public class TesseraTransactionManager implements TransactionManager {

    private static final byte ATTEMPTS = 20;
    private static final int SLEEP_DURATION = 100;

    private final Quorum quorum;
    private final String fromAddress;
    private final QuorumTransactionManager quorumTxManager;
    private final TransactionReceiptProcessor txReceiptProcessor;

    public TesseraTransactionManager(
            Quorum quorum,
            Credentials credentials,
            String publicKey,
            List<String> privateFor,
            Tessera tessera
    ) {
        this.quorum = quorum;
        this.fromAddress = credentials.getAddress();
        this.quorumTxManager = new QuorumTransactionManager(quorum, credentials, publicKey, privateFor, tessera);
        this.txReceiptProcessor = new PollingTransactionReceiptProcessor(quorum, SLEEP_DURATION, ATTEMPTS);
    }

    @Override
    public TransactionReceipt executeTransaction(
            final BigInteger gasPrice, final BigInteger gasLimit, final String to, final String data) {

        while (true) {
            try {
                final EthSendTransaction ethSendTx = sendTransaction(gasPrice, gasLimit, to, data);

                if (ethSendTx.hasError() && NONCE_TOO_LOW_ERROR_MESSAGE.equals(ethSendTx.getError().getMessage())) {
                    log.warn("[BLOCKCHAIN] try to re-send transaction cause error {}", ethSendTx.getError().getMessage());
                    continue;
                }
                return processResponse(ethSendTx);

            } catch (TransactionException ex) {
                log.error("[BLOCKCHAIN] exception while receiving TransactionReceipt from Quorum node", ex);
                throw new RuntimeException(ex);
            } catch (Exception ex) {
                log.error("[BLOCKCHAIN] exception while sending transaction to Quorum node", ex);
                throw new RuntimeException(ex);
            }
        }
    }

    private EthSendTransaction sendTransaction(
            final BigInteger gasPrice, final BigInteger gasLimit, final String to, final String data) throws IOException {

        final BigInteger nonce = getNonce();
        final RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit, to, data);

        return this.quorumTxManager.signAndSend(rawTransaction);
    }

    private TransactionReceipt processResponse(final EthSendTransaction transactionResponse)
            throws IOException, TransactionException {
        if (transactionResponse.hasError()) {
            throw new RuntimeException(
                    "[BLOCKCHAIN] error processing transaction request: "
                    + transactionResponse.getError().getMessage()
            );
        }

        final String transactionHash = transactionResponse.getTransactionHash();

        return this.txReceiptProcessor.waitForTransactionReceipt(transactionHash);
    }

    private BigInteger getNonce() throws IOException {
        final EthGetTransactionCount ethGetTxCount = this.quorum.ethGetTransactionCount(
                this.fromAddress, DefaultBlockParameterName.PENDING).send();
        return ethGetTxCount.getTransactionCount();
    }
}

  • TransactionReceipt executeTransaction(...) — реализация интерфейса, метод для выполнения транзакции в сети и обработки ошибок, если они случаються. Возращает обьект c результатом выполнения транзакции;
  • EthSendTransaction sendTransaction(...) — метод для подписи и отправки транзакций в блокчейн. Возращает обект с статусом транзакции и его хешом;
  • TransactionReceipt processResponse(...) — метод, который ожидает выполнения транзакции и возращает TransactionReceipt после ее выполнения;
  • BigInteger getNonce() — возращает "nonce" из сети.

И код GethTransactionManager:


@Slf4j
public class GethTransactionManager extends FastRawTransactionManager implements TransactionManager {

    private static final byte ATTEMPTS = 20;
    private static final int SLEEP_DURATION = 100;

    private final TransactionReceiptProcessor txReceiptProcessor;

    public GethTransactionManager(Web3j web3j, Credentials credentials) {
        this(web3j, credentials, new PollingTransactionReceiptProcessor(web3j, SLEEP_DURATION, ATTEMPTS));
    }

    public GethTransactionManager(Web3j web3j, Credentials credentials, TransactionReceiptProcessor txReceiptProcessor) {
        super(web3j, credentials, txReceiptProcessor);
        this.txReceiptProcessor = txReceiptProcessor;
    }

    @Override
    public TransactionReceipt executeTransaction(
            final BigInteger gasPrice, final BigInteger gasLimit, final String to, final String data) {

        while (true) {
            try {
                final EthSendTransaction ethSendTx = sendTransaction(gasPrice, gasLimit, to, data, BigInteger.ZERO);

                if (ethSendTx != null && ethSendTx.hasError() && NONCE_TOO_LOW_ERROR_MESSAGE.equals(ethSendTx.getError().getMessage())) {
                    log.warn("[BLOCKCHAIN] try to re-send transaction cause error: {}", ethSendTx.getError().getMessage());
                    continue;
                }

                return this.txReceiptProcessor.waitForTransactionReceipt(ethSendTx.getTransactionHash());

            } catch (TransactionException ex) {
                log.error("[BLOCKCHAIN] exception while receiving TransactionReceipt from Quorum node", ex);
                throw new RuntimeException(ex);

            } catch (IOException ex) {
                log.error("[BLOCKCHAIN] exception while sending transaction to Quorum node", ex);
                throw new RuntimeException(ex);
            }
        }
    }

    @Override
    public EthSendTransaction sendTransaction(
            final BigInteger gasPrice,
            final BigInteger gasLimit,
            final String to,
            final String data,
            final BigInteger value
    ) throws IOException {
        return super.sendTransaction(gasPrice, gasLimit.add(BigInteger.valueOf(21_000L)), to, data, value);
    }
}

  • TransactionReceipt executeTransaction(...) — реализация интерфейса, метод для выполнения транзакции в сети и обработки ошибок, если они случаються. Возращает обьект c результатом выполнения транзакции;
  • EthSendTransaction sendTransaction(...) — метод, который вызывает метод супер-класа для отправки транзакции в блокчейн.

Обработчик запросов, которые приходять на API:


@Slf4j
@Component
public class RequestHandler {

    private final Web3j web3j;

    private final Quorum quorum;

    private final Tessera tessera;

    private final Credentials credentials;

    private final BlockchainConfig blockchainConfig;

    private String deployedContract;

    @Autowired
    public RequestHandler(
            @Qualifier("initWeb3j") Web3j web3j,
            Quorum quorum,
            Tessera tessera,
            Credentials credentials,
            BlockchainConfig blockchainConfig
    ) {
        this.web3j = web3j;
        this.quorum = quorum;
        this.tessera = tessera;
        this.credentials = credentials;
        this.blockchainConfig = blockchainConfig;
    }

    /**
     * Deploy new smart-contract.
     *
     * @param serverRequest
     *          - {@link ServerRequest} object with request information
     * @return {@link ServerResponse} object with response data
     */
    public Mono<ServerResponse> deployContract(final ServerRequest serverRequest) {
        return serverRequest
                .bodyToMono(APIRequest.class)
                .map(this::getTransactionManager)
                .map(this::deployContract)
                .flatMap(this::generateResponse);
    }

    private TransactionManager getTransactionManager(final APIRequest apiRequest) {
        log.info("[HANDLER] privateFor = {}", apiRequest.getPrivateFor());
        TransactionManager txManager;
        if (isPrivate(apiRequest.getPrivateFor())) {
            if (apiRequest.getPrivateFor().size() == 0) {
                apiRequest.getPrivateFor().add(this.blockchainConfig.getTesseraPublicKey());
            }
            txManager = new TesseraTransactionManager(this.quorum, this.credentials, this.blockchainConfig.getTesseraPublicKey(), apiRequest.getPrivateFor(), this.tessera);
        } else {
            txManager = new GethTransactionManager(this.web3j, this.credentials);
        }

        return txManager;
    }

    private boolean isPrivate(final List<String> limitedTo) {
        return limitedTo == null || limitedTo.size() == 0 || !limitedTo.get(0).equals("public");
    }

    private APIResponse deployContract(final TransactionManager txManager) {
        log.info("[HANDLER] deploying new smart-contract");
        final String data = QuorumDemo.getBinary();
        final TransactionReceipt txReceipt = txManager.executeTransaction(GAS_PRICE, DEPLOY_GAS_LIMIT, null, data);
        final APIResponse apiResponse = APIResponse.newInstance(txReceipt);
        this.deployedContract = txReceipt.getContractAddress();
        log.info("[HANDLER] contract has been successfully deployed. Result: {}", apiResponse.getMap());

        return apiResponse;
    }

    private Mono<ServerResponse> generateResponse(final APIResponse apiResponse) {
        return ServerResponse
                .ok()
                .body(Mono.just(apiResponse.getMap()), Map.class);
    }

    /**
     * Send transaction on update user in smart-contract.
     *
     * @param serverRequest
     *          - {@link ServerRequest} object with request information
     * @return {@link ServerResponse} object with response data
     */
    public Mono<ServerResponse> updateUser(final ServerRequest serverRequest) {
        return serverRequest
                .bodyToMono(APIRequest.class)
                .map(this::sendTransaction)
                .flatMap(this::generateResponse);
    }

    private APIResponse sendTransaction(final APIRequest apiRequest) {
        final TransactionManager txManager = getTransactionManager(apiRequest);
        log.info("[HANDLER] sending new transaction");
        final String data = QuorumDemo.getDataOnWriteUser(apiRequest.getUser());
        final TransactionReceipt txReceipt = txManager.executeTransaction(GAS_PRICE, TX_GAS_LIMIT, this.deployedContract, data);
        final APIResponse apiResponse = APIResponse.newInstance(txReceipt);
        log.info("[HANDLER] transaction has been successfully executed. Result: {}", apiResponse.getMap());

        return apiResponse;
    }

    /**
     * Read user from smart-contract.
     *
     * @param serverRequest
     *          - {@link ServerRequest} object with request information
     * @return {@link ServerResponse} object with response data
     */
    public Mono<ServerResponse> getUser(final ServerRequest serverRequest) {
        final APIResponse apiResponse = getUser();
        return generateResponse(apiResponse);
    }

    private APIResponse getUser() {
        log.info("[HANDLER] reading user from smart-contract");
        final QuorumDemo quorumDemo = QuorumDemo.load(this.deployedContract, this.web3j, this.credentials, new StaticGasProvider(GAS_PRICE, DEPLOY_GAS_LIMIT));
        final String user = readUserFromSmartContract(quorumDemo);
        final APIResponse apiResponse = APIResponse.newInstance(user);
        log.info("[HANDLER] user: '{}'", user);

        return apiResponse;
    }

    private String readUserFromSmartContract(final QuorumDemo quorumDemo) {
        try {
            return quorumDemo.user().send().getValue();
        } catch (Exception ex) {
            log.info("[HANDLER] exception while reading user from smart-contract: {}", ex);
            return null;
        }
    }
}

Сейчас обьясню какие методы за что отвечают.
Метод Mono<ServerResponse> deployContract(...) — описывает общую логику загрузки (deploy) смарт-контракта, как публичного так и приватного.
Метод TransactionManager getTransactionManager(...) — возращает обьект реализации менеджера транзакций в зависимости от типа транзакций. Для этого в теле запроса будет находиться параметр privateFor, который являеться масивом строк публичный ключей Tessera нод.
Метод boolean isPrivate(...) — возращает "true", если параметр privateFor или пустой (private транзакция), или имеет список публичных ключей (permissioned транзакция). Возращает "false", если параметр privateFor не пустой, и первый елемент масива равен "public".
Метод APIResponse deployContract(...) — посылает транзакцию загрузки(deploy) в блокчейн.
Метод Mono<ServerResponse> generateResponse(...) — генерирует обьект с ответом к клиенту.
Метод Mono<ServerResponse> updateUser(...) — описывает общую логику выполнения транзакции на изменение состояния смарт-контракта.
Метод APIResponse sendTransaction(...) — посылает транзакцию на изменение состояния в блокчейн.
Метод APIResponse getUser() — описывает общую логику счытивания информации из смарт-контракта и возращает ответ клиенту.
Метод String readUserFromSmartContract(...) — считывает состояние из смарт-контракта и возращает результат.


Полный код приложение доступен в GitHub репозитории, ссылка на который будет вконце этой статьи.


Проверка


Для тестирования всех 3 типов транзакций я написал тестовые класы (код находиться в GitHub репозитории). Для этого я развернул блокчейн с 3 Quorum нодами (3 ноды Geth + 3 ноды Tessera). 3 Quorum ноды это минимум нод необходимых для проверки всех типов транзакций. Имейте это ввиду, если захотите попробовать сами.


Public транзакции


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


gradle test --tests *.PublicTransactionsTests

Этот тест кейс пошлёт 3 запроса на API. Первый на деплой смарт-контракта в блокчейн, второй — изменение состояние контракта и третий запрос — считывание информации из смарт-контракта. В результате выполнения теста вы увидите примерно такие логи (адреса в вашей сети будут отличаться, как и хеши транзакций):


[HANDLER] privateFor = [public]
[HANDLER] deploying new smart-contract
[HANDLER] contract has been successfully deployed. Result: {contract_address=0xf9425b94e459805da09950f5988071692d925097, transaction_hash=0x31bc179f8cd12c640d1663f3df51ce6da1fbc2875f2b724c3911108fcd19a5d0}
[HANDLER] privateFor = [public]
[HANDLER] sending new transaction
[HANDLER] transaction has been successfully executed. Result: {contract_address=null, transaction_hash=0x33ba66d5deec33f3142bfa190a0d37d0ff07c2e66b06037f5b5ff9578154a3ff}
[HANDLER] reading user from smart-contract
[HANDLER] user: 'Public Test User'

В общем, эти логи говорят о том, что все 3 операции прошли успешно. Первые 3 лога — принадлежат запросу на деплой смарт-контракта, следущее 3 лога — принадлежат выполнению транзакции, а последние 2 — считыванию информации из смарт-контракта.
То, что в по итогу загрузки контракта мы видим contract_address, а в случае простой транзакции — нет, это вполне нормально, так как во второй раз мы не деплоим контракт, а выполняем транзакцию на уже существующий смарт-контракт.


Теперь давайте проверим, что нам показывает Geth, выполняем следущую команду для подключения к IPC интерфейсу процеса Geth клиента:


geth attach /path/to/ipc

После того, как мы "приатачились" к процесу, можно пересматривать полностю всю необходимую информацию. Давайте посмотрим на TransactionReceipt транзакции на деплой нового смарт-контракта, выполнив команду (хеш транзакции нужно поставить свой, и взять его из логов теста):


web3.eth.getTransactionReceipt('0x31bc179f8cd12c640d1663f3df51ce6da1fbc2875f2b724c3911108fcd19a5d0');

В качестве результата видим следущее:



Нас интересуют следущие параметры:


  • "contractAddress" — если не "null", то мы понимаем, что это транзакция на деплой смарт-контракта;
  • "status" — в даном случае оно равно "0x1" — что означает, что транзакция прошла успешно.

И давайте посмотрим на саму транзакцию. Выполнив команду:


web3.eth.getTransaction('0x31bc179f8cd12c640d1663f3df51ce6da1fbc2875f2b724c3911108fcd19a5d0');

Результат:



Здесь нас интересую следущие параметры:


  • "input" — это payload транзакции;
  • "v" — в общем, это параметр для ECDSA, алгоритма цифровой подписи, но нас интересует сейчас другое — значение переменой. Важно оно потому, что у публичных и приватных транзакций оно будет отличаться. "0x1c" ("28" в десятичной системе) и "0x1b" ("27" в десятичной системе) характерны для публичных транзакций, а "0x25" ("37" в десятичной системе) и "0x26" ("38" в десятичной системе) — это коды приватных транзакций.

Можете также проверить, что на других нодах информация не отличаеться от той, что мы увидили сейчас.


Теперь можно просмотреть на транзакцию изменения состояния смарт-контракта. Выполним команду:


web3.eth.getTransactionReceipt('0x33ba66d5deec33f3142bfa190a0d37d0ff07c2e66b06037f5b5ff9578154a3ff');

Результат:



Нас интересуют следущие параметры:


  • "to" — видим, что транзакция пошла на только-что задеплоиный смарт-контракт;
  • "status" — оно равно "0x1", что означает, что транзакция прошла успешно.

Транзакция:



Ничего необычного, но можете проверить информацию на других нодах, это полезно.


Private транзакции


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


gradle test --tests *.PrivateTransactionsTests

Также, как в тест-кейсе с публичными транзакциями, этот тест-кейс задеплоит новый смарт-контракт, выполнит транзакцию на смену состояния и счытает информацию из переменой в смарт-контракте.


В результате програма напишет такие логи:


[HANDLER] privateFor = []
[HANDLER] deploying new smart-contract
[HANDLER] contract has been successfully deployed. Result: {contract_address=0x3e2284d92842f781b83cc7e56fbb074ab15f9a90, transaction_hash=0x8fd619bd9a526f83e29d7b417551e174862f7503ef430eb45793509d05039595}
[HANDLER] privateFor = []
[HANDLER] sending new transaction
[HANDLER] transaction has been successfully executed. Result: {contract_address=null, transaction_hash=0x72a0458a7b313c8a1c18269ae160e140c6a6e41cb2fd087c64cf665b08a6aefb}
[HANDLER] reading user from smart-contract
[HANDLER] user: 'Private Test User'

Изменением, по сравнению с публичными транзакциями, есть параметр privateFor — теперь он имеет значение пустого масива.
Давайте проверим TransactionReceipt по транзакции. Команда:


web3.eth.getTransactionReceipt('0x8fd619bd9a526f83e29d7b417551e174862f7503ef430eb45793509d05039595');

Результат:



Из изменений, по сравнению с публичными транзакциями, стоит сказать, что вы не увидите количество газа, потраченое на выполнение транзакции — gasUsed и cumulativeGasUsed имеют значение "0".
А теперь давайте посмотрим на саму транзакцию. Выполним команду:


web3.eth.getTransaction('0x8fd619bd9a526f83e29d7b417551e174862f7503ef430eb45793509d05039595');

В результате увидим такое:



Что стоит отметить в даной транзакции:


  1. Как я уже упоминал в начале этой статьи, вместо реального payload транзакции вы увидите фиксированую строку в 64 байта (128 символов) в поле input. Эта строка — идентификатор на данные в хранилище Tessera, реальные данные вы можете получить по запросу к Tessera.
  2. "v" — вместо кодов "0x1c" или "0x1b" как в публичных транзакциях, для приватных транзакций вы увидите "0x26" или "0x25".

Теперь давайте проверим TransactionReceipt и саму транзакцию на изменение состояние контракта (команды вы уже знаете). Результат:




Ничего нового мы из этой приватной транзакции, в принципе, не узнаем.


Permissioned транзакции


Так, как это тоже приватные транзакции, просто приватные они не для 1 ноды, а для нескольких, то результаты выполнения таких транзакций ничем не будут отличаться от приватных транзакций. Разницу можна почуствувать, если попробовать получить информацию из ноды, которая была указана в privateFor и из ноды, чей публичный ключ не прописан в privateFor (сможете получить информацию с первой ноды и не сможете со второй).
Что бы запустить тест-кейс с транзакциями приватными для нескольких учасников сети (permissioned транзакции), нужно выполнить следущую команду:


gradle test --tests *.PermissionTransactionsTests

Логи Java API:


[HANDLER] privateFor = [wQTHrl/eqa7TvOz9XJcazsp4ZuncfxHb8c1J1njIOGA=]
[HANDLER] deploying new smart-contract
[HANDLER] contract has been successfully deployed. Result: {contract_address=0xf1cc0ba22bd0d18fc9acb22dd57795a3f2fb4ebd, transaction_hash=0x585980bec88aa8a0fe5caffe6d6f24b82d3cd381fcf72fdd8e2102ce67799f01}
[HANDLER] privateFor = [wQTHrl/eqa7TvOz9XJcazsp4ZuncfxHb8c1J1njIOGA=]
[HANDLER] sending new transaction
[HANDLER] transaction has been successfully executed. Result: {contract_address=null, transaction_hash=0x47edc0d00fa9447b2da9f5a78f44602f96145497238cb1ce1d879afb351a3cbe}
[HANDLER] reading user from smart-contract
[HANDLER] user: 'Permissioned Test User'

Результаты в Geth-клиенте, на деплой нового смарт-контракта, TransactionReceipt и сама транзакция соотвествено:




И транзакция на изменение состояния, TransactionReceipt и сама транзакция:




HTTP запросы


Несмотря на то, что мы увидели, как публичные транзакции отличаються от приватных с точки зрения Geth-клиента, это не показывает реального ограничения на получение информации. Поэтому для того, чтобы показать вам, что действительно можно ограничить число нод, способных прочитать вашу транзакцию, я сделаю несколько запросов с помощью CURL на 3 ноды, для считывания информации со смарт-контракта (запросы будут касаться сделаных private и persmissioned транзакций).
HTTP запросы будут иметь в теле запроса 2 параметра:


  1. "endpoint" — непосредстверно endpoint к Quorum ноде, нужно для подключения к ноде.
  2. "contractAddress" — адрес контракта, с которого будут считываться данные.

В моем случае "endopint" будет иметь один хост — localhost — но разные порты для 3 Quorum нод: 22000 (с этой ноды делались все транзакции), 22001 (ее публичный ключ был указан в permissioned транзакциях), 22002 (не должна иметь доступ к информации).


Начнем с private транзакции (только нода на 22000 порте должна иметь возможность просматривать информацию в смарт-контракте).


CURL запрос на ноду которая делала транзакции:


curl -X POST     http://127.0.0.1:8080/user     -H 'Content-Type: application/json'     -d '{
    "endpoint": "http://127.0.0.1:22000",
    "contractAddress": "0x3e2284d92842f781b83cc7e56fbb074ab15f9a90"
}'

Как результ мы получили следущее:


{"data":{"user":"Private Test User"}}

Это означает, что нода имет возможность просматривать информацию в смарт-контракте.


Теперь давайте просмотрим, что вернет нам нода на 22001 порту. CURL запрос:


curl -X POST     http://127.0.0.1:8080/user     -H 'Content-Type: application/json'     -d '{
    "endpoint": "http://127.0.0.1:22001",
    "contractAddress": "0x3e2284d92842f781b83cc7e56fbb074ab15f9a90"
}'

Отлично! В результате мы получили следущее сообщение об ошибке:


{"data":{"status_code":500,"description":"Something went wrong"}}

В данном случае это означает, что мы не можем прочитать информацию из смарт-контракта — то чего мы добивались!


И ещё надо проверить последнюю, 3-ую ноду. CURL запрос:


curl -X POST     http://127.0.0.1:8080/user     -H 'Content-Type: application/json'     -d '{
    "endpoint": "http://127.0.0.1:22002",
    "contractAddress": "0x3e2284d92842f781b83cc7e56fbb074ab15f9a90"
}'

Отлично! API возращает ошибку:


{"data":{"status_code":500,"description":"Something went wrong"}}

Теперь мы убедились, что приватные транзакции действительно ограничивают доступ к данным. Теперь пришло время проверять "permissioned" транзакции.


Делаем CURL запросы на считывание данных из созданого "permissioned" смарт-контракта из ноды, запущеной на порту 22000:


curl -X POST     http://127.0.0.1:8080/user     -H 'Content-Type: application/json'     -d '{
    "endpoint": "http://127.0.0.1:22000",
    "contractAddress": "0xf1cc0ba22bd0d18fc9acb22dd57795a3f2fb4ebd"
}'

Результат:


{"data":{"user":"Permissioned Test User"}}

Это означает, что нода имеет доступ на чтение данных из контракта, что не удивительно, так как эта нода их и создавала.


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


curl -X POST     http://127.0.0.1:8080/user     -H 'Content-Type: application/json'     -d '{
    "endpoint": "http://127.0.0.1:22001",
    "contractAddress": "0xf1cc0ba22bd0d18fc9acb22dd57795a3f2fb4ebd"
}'

Отлично! В результате выполнения запроса мы действительно смогли прочитать данные:


{"data":{"user":"Permissioned Test User"}}

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


curl -X POST     http://127.0.0.1:8080/user     -H 'Content-Type: application/json'     -d '{
    "endpoint": "http://127.0.0.1:22002",
    "contractAddress": "0xf1cc0ba22bd0d18fc9acb22dd57795a3f2fb4ebd"
}'

Отлично! Последняя нода действительно не смогла прочитать информацию из смарт-контракта. Как и было задумано.


Заключение


В этой статье я хотел показать, как можно использовать Quorum blockchain для разработки Java приложений. Надеюсь что у меня всё получилось, и вы нашли что-то важное и интересное для себя.


Ссылки на ресурсы:


  1. Документация по Quorum
  2. Тестовая сеть на Quorum
  3. Проект на GitHub
  4. Quorum канал в Slack

Спасибо за внимание!




К сожалению, не доступен сервер mySQL