区块链应用与以太坊的交互

我们要谈的交互

首先要明确一点,以太坊是一个去中心化的平台,他不可能为了某个项目而新增交互接口。
我这里说的交互是指应用是链上合约的交互,更明确的说,是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
}
posted @ 2024-09-11 17:21  bighu  阅读(131)  评论(0编辑  收藏  举报