Ethereumブロックチェーンでのシンプルなピクセルゲームの実装

みなさんこんにちは! r / placeに触発され、最終的にブロックチェーンに最初のスマートコントラクトを実装したいので、私たちはEthereumネットワーク上で誰もがアクセス可能な楽しいアプリケーションにすることを決定しましたブロックチェーン。 また、友人とリアルタイムで描画し、スマートコントラクトのトランザクションがネットワークで確認されると、選択したピクセルの色がリアルタイムでどのように変化するかを見ることができます。



スマートコントラクトでは、ピクセルの色の変更に対する支払いは必要ありませんが、取引を確認するために鉱夫に少額の手数料を支払う必要があります。



この記事では、現在のアプリケーションの最初のバージョンをどのように入手し、どのような技術的な問題に直面しなければならないかを説明したいと思います。



コンポーネント



スマートコントラクトの作成に重点を置くことを計画していたため、サーバーアーキテクチャに多くの時間を費やすことは避け、すべてを可能な限りシンプルにしました。

アプリケーションで作業するための主な条件は、ウォレットにアクセスできるイーサリアム互換ブラウザとブラウザプラグイン(メタマスクなど)の存在です。 この場合、クライアントはトランザクションをブロックチェーンに個別に送信し、サーバーはNginxを通じて契約の更新された状態のみを提供します。 したがって、データの完全性とセキュリティはすべて、ブロックチェーン内のスマートコントラクトによって保証されます。



画像



したがって、次のようになりました。





スマートコントラクト



まず、スマートコントラクトの実装を取り上げ、Solidityを使用した最初の経験を考慮して、ネットワークで利用可能なベストプラクティスに依存して、MVCを実行し、コントラクト状態を保存するためのモデルと、このモデルを更新できるコントローラーを作成することにしました。



このアプローチの最大の利点は、将来、変更があり、新しい機能が登場しても、アプリケーションの状態を移行する必要がなく、コントローラーのみを更新する必要があることです。



このアプローチの唯一の欠点は、Ethereumネットワークでの初期化中のスマートコントラクトのコストが高いことです(当時は約1,100ドル)。



素朴な実装



最初は、実装と時期尚早な最適化の有効性については考えていなかったため、最初の契約はできる限り不器用なものにしました。 ペイント用のキャンバスのサイズが1000px x 1000pxであるとすると、すべてのピクセルを格納した100万個の配列が得られます。



契約の実装例は次のとおりです。



contract PixelsField is Controllable { uint[1000000] public colors; event PixelChanged(uint coordinates, uint clr); function setPixel(uint coordinates, uint clr) public onlyController { require(clr < 16); require(coordinates < 1000000); colors[coordinates] = clr; PixelChanged(coordinates, clr); } function allPixels() public view returns (uint[1000000]) { return colors; } }
      
      





難しさ



一見、すべてが正常に機能し、ブロックチェーンの最初のピクセルを保存できましたが、ネットワークから状態を読み取ろうとするとすぐに失望しました。





改良版



実装の最初のバージョンの失敗にイライラして、ピクセルを圧縮して読み取るための可能なアルゴリズム、配列のよりコンパクトなバージョンを作成し、そこに可能な16色のピクセルの1つを保存する方法について考え始めました。



Solidityで使用可能なデータ型を慎重に検討した結果、uint256を使用することにしましたが、ピクセルの位置と色の両方をそこに書き込むことから、使用可能な256のうち4ビットに各ピクセルを配置したいと考えました。



これを行うには、ビットマジックのビットが必要でした。このマジックでは、各ピクセルのX座標とY座標を配列要素のインデックスにエンコードし、ビットシフトとマスクを適用します。



以下の実装例:



 function setPixel(uint coordinate, uint color) public onlyController { require(color < 16); require(coordinate < 1000000); uint idx = coordinate / ratio; uint bias = coordinate % ratio; uint old = colors[idx]; uint zeroMask = ~(bitMask << (n * bias)); colors[idx] = (old & zeroMask) | (color << (n * bias)); PixelChanged(coordinate, color); }
      
      





実装された最適化の簡単な計算により、100万ピクセルを保存するために必要なのは、uint256型の1,000,000 / 64ビット= 15,625要素のみであることが示されました。



したがって、単純な実装の初期配列を64倍に削減し、クライアントで許容可能な時間内に配列全体を読み取ることができました。



完全な契約状態の例を以下に示します。



 contract PixelsField is Controllable { event PixelChanged(uint coordinates, uint clr); uint[15625] public colors; uint bitMask; uint n = 4; uint ratio = 64; function PixelsField() public { bitMask = (uint8(1) << n) - 1; } function setPixel(uint coordinate, uint color) public onlyController { require(color < 16); require(coordinate < 1000000); uint idx = coordinate / ratio; uint bias = coordinate % ratio; uint old = colors[idx]; uint zeroMask = ~(bitMask << (n * bias)); colors[idx] = (old & zeroMask) | (color << (n * bias)); PixelChanged(coordinate, color); } function getPixel(uint coordinate) public view returns (uint) { var idx = coordinate / ratio; var bias = coordinate % ratio; return (colors[idx] >> (n * bias)) & bitMask; } function allPixels() public view returns (uint256[15625]) { return colors; } }
      
      





契約の相互作用



UIからコントラクトとやり取りするために、コントラクトの状態にアクセスできる次の関数を追加しました。



 function getPixel(uint coordinate) public view returns (uint) function allPixels() public view returns (uint256[15625])
      
      





