Skip to content

Commit

Permalink
Extend CfxBridge for more evm compatibility (#205)
Browse files Browse the repository at this point in the history
- Core space endpoint are not mandatory now;
- Multiple implementations support to get epoch receipts;
- Concurrency support if `GetTransactionReceipt` is used to get epoch receipts.
  • Loading branch information
wanliqun authored Jul 31, 2024
1 parent c9b5a56 commit 2be6a3a
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 43 deletions.
14 changes: 11 additions & 3 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ rpc:
cfxBridge:
# EVM space fullnode endpoint
ethNode: http://evmtestnet.confluxrpc.com
# core space fullnode endpoint
cfxNode: http://test.confluxrpc.com
# core space fullnode endpoint (optional)
# cfxNode: http://test.confluxrpc.com
# Implementation method for batch receipts, available options are:
# 0 - `parity_getBlockReceipts`
# 1 - `eth_getTransactionReceipt`
# 2 - `eth_getBlockReceipts`
# batchRcptImpl: 0
# The number of concurrencies to retrieve batch receipts for better performance, only effective
# when `eth_getTransactionReceipt` is used as the implementation method.
# batchRcptConcurrency: 4
# Available exposed modules are `cfx`, `txpool`, `trace`, if empty all APIs will be exposed.
# exposedModules: []
# Served HTTP endpoint
Expand Down Expand Up @@ -424,4 +432,4 @@ node:
# # Switch to turn on/off pprof
# enabled: false
# # The endpoint to start a http server for pprof
# httpEndpoint: ":6060"
# httpEndpoint: ":6060"
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/valyala/fasthttp v1.40.0
github.com/zealws/golang-ring v0.0.0-20210116075443-7c86fdb43134
go.uber.org/multierr v1.6.0
golang.org/x/sync v0.7.0
golang.org/x/time v0.5.0
gorm.io/driver/mysql v1.3.6
gorm.io/gorm v1.23.8
Expand Down Expand Up @@ -153,7 +154,6 @@ require (
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
Expand Down
18 changes: 12 additions & 6 deletions rpc/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/Conflux-Chain/confura/rpc/handler"
"github.com/Conflux-Chain/confura/util/metrics/service"
"github.com/Conflux-Chain/confura/util/rpc"
sdk "github.com/Conflux-Chain/go-conflux-sdk"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -144,7 +145,8 @@ func evmSpaceApis(
}

// nativeSpaceBridgeApis adapts evm space RPCs to core space RPCs.
func nativeSpaceBridgeApis(ethNodeURL, cfxNodeURL string) ([]API, error) {
func nativeSpaceBridgeApis(config *CfxBridgeServerConfig) ([]API, error) {
ethNodeURL, cfxNodeURL := config.EthNode, config.CfxNode
eth, err := rpc.NewEthClient(ethNodeURL)
if err != nil {
return nil, errors.WithMessage(err, "Failed to connect to eth space")
Expand All @@ -156,11 +158,15 @@ func nativeSpaceBridgeApis(ethNodeURL, cfxNodeURL string) ([]API, error) {
}
rpc.HookMiddlewares(eth.Provider(), ethNodeURL, "eth")

cfx, err := rpc.NewCfxClient(cfxNodeURL)
if err != nil {
return nil, errors.WithMessage(err, "Failed to connect to cfx space")
var cfx *sdk.Client
if len(cfxNodeURL) > 0 { // optioinal
cfx, err = rpc.NewCfxClient(cfxNodeURL)
if err != nil {
return nil, errors.WithMessage(err, "Failed to connect to cfx space")
}

rpc.HookMiddlewares(cfx.Provider(), cfxNodeURL, "cfx")
}
rpc.HookMiddlewares(cfx.Provider(), cfxNodeURL, "cfx")

// Hook an event handler to reidrect HTTP headers for the sdk client request
// because we could use `Confura` RPC service as our client endpoint for `cfxBridge`.
Expand All @@ -170,7 +176,7 @@ func nativeSpaceBridgeApis(ethNodeURL, cfxNodeURL string) ([]API, error) {
{
Namespace: "cfx",
Version: "1.0",
Service: cfxbridge.NewCfxAPI(eth, uint32(*ethChainId), cfx),
Service: cfxbridge.NewCfxAPI(eth, uint32(*ethChainId), cfx, config.BatchRcptImpl, config.BatchRcptConcurrency),
Public: true,
}, {
Namespace: "trace",
Expand Down
231 changes: 206 additions & 25 deletions rpc/cfxbridge/cfx_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,35 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
rpcp "github.com/openweb3/go-rpc-provider"
"github.com/openweb3/web3go"
ethTypes "github.com/openweb3/web3go/types"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)

const (
BatchRcptImplWithParityBlockReceipts = iota
BatchRcptImplWithEthTxnReceipt
BatchRcptImplWithEthBlockReceipts
)

type CfxAPI struct {
w3c *web3go.Client
cfx *sdk.Client
ethNetworkId uint32
w3c *web3go.Client
cfx *sdk.Client // optional
ethNetworkId uint32
batchRcptImpl int
batchRcptConcurrency int
}

func NewCfxAPI(w3client *web3go.Client, ethNetId uint32, cfxClient *sdk.Client) *CfxAPI {
func NewCfxAPI(
w3client *web3go.Client, ethNetId uint32, cfxClient *sdk.Client, batchRcptImpl int, concurrency int) *CfxAPI {
return &CfxAPI{
w3c: w3client,
cfx: cfxClient,
ethNetworkId: ethNetId,
w3c: w3client,
cfx: cfxClient,
ethNetworkId: ethNetId,
batchRcptImpl: batchRcptImpl,
batchRcptConcurrency: concurrency,
}
}

Expand All @@ -36,7 +50,34 @@ func (api *CfxAPI) EpochNumber(ctx context.Context, epoch *types.Epoch) (*hexuti
epoch = types.EpochLatestState
}

return api.cfx.WithContext(ctx).GetEpochNumber(epoch)
if api.cfx != nil {
return api.cfx.WithContext(ctx).GetEpochNumber(epoch)
}

var blockNum ethTypes.BlockNumber
switch {
case epoch.Equals(types.EpochEarliest):
blockNum = ethTypes.EarliestBlockNumber
case epoch.Equals(types.EpochLatestConfirmed):
blockNum = ethTypes.SafeBlockNumber
case epoch.Equals(types.EpochLatestState):
blockNum = ethTypes.LatestBlockNumber
case epoch.Equals(types.EpochLatestMined):
blockNum = ethTypes.LatestBlockNumber
case epoch.Equals(types.EpochLatestFinalized):
blockNum = ethTypes.FinalizedBlockNumber
case epoch.Equals(types.EpochLatestCheckpoint):
return HexBig0, nil
default:
return nil, ErrEpochUnsupported
}

block, err := api.w3c.WithContext(ctx).Eth.BlockByNumber(blockNum, false)
if err != nil {
return nil, err
}

return (*hexutil.Big)(block.Number), nil
}

func (api *CfxAPI) GetBalance(ctx context.Context, address EthAddress, bn *EthBlockNumber) (*hexutil.Big, error) {
Expand Down Expand Up @@ -228,19 +269,87 @@ func (api *CfxAPI) GetTransactionReceipt(ctx context.Context, txHash common.Hash
}

func (api *CfxAPI) GetEpochReceipts(ctx context.Context, bnh EthBlockNumberOrHash) ([][]*types.TransactionReceipt, error) {
receipts, err := api.w3c.WithContext(ctx).Parity.BlockReceipts(bnh.ToArg())
if err != nil {
return nil, err
var receipts []*ethTypes.Receipt

switch api.batchRcptImpl {
case BatchRcptImplWithEthTxnReceipt:
block, err := api.getBlockByBlockNumberOrHash(ctx, bnh, false)
if err != nil {
return nil, err
}

errGrp, ctx := errgroup.WithContext(ctx)
errGrp.SetLimit(api.batchRcptConcurrency)

txnHashes := block.Transactions.Hashes()
receipts = make([]*ethTypes.Receipt, len(txnHashes))

loop:
for idx, txn := range txnHashes {
rcptIdx := idx
errGrp.Go(func() error {
receipt, err := api.w3c.WithContext(ctx).Eth.TransactionReceipt(txn)
if err != nil {
return err
}

if receipt != nil && receipt.BlockHash != block.Hash { // reorg?
return errors.New("pivot reorg, please retry again")
}

receipts[rcptIdx] = receipt
return nil
})

select {
case <-ctx.Done(): // any error happened during the fetch?
break loop
default:
}
}

if err := errGrp.Wait(); err != nil {
return nil, err
}

case BatchRcptImplWithParityBlockReceipts:
blockReceipts, err := api.w3c.WithContext(ctx).Parity.BlockReceipts(bnh.ToArg())
if err != nil {
return nil, err
}

for i := range blockReceipts {
receipts = append(receipts, &blockReceipts[i])
}

case BatchRcptImplWithEthBlockReceipts:
blockReceipts, err := api.w3c.WithContext(ctx).Eth.BlockReceipts(bnh.ToArg())
if err != nil {
return nil, err
}

receipts = blockReceipts
default:
return nil, errors.New("unsupported batch receipt implementation")
}

result := make([]*types.TransactionReceipt, len(receipts))
for i := range receipts {
result[i] = ConvertReceipt(&receipts[i], api.ethNetworkId)
result[i] = ConvertReceipt(receipts[i], api.ethNetworkId)
}

return [][]*types.TransactionReceipt{result}, nil
}

func (api *CfxAPI) getBlockByBlockNumberOrHash(
ctx context.Context, bnh EthBlockNumberOrHash, isFull bool) (*ethTypes.Block, error) {
if bn, ok := bnh.ToArg().Number(); ok {
return api.w3c.WithContext(ctx).Eth.BlockByNumber(bn, isFull)
}

return api.w3c.WithContext(ctx).Eth.BlockByHash(*bnh.ToArg().BlockHash, isFull)
}

func (api *CfxAPI) GetAccount(ctx context.Context, address EthAddress, bn *EthBlockNumber) (types.AccountInfo, error) {
balance, err := api.w3c.WithContext(ctx).Eth.Balance(address.value, bn.ToArg())
if err != nil {
Expand Down Expand Up @@ -277,11 +386,65 @@ func (api *CfxAPI) GetAccumulateInterestRate(ctx context.Context, bn *EthBlockNu
}

func (api *CfxAPI) GetConfirmationRiskByHash(ctx context.Context, blockHash types.Hash) (*hexutil.Big, error) {
return api.cfx.WithContext(ctx).GetRawBlockConfirmationRisk(blockHash)
if api.cfx != nil {
return api.cfx.WithContext(ctx).GetRawBlockConfirmationRisk(blockHash)
}

block, err := api.w3c.Eth.BlockByHash(*blockHash.ToCommonHash(), false)
if block == nil || err != nil {
return nil, err
}

// TODO: calculate confirmation risk based on various confirmed blocks.
return HexBig0, nil
}

func (api *CfxAPI) GetStatus(ctx context.Context) (status types.Status, err error) {
if api.cfx != nil {
return api.getCrossSpaceStatus(ctx)
}

var batchElems []rpcp.BatchElem
for _, bn := range []rpcp.BlockNumber{
ethTypes.LatestBlockNumber,
ethTypes.SafeBlockNumber,
ethTypes.FinalizedBlockNumber,
} {
batchElems = append(batchElems, rpcp.BatchElem{
Method: "eth_getBlockByNumber",
Args: []interface{}{bn, false},
Result: new(ethTypes.Block),
})
}

if err := api.w3c.Provider().BatchCallContext(ctx, batchElems); err != nil {
return types.Status{}, err
}

latestBlock := batchElems[0].Result.(*ethTypes.Block)
safeBlock := batchElems[1].Result.(*ethTypes.Block)
finBlock := batchElems[2].Result.(*ethTypes.Block)

chainID := hexutil.Uint64(api.ethNetworkId)
latestBlockNumber := hexutil.Uint64(latestBlock.Number.Uint64())

return types.Status{
BestHash: ConvertHash(latestBlock.Hash),
ChainID: chainID,
EthereumSpaceChainId: chainID,
NetworkID: chainID,
EpochNumber: latestBlockNumber,
BlockNumber: latestBlockNumber,
LatestState: latestBlockNumber,
LatestConfirmed: hexutil.Uint64(safeBlock.Number.Uint64()),
LatestFinalized: hexutil.Uint64(finBlock.Number.Uint64()),
LatestCheckpoint: 0,
PendingTxNumber: 0,
}, nil
}

func (api *CfxAPI) GetStatus(ctx context.Context) (types.Status, error) {
status, err := api.cfx.WithContext(ctx).GetStatus()
func (api *CfxAPI) getCrossSpaceStatus(ctx context.Context) (status types.Status, err error) {
status, err = api.cfx.WithContext(ctx).GetStatus()
if err != nil {
return types.Status{}, err
}
Expand All @@ -305,7 +468,13 @@ func (api *CfxAPI) GetStatus(ctx context.Context) (types.Status, error) {
}

func (api *CfxAPI) GetBlockRewardInfo(ctx context.Context, epoch types.Epoch) ([]types.RewardInfo, error) {
return api.cfx.WithContext(ctx).GetBlockRewardInfo(epoch)
if api.cfx != nil {
return api.cfx.WithContext(ctx).GetBlockRewardInfo(epoch)
}

// TODO: Calculate block reward based on the following implementation:
// https://docs.alchemy.com/docs/how-to-calculate-ethereum-miner-rewards
return []types.RewardInfo{}, nil
}

func (api *CfxAPI) ClientVersion(ctx context.Context) (string, error) {
Expand All @@ -317,15 +486,27 @@ func (api *CfxAPI) GetSupplyInfo(ctx context.Context, epoch *types.Epoch) (types
epoch = types.EpochLatestState
}

result, err := api.cfx.WithContext(ctx).GetSupplyInfo(epoch)
if err != nil {
return types.TokenSupplyInfo{}, err
}
if api.cfx != nil {
result, err := api.cfx.WithContext(ctx).GetSupplyInfo(epoch)
if err != nil {
return types.TokenSupplyInfo{}, err
}

result.TotalCirculating = result.TotalEspaceTokens
result.TotalIssued = result.TotalEspaceTokens
result.TotalStaking = HexBig0
result.TotalCollateral = HexBig0
result.TotalCirculating = result.TotalEspaceTokens
result.TotalIssued = result.TotalEspaceTokens
result.TotalStaking = HexBig0
result.TotalCollateral = HexBig0

return result, nil
return result, nil
}

// TODO: Calculate supply info based on the following implementation:
// https://github.com/lastmjs/eth-total-supply
return types.TokenSupplyInfo{
TotalCirculating: HexBig0,
TotalIssued: HexBig0,
TotalStaking: HexBig0,
TotalCollateral: HexBig0,
TotalEspaceTokens: HexBig0,
}, nil
}
Loading

0 comments on commit 2be6a3a

Please sign in to comment.