Skip to content

Commit

Permalink
Merge pull request #10 from iden3/chore/improve-eth-cli-tx-handling
Browse files Browse the repository at this point in the history
Improve tx handling for on-chain RHS CLI
  • Loading branch information
vmidyllic authored Nov 29, 2023
2 parents db15953 + ca37d71 commit 351be8b
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 87 deletions.
40 changes: 38 additions & 2 deletions .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,56 @@ jobs:
runs-on: ubuntu-20.04
container: golang:1.19.13-bullseye
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
- uses: actions/checkout@v4

- uses: actions/cache@v3
with:
path: |
~/.cache/go-build
/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Pull contracts repository, branch master
uses: actions/checkout@v4
with:
repository: iden3/contracts
ref: master
path: ./contracts

- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: contracts/package-lock.json

- name: Run hardhat
working-directory: ./contracts
run: |
npm ci
npx hardhat node > hardhat.log 2>&1 &
- name: Deploy IRHSStorage to hardhat
working-directory: ./contracts
env:
STATE_CONTRACT_ADDRESS: "0x0000000000000000000000000000000000000000"
run: |
sleep 5
npx hardhat run --network localhost scripts/deployIdentityTreeStore.ts > deploy_contracts.log
echo "IRHS_STORAGE_ADDRESS=$(cat deploy_contracts.log | grep -oP "(?<=to: ).*" | tail -1)" >> $GITHUB_ENV
echo "IRHSStorage deploy logs:"
cat deploy_contracts.log
- name: Integration Tests
run: go test -v -race ./...
working-directory: tests/integration
env:
RHS_URL: http://rhs:8080

- name: Show Hardhat logs
run: cat ./contracts/hardhat.log

services:
rhs:
image: ghcr.io/iden3/reverse-hash-service:latest
Expand Down
206 changes: 167 additions & 39 deletions eth/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,56 +8,86 @@ import (
"strings"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/iden3/contracts-abi/rhs-storage/go/abi"
"github.com/iden3/go-merkletree-sql/v2"
"github.com/iden3/merkletree-proof"
)

type ReverseHashCli struct {
contract *abi.IRHSStorage
txOpts *bind.TransactOpts
rpcTimeout time.Duration
contract *abi.IRHSStorage
ethClient *ethclient.Client
from ethcommon.Address
signer bind.SignerFn
rpcTimeout time.Duration
needWaitReceipt bool
txReceiptTimeout time.Duration
waitReceiptCycleTime time.Duration
}

