Thursday, December 12, 2019

Deploy Smart Contract on Ethereum using GoLang


This article presents all the steps required to deploy and use a contract using Go.

Few other posts about this exist:
So, I've decided creating this article to cover all the steps, hoping you will find it useful.
The article is setup for a private Ethereum network, but can be also used for the public Ethereum network.


We will review the following steps:
  1. Create a Smart Contract in Solidity
  2. Manually Compile the Contract to Go
  3. Create a Go Application 
  4. Create a 3 stages Dockerfile to compile the contract and the application 

1. Create Smart Contract in Solidity



Contracts are created using solidity. In this article, a simple contract with a call method and a transaction method. See this for explanation about the difference between a call and a transaction.

pragma solidity ^0.5.8;

contract Price {
  uint price = 100;

  function setPrice(uint newPrice) external payable  {
    price = newPrice;
  }

  function getPrice() external view returns (uint) {
    return price;
  }
}

The contract contains the following methods:

  • setPrice: a transaction method
  • getPrice: a call method

2. Manually Compile the Contract to Go



To use a contract in Go, we will need to generate a Go file for it.

First install the solc: the solidity compiler as explained here.

Next, install abigen, which is part of the geth-tools. The geth tools can be downloaded from the geth download site. For example, the current version is:
https://gethstore.blob.core.windows.net/builds/geth-alltools-linux-amd64-1.9.9-01744997.tar.gz

Now we can generate the go contract using the following commands:

solc --abi --output-dir Price.sol
solc --bin --output-dir Price.sol
abigen --bin Price.bin --abi Price.abi --pkg=price --out=Price.go

Copy the Price.go to the a folder named "generated" in the go application source folder.
In later stage we will automate this step as part of a Docker build.


3. Create a Go Application



The go application should handle the following:

  • Connect to the Ethereum network
  • Deploy the contract
  • Load the contract
  • Use the contract
To connect to the Ethereum network, we use the following:

url:= "ws://THE_ETHEREUM_NETWORK_IP_ADDRESS"
timedContext, _ := context.WithTimeout(context.Background(), 30*time.Second)
client, err := ethclient.DialContext(timedContext, url)
if err == nil {
  log.Fatalf("connection failed %v", err)
  return
}

Notice that we use a 30 seconds timeout based connection. Choose whatever timeout that fit your need. Also, replace the URL with your Ethereum  network address.
Notice that once all Ethereum actions are done, the ethclient should be close, so eventually call to:

client.Close()

To deploy the contract, we use the Price.go that we've generate before.

import (
  "crypto/ecdsa"
  "github.com/ethereum/go-ethereum/common"
  "github.com/ethereum/go-ethereum/crypto"
  "github.com/ethereum/go-ethereum/accounts/abi/bind"
  "github.com/ethereum/go-ethereum/common"
  "github.com/ethereum/go-ethereum/core/types"
  "github.com/ethereum/go-ethereum/ethclient"
  contract "sample.com/contract/generated"
)

func getAccountPrivateKey() *ecdsa.PrivateKey {
  // replace the private key here
  accountPrivateKey := "211dbaa6ca5e3fe1141eef3b00a0dd6d630a8d8e5bfbb7a7516865f1c746a3a0"
  privateKey, err := crypto.HexToECDSA(accountPrivateKey)
  if err != nil {
    log.Fatalf("private key to ECDSA failed: %v", err)
  }
  return privateKey
}