ユーザーインターフェース



私たちの目標は、最もシンプルで簡単なUIを作成することでした。ユーザーは描画のためにキャンバスに焦点を合わせ、使用可能なツールからは色とズーム機能のみを選択できます。



主にブロックチェーンのピクセル配列のサイズが小さく、ブラウザーでCanvasを使用しているため、キャンバス全体のレンダリングはかなり高速で安価な操作です。



さらに、訪問者にはイーサリアム互換のブラウザまたはブラウザプラグイン(メタマスクなど)を持たない人もいる可能性があることを考慮したため、サーバーがブロックチェーンからキャンバス上のすべてのピクセルの現在の状態を生成し、クライアントに静的なNginxを使用した画像。



ピクセルの色を変更するには、web3.jsライブラリを使用します。 コントラクトからの関数呼び出しを以下に示します:



 const colorSelected = (color) => () => { hidePicker(); web3.eth.getAccounts((_error, accounts) => { if (accounts.length === 0) { alert("Please login in you wallet. Account not found ¯\_(ツ)_/¯."); return; }; const config = { from: accounts[0], gasPrice: 2500000000, gasLimit: 50000, value: 0 }; try { controllerContract.methods.setPixel(settings.selectedcoordinate, color).send(config, (error, addr) => { if (error) { console.log(error); return; } userPixels.push({ coord: settings.selectedcoordinate, color: color }); const {x, y} = numberToCoord(settings.selectedcoordinate); setPixel(ctx, x, y, settings.colors[color]); }); } catch (error) { console.log(error); } }); }
      
      





サーバー



クライアントのweb3.jsライブラリに完全に依存することを望んでいたため、APIの実装は優先事項ではありませんでした。



しかし、Ethereum互換プラグインとモバイルデバイスを持たないブラウザーユーザーが多数いるため、DigitalOcean環境でParityノードを上げてネットワークと同期することにしました。



Parityとやり取りするために、軽量なAPIを作成しました。これは、Parityノードをポーリングし、コントラクトの現在の状態を取得してこの最新の状態を描画し、画像をpng形式でサーバーに保存します。 さらに、クライアントに写真を提供することはNginxの関心事です。



コントラクトの状態はuint256データの配列であるため、コントラクトから取得するおおよそのペイロードは次のようになります。

0x0000000000000000000000000000000000000000000000000000000000000000000000000b ...



そして、クライアントで利用可能な16色と必要な結果をPNG画像の形式で考慮して変換を行う必要があります。



以下の例:



 mport java.awt.{Color => AwtColor} import java.io.{File, FileOutputStream} import java.time.Instant import com.sksamuel.scrimage.nio.PngWriter import com.sksamuel.scrimage.{Image, Pixel} import com.typesafe.scalalogging.StrictLogging import org.web3j.utils.{Numeric => NumericTools} import scala.util.Try object Composer extends StrictLogging { private lazy val colorMapping: Map[Char, String] = Map(  '0' -> "#FFFFFF",  '1' -> "#9D9D9D",  '2' -> "#000000",  '3' -> "#BE2633",  '4' -> "#E06F8B",  '5' -> "#493C2B",  '6' -> "#A46422",  '7' -> "#EB8931",  '8' -> "#F7E26B",  '9' -> "#2F484E",  'a' -> "#44891A",  'b' -> "#A3CE27",  'c' -> "#1B2632",  'd' -> "#005784",  'e' -> "#31A2F2",  'f' -> "#B2DCEF") private lazy val pixelsMapping: Map[Char, Pixel] = hex2Pixels(colorMapping) private val canvasHeight = 1000 private val canvasWidth = 1000 private val segmentLength = 64 def hex2Pixels(map: Map[Char, String]): Map[Char, Pixel] = {  def pixel(hex: String) = {    for {      color <- Try(AwtColor.decode(hex)).toOption      pixel = Pixel(color.getRed, color.getGreen, color.getBlue, 255)    } yield pixel  }  for {    (color, hex) <- map    pixel <- pixel(hex)  } yield color -> pixel } def apply(encoded: String): Unit = {  val startedAt = Instant.now  val pixels = translateToPixels(encoded)  write(pixels, fileName)  logger.info(s"Successfully wrote $fileName, took ${ Instant.now.toEpochMilli - startedAt.toEpochMilli } ms") } def translateToPixels(encoded: String): List[Pixel] = {  def decode(color: Char) = for (pixel <- pixelsMapping.get(color)) yield pixel  val extracted = NumericTools.cleanHexPrefix(encoded)  extracted.grouped(segmentLength)    .toList    .par    .flatMap(_.reverse.toSeq)    .flatMap(decode)    .toList } private def write(pixels: List[Pixel], fileName: String): Unit = {  val file = new File(fileName)  val out = new FileOutputStream(file, false) // don't append existing file  val image = Image(canvasWidth, canvasHeight, pixels.toArray)  val pngWriter = PngWriter()  pngWriter.write(image, out)  out.flush()  out.close() } }
      
      





結果



彼らが学んだこと:



要約すると、ブロックチェーンで最初のアプリケーションを作成してメインネットに置くことは非常に興味深く、有益でしたし、ユーザーがブロックチェーンに何かを描いて歴史に残すことも楽しいことを願っています:)



今後の計画



ethplace.ioを開発する予定であり、まもなく作業中の新しい興味深い機能に関するニュースを共有できるようになります!



All Articles