func NewReverseHashCli(contractAddress ethcommon.Address,
ethClient *ethclient.Client, txOpts *bind.TransactOpts,
defaultRPCTimeout time.Duration) (*ReverseHashCli, error) {
type Option func(cli *ReverseHashCli) error

if ethClient == nil {
return nil, errors.New("ethClient is nil")
func WithRPCTimeout(timeout time.Duration) Option {
return func(cli *ReverseHashCli) error {
cli.rpcTimeout = timeout
return nil
}
}

contract, err := abi.NewIRHSStorage(contractAddress, ethClient)
if err != nil {
return nil, fmt.Errorf("failed to instantiate a smart contract: %s", err)
func WithNeedWaitReceipt(needWaitReceipt bool) Option {
return func(cli *ReverseHashCli) error {
cli.needWaitReceipt = needWaitReceipt
return nil
}
}

func WithTxReceiptTimeout(timeout time.Duration) Option {
return func(cli *ReverseHashCli) error {
cli.txReceiptTimeout = timeout
return nil
}
}

return &ReverseHashCli{
contract: contract,
txOpts: txOpts,
rpcTimeout: defaultRPCTimeout,
}, nil
func WithWaitReceiptCycleTime(cycleTime time.Duration) Option {
return func(cli *ReverseHashCli) error {
cli.waitReceiptCycleTime = cycleTime
return nil
}
}

func (cli *ReverseHashCli) ctx(
ctx context.Context) (context.Context, context.CancelFunc) {
func NewReverseHashCli(ethClient *ethclient.Client,
contractAddress ethcommon.Address, from ethcommon.Address, signerFn bind.SignerFn,
opts ...Option) (*ReverseHashCli, error) {

if ctx == nil {
ctx = cli.txOpts.Context
rhc := &ReverseHashCli{
ethClient: ethClient,
from: from,
signer: signerFn,
rpcTimeout: 30 * time.Second,
needWaitReceipt: false,
txReceiptTimeout: 30 * time.Second,
waitReceiptCycleTime: time.Second,
}

if ctx == nil {
ctx = context.Background()
for _, o := range opts {
err := o(rhc)
if err != nil {
return nil, err
}
}

if cli.rpcTimeout > 0 {
return context.WithTimeout(ctx, cli.rpcTimeout)
contract, err := abi.NewIRHSStorage(contractAddress, rhc.ethClient)
if err != nil {
return nil, fmt.Errorf("failed to instantiate a smart contract: %s", err)
}
rhc.contract = contract

return ctx, func() {}
return rhc, nil
}

func (cli *ReverseHashCli) GenerateProof(ctx context.Context,
Expand All @@ -72,7 +102,7 @@ func (cli *ReverseHashCli) GetNode(ctx context.Context,

id := hash.BigInt()

ctx, cancel := cli.ctx(ctx)
ctx, cancel := cli.ctxWithRPCTimeout(ctx)
defer cancel()

opts := &bind.CallOpts{Context: ctx}
Expand Down Expand Up @@ -101,18 +131,6 @@ func (cli *ReverseHashCli) GetNode(ctx context.Context,
func (cli *ReverseHashCli) SaveNodes(ctx context.Context,
nodes []merkletree_proof.Node) error {

ctx, cancel := cli.ctx(ctx)
defer cancel()

txOpts := &bind.TransactOpts{
From: cli.txOpts.From,
Signer: cli.txOpts.Signer,
GasFeeCap: cli.txOpts.GasFeeCap,
GasTipCap: cli.txOpts.GasTipCap,
Context: ctx,
NoSend: false,
}

nodesBigInt := make([][]*big.Int, len(nodes))
for i, node := range nodes {
nodesBigInt[i] = make([]*big.Int, len(node.Children))
Expand All @@ -121,10 +139,120 @@ func (cli *ReverseHashCli) SaveNodes(ctx context.Context,
}
}

_, err := cli.contract.SaveNodes(txOpts, nodesBigInt)
ctxRPC, cancelRPC := cli.ctxWithRPCTimeout(ctx)
defer cancelRPC()

txOpts, err := cli.txOptions(ctx, ctxRPC)
if err != nil {
return err
}

tx, err := cli.contract.SaveNodes(txOpts, nodesBigInt)
if err != nil {
return err
}

_, err = cli.waitReceipt(ctx, cli.ethClient, tx)
if err != nil {
return err
}
return nil
}

func (cli *ReverseHashCli) txOptions(ctx, ctxRPC context.Context) (*bind.TransactOpts, error) {
gasTipCap, err := cli.suggestGasTipCap(ctx)
if err != nil {
return nil, err
}

txOpts := &bind.TransactOpts{
From: cli.from,
Signer: cli.signer,
GasTipCap: gasTipCap,
GasLimit: 0, // go-ethereum library will estimate gas limit automatically if it is 0
Context: ctxRPC,
NoSend: false,
}
return txOpts, nil
}

func (cli *ReverseHashCli) ctxWithRPCTimeout(ctx context.Context) (context.Context,
context.CancelFunc) {

if cli.rpcTimeout > 0 {
return context.WithTimeout(cli.ctx(ctx), cli.rpcTimeout)
}

return ctx, func() {}
}

func (cli *ReverseHashCli) ctxWithTxReceiptTimeout(ctx context.Context) (context.Context,
context.CancelFunc) {

if cli.txReceiptTimeout > 0 {
return context.WithTimeout(cli.ctx(ctx), cli.txReceiptTimeout)
}

return ctx, func() {}
}

func (cli *ReverseHashCli) ctx(ctx context.Context) context.Context {
if ctx == nil {
ctx = context.Background()
}

return ctx
}

func (cli *ReverseHashCli) waitReceipt(ctx context.Context,
cl *ethclient.Client, tx *types.Transaction) (*types.Receipt, error) {

if !cli.needWaitReceipt {
return nil, nil
}

ctx, cancel := cli.ctxWithTxReceiptTimeout(ctx)
defer cancel()

queryTicker := time.NewTicker(cli.waitReceiptCycleTime)
defer queryTicker.Stop()

logger := log.New("hash", tx.Hash())
for {
receipt, err := cl.TransactionReceipt(ctx, tx.Hash())
if err == nil {
return receipt, nil
}

if errors.Is(err, ethereum.NotFound) {
logger.Trace("Transaction not yet mined")
} else {
logger.Trace("Receipt retrieval failed", "err", err)
return nil, err
}

// Wait for the next round.
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-queryTicker.C:
}
}
}

func (cli *ReverseHashCli) suggestGasTipCap(ctx context.Context) (*big.Int, error) {
ctxRPC, cancel := cli.ctxWithRPCTimeout(ctx)
defer cancel()

tip, err := cli.ethClient.SuggestGasTipCap(ctxRPC)
// since hardhat doesn't support 'eth_maxPriorityFeePerGas' rpc call.
// we should hard code 0 as a mainer tips. More information: https://github.com/NomicFoundation/hardhat/issues/1664#issuecomment-1149006010
if err != nil && strings.Contains(err.Error(), "eth_maxPriorityFeePerGas not found") {
log.Trace("failed get suggest gas tip. Use 0 instead", "err", err)
tip = big.NewInt(0)
} else if err != nil {
return nil, fmt.Errorf("failed get suggest gas tip: %w", err)
}

return tip, nil
}
52 changes: 9 additions & 43 deletions tests/integration/fixtures.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package integration

import (
"context"
"encoding/hex"
"math/big"
"strings"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/iden3/merkletree-proof/eth"
Expand All @@ -18,35 +14,17 @@ func NewTestEthRpcReserveHashCli(contractAddress common.Address) (*eth.ReverseHa
if err != nil {
return nil, err
}

timeout := 10 * time.Second
signer := newTestSigner()

addr, _ := signer.Address()

ctx := context.Background()
ctxWT, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
tip, err := suggestGasTipCap(ctxWT, ethCl, err)
if err != nil {
return nil, err
}

ctxWT2, cancel2 := context.WithTimeout(ctx, timeout)
defer cancel2()
txOpts := &bind.TransactOpts{
From: addr,
Signer: signer.SignerFn,
GasTipCap: tip, // The only option we need to set is gasTipCap as some Ethereum nodes don't support eth_maxPriorityFeePerGas
GasLimit: 0, // go-ethereum library will estimate gas limit automatically if it is 0
Context: ctxWT2,
NoSend: false,
}

return eth.NewReverseHashCli(contractAddress, ethCl, txOpts, timeout)
signer := NewTestSigner()
fromAddr, _ := signer.Address()

return eth.NewReverseHashCli(ethCl,
contractAddress,
fromAddr,
signer.SignerFn,
)
}

func newTestSigner() *TestSigner {
func NewTestSigner() *TestSigner {
pk, _ := hex.DecodeString("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
chainID := int64(31337)

Expand All @@ -55,15 +33,3 @@ func newTestSigner() *TestSigner {
ChainId: big.NewInt(chainID),
}
}

func suggestGasTipCap(ctx context.Context, ethCl *ethclient.Client, err error) (*big.Int, error) {
tip, err := ethCl.SuggestGasTipCap(ctx)
// since hardhat doesn't support 'eth_maxPriorityFeePerGas' rpc call.
// we should hard code 0 as a mainer tips. More information: https://github.com/NomicFoundation/hardhat/issues/1664#issuecomment-1149006010
if err != nil && strings.Contains(err.Error(), "eth_maxPriorityFeePerGas not found") {
tip = big.NewInt(0)
} else if err != nil {
return nil, err
}
return tip, nil
}
Loading

0 comments on commit 351be8b

Please sign in to comment.