func getAccountPublicKey() common.Address {
  publicKey := getAccountPrivateKey().Public()
  publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
  if !ok {
    log.Fatalf("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
  }

  address := crypto.PubkeyToAddress(*publicKeyECDSA)
  return address
}

func getTransactionOptions(client *ethclient.Client) *bind.TransactOpts {
  nonce, err := client.PendingNonceAt(context.Background(), getAccountPublicKey())
  if err != nil {
    log.Fatalf("get pending failed: %v", err)
  }

  gasPrice, err := client.SuggestGasPrice(context.Background())
  if err != nil {
    log.Fatalf("suggest gas price failed: %v", err)
  }

  transactOpts := bind.NewKeyedTransactor(getSystemAccountPrivateKey())
  transactOpts.Nonce = big.NewInt(int64(nonce))
  transactOpts.Value = big.NewInt(0)
  // you might need to update gas price if you have extremly large contract
  transactOpts.GasLimit = uint64(3000000)
  transactOpts.GasPrice = gasPrice
  return transactOpts
}

func Deploy(client *ethclient.Client) string {
  transactOptions := getTransactionOptions(client)

  address, transaction, _, err := contract.DeployPriceContract(transactOpts, client)
  if err != nil {
    log.Fatalf("deploy contract failed: %v", err)
  }
  
  track(transaction)

  return address.Hex()
}

To use this, update the private key to your owner used Ethereum account private key.
This account must have enough ETH balance to pay for the contract deploy.

This code also includes a call to a track(transaction) function.
We will review transaction tracking later.

Once the contract is deployed we get an address of the contract. This address should be saved out of the scope of the Ethereum, so that it can be reused by other remote/distributed clients.

Notice that the Deploy function returns an instance of the contract, and so we can use it to run the contract method. However I prefer not to use it, and instead to show how to load the contract using the contract address, since you will probably need this.

func LoadContract(client *ethclient.Client, contractAddress string) *contract.PriceContract {
  address := common.HexToAddress(contractAddress)
  instance, err := contract.NewPriceContract(address, client)
  if err != nil {
    log.Fatalf("could not load contract: %v", err)
  }
  return instance
}


Using a contract "call" method is very simple:

callOptions := bind.CallOpts{From: getSystemAccountPublicKey()}
price, err := contract.GetPrice(&callOptions)
if err != nil {
  log.Fatalf("get price failed: %v", err)
}

Using a "transaction" method is similar:

transactionOptions := getTransactionOptions(client)
transaction, err := contract.SetPrice(transactionOptions, big.NewInt(777))

if err != nil {
  Fatal("set price failed: %v", err)
}
track(transaction)

This code also includes a call to a track(transaction) function.

So, why do we need to track the transaction?
The transaction might fail. It also might not be mined at all due to low gas price.
If we want to know that the transaction is success, we need to check the status of it whenever a new block is mined.

This code section tracks the transaction:

func track(transaction *types.Transaction) {
  headers = make(chan *types.Header)
  var err error
  subscription, err = client.SubscribeNewHead(context.Background(), headers)
  if err != nil {
    log.Fatalf("subscribe to read blocks failed: %v", err)
  }
  defer subscription.Unsubscribe()

  for {
    select {
    case err := <-subscription.Err():
      log.Fatalf("subscription failed: %v", err)
    case <-headers:
      // got block, checking transaction
      transactionLocated := checkTransactionOnce(transaction)
      if transactionLocated {
        return 
      }
    case <-time.After(60*time.Second):
      log.Fatalf("timeout waiting for transaction")
    }
  }
}

func (wrapper *EthereumTransactionWrapper) checkTransactionOnce(transaction *types.Transaction) bool {
  _, pending, err := Client.TransactionByHash(context.Background(), transaction.Hash())
  if err != nil {
    log.Fatalf("get transaction failed: %v", err)
  }
  if pending {
    return false
  }

  receipt, err := client.TransactionReceipt(context.Background(), transaction.Hash())
  if err != nil {
    log.Fatalf("transaction receipt failed: %v", err)
  }
  if receipt.Status == 1 {
    return true
  }
  log.Fatalf("transaction failed with logs %v", receipt.Logs)
  // dead code
  return true
}

4. Create a Dockerfile



The docker file is a multi-stage docker file.
You might want to review: Use cache in a docker multi stage build for faster builds.

The Dockerfile includes 3 steps:

  1. compile the contracts
  2. compile the go application
  3. package the compiled go application

#==================
# Stage1: contracts
#==================

FROM ubuntu:18.04 as contracts-compiler

# install wget
RUN apt-get update && \
    apt-get install -y software-properties-common wget && \
    rm -rf /var/lib/apt/lists/*

# install solc
RUN add-apt-repository ppa:ethereum/ethereum && \
    apt-get update && \
    apt-get install -y solc

# install geth tools
RUN mkdir /geth_extract
ARG SOLIDITY_ALL_TOOLS
RUN wget --progress=dot:giga https://gethstore.blob.core.windows.net/builds/${SOLIDITY_ALL_TOOLS} -O /geth_extract/tools.tar.gz
RUN tar xvzf /geth_extract/tools.tar.gz -C /geth_extract
RUN rm /geth_extract/tools.tar.gz
RUN mv /geth_extract/* /geth_extract/extracted
RUN mv -v /geth_extract/extracted/* /usr/local/bin
RUN rm -rf /geth_extract

ADD ./src/PriceContract.sol /contracts/PriceContract.sol
WORKDIR /contracts
RUN solc --abi --output-dir /compiled PriceContract.sol
RUN solc --bin --output-dir /compiled PriceContract.sol

WORKDIR /compiled
RUN abigen --bin PriceContract.bin --abi PriceContract.abi --pkg=bouncer_contract --out=PriceContract.go

#================
# Stage2: compile
#================

FROM golang:1.12 AS go-compiler

RUN apt-get update && \
    apt-get install -y git

WORKDIR /src
ENV GOPATH=/go
ENV GOBIN=/go/bin

# get dependencies
COPY ["./src/go.mod", "./src/go.sum", "/src/"]
RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go mod download

# compile source
ADD ./src /src

# copy generated contract
RUN mkdir -p /src/internal/generated
COPY --from=contracts-compiler /compiled/PriceContract.go /src/generated/PriceContract.go

RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -a -installsuffix cgo -o my-go-application

#================
# Stage3: package
#================

FROM ubuntu:18.04
COPY --from=go-compiler /src/my-go-application /my-go-application
WORKDIR /
ENTRYPOINT ["/my-go-application"]


Summary

In this article we have reviewed how to use Ethereum contract in a Go based application, include deploy of the contract, using the contract methods, and building the application.
We've included a method of tracking the contract transactions.

No comments:

Post a Comment