Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

这篇文章深入探讨了以太坊的交互机制,强调通过以太坊支持的工具链实现链上合约与链下应用(如Chainlink和Arbitrum)的有效交互,特别是在合约调用、事件监控及交易构造等方面的具体实现和注意事项。

我们要谈的交互

首先要明确一点,以太坊是一个去中心化的平台,他不可能为了某个项目而新增交互接口。
我这里说的交互是指应用是链上合约的交互,更明确的说,是chainlink,arbitrum,cosmos这些链下应用与链上合约的交互。
所以这里我不是想说以下交互方式:

  • 通过钱包交互:将应用打包成一个网页,连接类似与小狐狸 这样的钱包,与链发生交互。
  • 手动构造交易:构建交易tx并使用私钥对交易进行签名,然后直接发送到链给定的接口上。

对于一些简单的调用,通过上面两种方式是可行的,例如我们只是做一些nft的构造,通过钱包是最合适的。但是对于向arbitrum这类
链上的应用,如果要做一个交互式单步证明,在这个过程中我需要监控链上合约抛出的event,分析event并构造出相应结果。
这个时候钱包就很难插手,而如果主动构造交易并签名,那么过程太繁琐。实际上以太坊上已经提供了相关的工具链。

我们要谈的交互方式就是,通过以太坊支持的工具链实现与以太坊的交互

工具链

工具链

简单而言是使用以太坊工具将sol的合约代码转换成go的类文件,并对调用细节进行封装。
而在应用层(arbitrum,chainlink这一层)可以直接将对应参数传过去就可以.

以arbitrum为例:

生成go代码

生成工具在: https://github.com/OffchainLabs/nitro/blob/master/solgen/gen.go
由于arbitrum用到链makefile,所我们没法通过运行这个文件(go run gen.go)的方式,去生成合约文件。
不过实际上这是一个路径的问题,在代码的第71行:

    filePaths, err := filepath.Glob(filepath.Join(parent, "contracts", "build", "contracts", "src", "*", "*.sol", "*.json"))
    if err != nil {
        log.Fatal(err)
    }

    filePathsSafeSmartAccount, err := filepath.Glob(filepath.Join(parent, "safe-smart-account", "build", "artifacts", "contracts", "*", "*.sol", "*.json"))
    if err != nil {
        log.Fatal(err)
    }
    filePathsSafeSmartAccountOuter, err := filepath.Glob(filepath.Join(parent, "safe-smart-account", "build", "artifacts", "contracts", "*.sol", "*.json"))
    if err != nil {
        log.Fatal(err)
    }

这里实际上就指定了合约代码的路径,当然如果只是初次下载合约文件应该是看不到build目录的,需要在合约所在项目构建一下,才能生成这个build文件

构造方法:

yarn --cwd contracts build
yarn --cwd contracts build:forge:yul
# 其实就是hardhat compile的产物

然后就会在solgen这个目录下生成对应的go文件
solgen

我们具体看一下这个生成的代码怎么用

使用生成的代码

首先可以看到在生成的代码中,每一个合约都有一个对应的类
eg:

// ChallengeLibTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
type ChallengeLibTransactorRaw struct {
    Contract *ChallengeLibTransactor // Generic write-only contract binding to access the raw methods on
}

合约中的方法则对应到类的方法

eg:

// Solidity: function oneStepProveExecution(uint64 challengeIndex, (uint256,uint256,bytes32[],uint256) selection, bytes proof) returns()
func (_ChallengeManager *ChallengeManagerTransactor) OneStepProveExecution(opts *bind.TransactOpts, challengeIndex uint64, selection ChallengeLibSegmentSelection, proof []byte) (*types.Transaction, error) {
    return _ChallengeManager.contract.Transact(opts, "oneStepProveExecution", challengeIndex, selection, proof)
}

其中关键在与这里的opts,如果我们继续进到这个Transact方法里面会发现,链上的信息都是由这里的opts获取的,用户签名接口,用户信息等

那么对于一个合约调用就分为两部分,一是调用参数,也就是这里的opts和合约参数,一是对接一台的的client

参数

进入到这里的opts,这是以太坊里面的数据结构

