集中型NFT取引所プロジェクトの作成


以前に行われていたOpenSeaクローン符号化プロジェクトでは,ブロックチェーン読み出しにおける性能が非常に劣っていることが分かり,最近集中型NFT取引所を実施するプロジェクトが行われた.
しかし、実現機能を追求しすぎたため、プロジェクトを完了できなかった.
そこで、このプロジェクトを行う過程で、私が学んだことについてのブログを書きたいと思います.
集中型NFT取引所プロジェクトgithub repositoryリンクの作成
実装された機能
本プロジェクトはNFT(ERC-721)に対するインテリジェント契約(solidity)、back-endを担当する.
実現する機能は大体5種類ある.
  • ユーザごとにブロックチェーンアカウントを作成するための登録および登録機能
  • .
  • サーバにERC-721インテリジェント契約
  • を導入
  • サーバ上のNFT mint機能
  • NFTを他のユーザーに販売する機能
  • Front-EndオンデマンドでNFTを表示する機能
  • 今回は実現した機能をすべて話すよりも、私がプロジェクトで学んだ部分についてのブログを書きます.
    ExpressとMySQL
    最初は、画像の下のコードモードと同じようにback-endを実現してみました.
    ただし、コードの下の結果から、サーバは実際にqueryを送信して待機しているが、現在の接続が切断されるまで他の接続で待機問題が発生していることがわかる.
    その結果、次のコードは、サーバに接続されている複数のユーザを線形に処理し、パフォーマンスに大きな問題をもたらす可能性があります.
    const mysql = require('mysql2');
    require('dotenv').config();
    const express = require('express');
    const app = express();
    const port = 3000;
    
    const conn = mysql.createConnection({
        host: 'localhost',
        user: process.env.DATABASE_USER,
        database: process.env.DATABASE,
        password: process.env.DATABASE_PASSWORD,
    }).promise();
    
    app.get('/', async (req, res) => {
        console.log('Connection');
        await conn.query('DO SLEEP(5)');
        console.log('Disconnection');
    
        res.send('Hello World');
    });
    
    app.listen(port, () => {
        console.log(`Server is serviced at http://localhost:${port}`);
    });

    パフォーマンス損失を解決するために線形処理するために、次のコードに示すように、接続プールを使用する方法を決定しました.
    次のコードでは、接続プールは最大3つの接続を受信できます.
    コードの下の結果から、3つの接続を同時に処理することで(nodejsであるため、マルチスレッドと同じ方法での「同時」とは異なる)、サーバ上の空き時間を大幅に削減できることがわかります.
    const mysql = require('mysql2/promise');
    require('dotenv').config();
    const express = require('express');
    const app = express();
    const port = 3000;
    
    const pool = mysql.createPool({
        host: 'localhost',
        user: process.env.DATABASE_USER,
        database: process.env.DATABASE,
        password: process.env.DATABASE_PASSWORD,
        waitForConnections: true,
        connectionLimit: 3,
    });
    
    app.get('/', async (req, res) => {
        console.log('Connection');
        await pool.query('DO SLEEP(5)');
        console.log('Disconnection');
    
        res.send('Hello World');
    });
    
    app.listen(port, () => {
        console.log(`Server is serviced at http://localhost:${port}`);
    });

    しかし、ここでは、1つのトランザクションが複数のqueryで構成されている場合、データベースの性質原子性をどのように保護するかを調べました.
    最終的に、データベースの原子性を維持する方法によって、私が見つけたコードパターンは以下のようになります.
    const mysql = require('mysql2/promise');
    require('dotenv').config();
    const express = require('express');
    const app = express();
    const port = 3000;
    
    const pool = mysql.createPool({
        host: 'localhost',
        user: process.env.DATABASE_USER,
        database: process.env.DATABASE,
        password: process.env.DATABASE_PASSWORD,
        waitForConnections: true,
        connectionLimit: 3,
    });
    
    app.get('/', async (req, res) => {
        const conn = await pool.getConnection(conn => conn);
    
        // 유저 A가 B에게 100만원을 보내는 transaction이라고 가정
        try {
            await conn.beginTransaction();
    
            await conn.query('유저 A에서 100만원을 출금');
            await conn.query('유저 B에 100만원 입금');
    
            await conn.commit();
        } catch (err) {
            await conn.rollback();
        } finally {
            conn.release();
        }
    });
    
    app.listen(port, () => {
        console.log(`Server is serviced at http://localhost:${port}`);
    });
    CREATE2
    まず、CREATE 2はEIP-1014で提案された内容であり、簡単に言えばopcode 0 xf 5にCREATEが追加されている.
    CREATEはmsgです.senderのaddressとnonceを使用して契約アドレスを生成します.
    // Ethereum에서는 keccak256을 돌리고 나온 256bits 결과값에 처음 96bits를 버리고 나머지 160bits가 address이다.
    keccak256(rlp([sender, nonce]))[12:]
    しかし、CREATE 2は0 xff、address、salt、バイトコードの4つの値でcontract addressを生成する.
    keccak256( 0xff ++ address ++ salt ++ keccak256(bytecode))[12:]
    この程度になるとこのような考えがあります.
    ああ...でもこれはどうやって使いますか?
    私もそう思います.
    上記2つの契約アドレス生成方法において重要な違いは、CREATEがnonceを使用し、CREATE 2がnonceを使用しないことである.
    このため、CREATEでは同一アドレスに契約を作成することができず、CREATE 2では同一アドレスに契約を作成することができる.
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.10;
    
    contract Factory {
        event Deployed(address addr, uint salt);
    
        function getBytecode(address _owner, uint _foo) public pure returns (bytes memory) {
            bytes memory bytecode = type(TestContract).creationCode;
    
            return abi.encodePacked(bytecode, abi.encode(_owner, _foo));
        }
    
        function getAddress(bytes memory bytecode, uint _salt)
            public
            view
            returns (address)
        {
            bytes32 hash = keccak256(
                abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(bytecode))
            );
    
            // NOTE: cast last 20 bytes of hash to address
            return address(uint160(uint(hash)));
        }
    
        function deploy(bytes memory bytecode, uint _salt) public payable {
            address addr;
    
            assembly {
                addr := create2(
                    callvalue(), // wei sent with current call
                    add(bytecode, 0x20),
                    mload(bytecode),
                    _salt
                )
    
                if iszero(extcodesize(addr)) {
                    revert(0, 0)
                }
            }
    
            emit Deployed(addr, _salt);
        }
    }
    
    contract TestContract {
        address public owner;
        uint public foo;
    
        constructor(address _owner, uint _foo) payable {
            owner = _owner;
            foo = _foo;
        }
    
        function getBalance() public view returns (uint) {
            return address(this).balance;
        }
    }
    上記のコードを使用してgetAddress()で生成されたアドレスを予測できます.
    しかし、私はここで終わりたくない.
    急にJavaScriptで実現しようとした.
    そこで、スマート契約ではdeploy()関数のみを保持し、残りのgetBytecode()関数とgetAddress()関数はJavaScriptを実装しようとすることにしました.
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.7;
    
    contract Factory {
        event Deployed(address addr, uint salt);
    
        function deploy(bytes memory bytecode, uint _salt) public payable {
            address addr;
    
            assembly {
                addr := create2(
                    callvalue(),
                    add(bytecode, 0x20),
                    mload(bytecode),
                    _salt
                )
    
                if iszero(extcodesize(addr)) {
                    revert(0, 0)
                }
            }
    
            emit Deployed(addr, _salt);
        }
    }
    
    contract TestContract {
        address public owner;
        uint public foo;
    
        constructor(address _owner, uint _foo) payable {
            owner = _owner;
            foo = _foo;
        }
    
        function getBalance() public view returns (uint) {
            return address(this).balance;
        }
    }
    function getBytecode(owner, foo) {
      return `0x${[
        bytecode,
        web3.eth.abi.encodeParameters(['address', 'uint'], [owner, foo]),
      ]
        .map((x) => x.replace(/0x/, ''))
        .join('')}`;
    }
    
    function getAddress(creatorAddress, saltInteger, bytecode) {
      const salt = web3.eth.abi.encodeParameter('uint', saltInteger);
      return `0x${web3.utils
        .keccak256(
          `0x${['ff', creatorAddress, salt, web3.utils.keccak256(bytecode)]
            .map((x) => x.replace(/0x/, ''))
            .join('')}`,
        )
        .slice(-40)}`.toLowerCase();
    }
    以下のアドレスが現れることを実験で確認した.

    振り返る

  • Keep
    実際のサーバ上でデータベース原子性を実装するのは初めてです.また,create 2を実際に体験するとともに,create 2についてより多くの理解が得られるようになった.今回のプロジェクトを通じて、この2つの経験は本当に良い経験です.

  • Problem
    プロジェクトが完成できなかったことが気になる.私がもっと積極的に管理すれば、こんな問題は起こらないと思いますが、少なくとも私たちのチームは最善を尽くしているので、プロジェクトで知ったことはとても貴重だと思いますので、気分が悪くありません.

  • Try
    OnderのテクニカルブログからCREATE 2に関する多くの知識を得ました.しかし、誰もが読んだことがないわけではありません.CREATE 2は安全面で弱点があるそうです.onthetechnologyブログのCREATE 2の記事は必ず後で全部読みます.( CREATE 2は本当に大丈夫ですか? )