diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index c2d9d17..dda923e 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -11,8 +11,9 @@ 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 @@ -20,11 +21,46 @@ jobs: 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 diff --git a/eth/proof.go b/eth/proof.go index 51d6e88..7d7eb03 100644 --- a/eth/proof.go +++ b/eth/proof.go @@ -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, @@ -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} @@ -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)) @@ -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 +} diff --git a/tests/integration/fixtures.go b/tests/integration/fixtures.go index 9e0e82b..905d7fd 100644 --- a/tests/integration/fixtures.go +++ b/tests/integration/fixtures.go @@ -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" @@ -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) @@ -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 -} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 1b0be7e..9c10467 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -28,10 +28,9 @@ func TestProof_Http(t *testing.T) { } func TestProof_Eth(t *testing.T) { - t.Skip("skipping eth test") - addrStr, ok := os.LookupEnv("IDENTITY_TREE_STORE_ADDRESS") + addrStr, ok := os.LookupEnv("IRHS_STORAGE_ADDRESS") if !ok { - panic("IDENTITY_TREE_STORE_ADDRESS not set") + panic("IRHS_STORAGE_ADDRESS not set") } addr := ethcommon.HexToAddress(addrStr)