// valid Ethereum transaction.
type TransactOpts struct {
    From   common.Address // Ethereum account to send the transaction from
    Nonce  *big.Int       // Nonce to use for the transaction execution (nil = use pending state)
    Signer SignerFn       // Method to use for signing the transaction (mandatory)

    Value     *big.Int // Funds to transfer along the transaction (nil = 0 = no funds)
    GasPrice  *big.Int // Gas price to use for the transaction execution (nil = gas price oracle)
    GasFeeCap *big.Int // Gas fee cap to use for the 1559 transaction execution (nil = gas price oracle)
    GasTipCap *big.Int // Gas priority fee cap to use for the 1559 transaction execution (nil = gas price oracle)
    GasLimit  uint64   // Gas limit to set for the transaction execution (0 = estimate)
    GasMargin uint64   // Arbitrum: adjusts gas estimate by this many basis points (0 = no adjustment)

    Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)

    NoSend bool // Do all transact steps but do not send the transaction
}

我们可以看到,这里包含用户信息的签名数据

对于链下的开发者,我们需要构造这个结构,来调用方法

client

我们有链合约的调用参数,那么就需要有一个client来为我们发送交易,(虽然构造交易的时候也用到链client,但这都是已经被工具封装好的,开发者没必要细究它是怎么构建的)

实际上client是在我们构建合约对象时构建的

// NewChallengeManager creates a new instance of ChallengeManager, bound to a specific deployed contract.
func NewChallengeManager(address common.Address, backend bind.ContractBackend) (*ChallengeManager, error) {
    contract, err := bindChallengeManager(address, backend, backend, backend)
    if err != nil {
        return nil, err
    }
    return &ChallengeManager{ChallengeManagerCaller: ChallengeManagerCaller{contract: contract}, ChallengeManagerTransactor: ChallengeManagerTransactor{contract: contract}, ChallengeManagerFilterer: ChallengeManagerFilterer{contract: contract}}, nil
}

在构建ChallengeManger这个合约对象时,我们需要给他一个backend,这里的backend就是链的client,地址也就是链上的合约地址

可以看到backend也是bind这个包里面的,实际上它也是以太坊源码里面的包

type ContractBackend interface {
    ContractCaller
    ContractTransactor
    ContractFilterer
}

type ContractCaller interface {
    // CodeAt returns the code of the given account. This is needed to differentiate
    // between contract internal errors and the local chain being out of sync.
    CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error)

    // CallContract executes an Ethereum contract call with the specified data as the
    // input.
    CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
}
type ContractTransactor interface {
    ethereum.GasEstimator
    ethereum.GasPricer
    ethereum.GasPricer1559
    ethereum.TransactionSender

    // HeaderByNumber returns a block header from the current canonical chain. If
    // number is nil, the latest known header is returned.
    HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)

    // PendingCodeAt returns the code of the given account in the pending state.
    PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error)

    // PendingNonceAt retrieves the current pending nonce associated with an account.
    PendingNonceAt(ctx context.Context, account common.Address) (uint64, error)
}

type ContractFilterer interface {
    ethereum.LogFilterer
}

这个client看起来构造很麻烦,实际上也是有迹可循的,这里面都是以太坊里的数据结构,所以理论上以太坊里面已经有对象实现了这些接口

type Client struct {
    c rpc.ClientInterface
}
type ClientInterface interface {
    CallContext(ctx_in context.Context, result interface{}, method string, args ...interface{}) error
    EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*ClientSubscription, error)
    BatchCallContext(ctx context.Context, b []BatchElem) error
    Close()
}

// Client represents a connection to an RPC server.
type Client struct {
    idgen    func() ID // for subscriptions
    isHTTP   bool      // connection type: http, ws or ipc
    services *serviceRegistry

    idCounter atomic.Uint32

    // This function, if non-nil, is called when the connection is lost.
    reconnectFunc reconnectFunc

    // config fields
    batchItemLimit       int
    batchResponseMaxSize int

    // writeConn is used for writing to the connection on the caller's goroutine. It should
    // only be accessed outside of dispatch, with the write lock held. The write lock is
    // taken by sending on reqInit and released by sending on reqSent.
    writeConn jsonWriter

    // for dispatch
    close       chan struct{}
    closing     chan struct{}    // closed when client is quitting
    didClose    chan struct{}    // closed when client quits
    reconnected chan ServerCodec // where write/reconnect sends the new connection
    readOp      chan readOp      // read messages
    readErr     chan error       // errors from read
    reqInit     chan *requestOp  // register response IDs, takes write lock
    reqSent     chan error       // signals write completion, releases write lock
    reqTimeout  chan *requestOp  // removes response IDs when call timeout expires
}

评论