|
| 1 | +package provider |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "errors" |
| 6 | + "math/big" |
| 7 | + "strconv" |
| 8 | + "sync" |
| 9 | + "testing" |
| 10 | + "time" |
| 11 | + |
| 12 | + "github.com/ethereum/go-ethereum/accounts/abi/bind" |
| 13 | + "github.com/ethereum/go-ethereum/common" |
| 14 | + "github.com/ethereum/go-ethereum/core/types" |
| 15 | + "github.com/ethereum/go-ethereum/crypto" |
| 16 | + "github.com/ethereum/go-ethereum/ethclient" |
| 17 | + chain_selectors "github.com/smartcontractkit/chain-selectors" |
| 18 | + "github.com/smartcontractkit/chainlink-testing-framework/framework" |
| 19 | + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" |
| 20 | + "github.com/smartcontractkit/freeport" |
| 21 | + "github.com/stretchr/testify/require" |
| 22 | + "github.com/testcontainers/testcontainers-go" |
| 23 | + "github.com/zksync-sdk/zksync2-go/accounts" |
| 24 | + "github.com/zksync-sdk/zksync2-go/clients" |
| 25 | + |
| 26 | + "github.com/smartcontractkit/chainlink-deployments-framework/chain" |
| 27 | + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" |
| 28 | +) |
| 29 | + |
| 30 | +// ZkCTFChainProviderConfig holds the configuration to initialize the ZkSyncCTFChainProvider. |
| 31 | +type ZkSyncCTFChainProviderConfig struct { |
| 32 | + // Required: A sync.Once instance to ensure that the CTF framework only sets up the new |
| 33 | + // DefaultNetwork once |
| 34 | + Once *sync.Once |
| 35 | +} |
| 36 | + |
| 37 | +// validate checks if the config fields are valid. |
| 38 | +func (c ZkSyncCTFChainProviderConfig) validate() error { |
| 39 | + if c.Once == nil { |
| 40 | + return errors.New("sync.Once instance is required") |
| 41 | + } |
| 42 | + |
| 43 | + return nil |
| 44 | +} |
| 45 | + |
| 46 | +var _ chain.Provider = (*ZkSyncCTFChainProvider)(nil) |
| 47 | + |
| 48 | +// ZkSyncCTFChainProvider manages an ZkSync EVM chain instance running inside a (CTF) Docker |
| 49 | +// container. |
| 50 | +// |
| 51 | +// This provider requires Docker to be installed and operational. Spinning up a new container |
| 52 | +// can be slow, so it is recommended to initialize the provider only once per test suite or parent |
| 53 | +// test to optimize performance. |
| 54 | +type ZkSyncCTFChainProvider struct { |
| 55 | + t *testing.T |
| 56 | + selector uint64 |
| 57 | + config ZkSyncCTFChainProviderConfig |
| 58 | + |
| 59 | + chain *evm.Chain |
| 60 | +} |
| 61 | + |
| 62 | +// NewZkCTFChainProvider creates a new ZkSyncCTFChainProvider with the given selector and |
| 63 | +// configuration. |
| 64 | +func NewZkSyncCTFChainProvider( |
| 65 | + t *testing.T, selector uint64, config ZkSyncCTFChainProviderConfig, |
| 66 | +) *ZkSyncCTFChainProvider { |
| 67 | + t.Helper() |
| 68 | + |
| 69 | + p := &ZkSyncCTFChainProvider{ |
| 70 | + t: t, |
| 71 | + selector: selector, |
| 72 | + config: config, |
| 73 | + } |
| 74 | + |
| 75 | + return p |
| 76 | +} |
| 77 | + |
| 78 | +// Initialize sets up the ZkSync EVM chain instance managed by this provider. It starts a CTF |
| 79 | +// container, initializes the Ethereum client, and sets up the chain instance with the necessary |
| 80 | +// transactors and deployer key gathered from the CTF's default zkSync accounts. The first |
| 81 | +// account is used as the deployer key, and the rest are used as users for the chain. |
| 82 | +func (p *ZkSyncCTFChainProvider) Initialize(ctx context.Context) (chain.BlockChain, error) { |
| 83 | + if p.chain != nil { |
| 84 | + return *p.chain, nil // Already initialized |
| 85 | + } |
| 86 | + |
| 87 | + err := p.config.validate() |
| 88 | + require.NoError(p.t, err, "failed to validate provider config") |
| 89 | + |
| 90 | + // Get the Chain ID |
| 91 | + chainID, err := chain_selectors.GetChainIDFromSelector(p.selector) |
| 92 | + require.NoError(p.t, err, "failed to get chain ID from selector") |
| 93 | + |
| 94 | + // Start the Zksync CTF container |
| 95 | + httpURL := p.startContainer(chainID) |
| 96 | + |
| 97 | + // Setup the Ethereum client |
| 98 | + client, err := ethclient.Dial(httpURL) |
| 99 | + require.NoError(p.t, err) |
| 100 | + |
| 101 | + // Fetch the suggested gas price for the chain to set on the transactors. |
| 102 | + // Anvil zkSync does not support eth_maxPriorityFeePerGas so we set gasPrice to force using |
| 103 | + // createLegacyTx |
| 104 | + gasPrice, err := client.SuggestGasPrice(ctx) |
| 105 | + require.NoError(p.t, err) |
| 106 | + |
| 107 | + // Build transactors from the default accounts provided by the CTF |
| 108 | + transactors := p.getTransactors(chainID, gasPrice) |
| 109 | + |
| 110 | + // Initialize the zksync client and wallet |
| 111 | + clientZk := clients.NewClient(client.Client()) |
| 112 | + deployerZk, err := accounts.NewWallet( |
| 113 | + common.Hex2Bytes(blockchain.AnvilZKSyncRichAccountPks[0]), clientZk, nil, |
| 114 | + ) |
| 115 | + require.NoError(p.t, err, "failed to create deployer wallet for ZkSync") |
| 116 | + |
| 117 | + // Construct the chain |
| 118 | + p.chain = &evm.Chain{ |
| 119 | + Selector: p.selector, |
| 120 | + Client: client, |
| 121 | + DeployerKey: transactors[0], // The first transactor is the deployer |
| 122 | + Users: transactors[1:], // The rest are users |
| 123 | + Confirm: func(tx *types.Transaction) (uint64, error) { |
| 124 | + ctxWithTimeout, cancel := context.WithTimeout(ctx, 2*time.Minute) |
| 125 | + defer cancel() |
| 126 | + |
| 127 | + receipt, err := bind.WaitMined(ctxWithTimeout, client, tx) |
| 128 | + if err != nil { |
| 129 | + return 0, err |
| 130 | + } |
| 131 | + |
| 132 | + return receipt.Status, nil |
| 133 | + }, |
| 134 | + IsZkSyncVM: true, |
| 135 | + ClientZkSyncVM: clientZk, |
| 136 | + DeployerKeyZkSyncVM: deployerZk, |
| 137 | + } |
| 138 | + |
| 139 | + return *p.chain, nil |
| 140 | +} |
| 141 | + |
| 142 | +// Name returns the name of the ZkSyncCTFChainProvider. |
| 143 | +func (*ZkSyncCTFChainProvider) Name() string { |
| 144 | + return "ZkSync EVM CTF Chain Provider" |
| 145 | +} |
| 146 | + |
| 147 | +// ChainSelector returns the chain selector of the ZkSync EVM chain managed by this provider. |
| 148 | +func (p *ZkSyncCTFChainProvider) ChainSelector() uint64 { |
| 149 | + return p.selector |
| 150 | +} |
| 151 | + |
| 152 | +// BlockChain returns the ZkSync EVM chain instance managed by this provider. You must call Initialize |
| 153 | +// before using this method to ensure the chain is properly set up. |
| 154 | +func (p *ZkSyncCTFChainProvider) BlockChain() chain.BlockChain { |
| 155 | + return *p.chain |
| 156 | +} |
| 157 | + |
| 158 | +// startContainer starts a CTF container for the ZkSync EVM returning the HTTP URL of the node. |
| 159 | +func (p *ZkSyncCTFChainProvider) startContainer( |
| 160 | + chainID string, |
| 161 | +) string { |
| 162 | + // initialize the docker network used by CTF |
| 163 | + err := framework.DefaultNetwork(p.config.Once) |
| 164 | + require.NoError(p.t, err) |
| 165 | + |
| 166 | + // Initialize a port for the container |
| 167 | + port := freeport.GetOne(p.t) |
| 168 | + |
| 169 | + // Create the CTF container for ZkSync |
| 170 | + output, err := blockchain.NewBlockchainNetwork(&blockchain.Input{ |
| 171 | + Type: "anvil-zksync", |
| 172 | + ChainID: chainID, |
| 173 | + Port: strconv.Itoa(port), |
| 174 | + }) |
| 175 | + require.NoError(p.t, err) |
| 176 | + testcontainers.CleanupContainer(p.t, output.Container) |
| 177 | + |
| 178 | + return output.Nodes[0].ExternalHTTPUrl |
| 179 | +} |
| 180 | + |
| 181 | +// getTransactors generates transactors from the default list of accounts provided by the CTF. |
| 182 | +func (p *ZkSyncCTFChainProvider) getTransactors( |
| 183 | + chainID string, gasPrice *big.Int, |
| 184 | +) []*bind.TransactOpts { |
| 185 | + require.Greater(p.t, len(blockchain.AnvilZKSyncRichAccountPks), 1) |
| 186 | + |
| 187 | + cid, ok := new(big.Int).SetString(chainID, 10) |
| 188 | + if !ok { |
| 189 | + require.FailNowf(p.t, "failed to parse chain ID into big.Int: %s", chainID) |
| 190 | + } |
| 191 | + |
| 192 | + transactors := make([]*bind.TransactOpts, 0) |
| 193 | + for _, pk := range blockchain.AnvilZKSyncRichAccountPks { |
| 194 | + privateKey, err := crypto.HexToECDSA(pk) |
| 195 | + require.NoError(p.t, err) |
| 196 | + |
| 197 | + transactor, err := bind.NewKeyedTransactorWithChainID(privateKey, cid) |
| 198 | + transactor.GasPrice = gasPrice |
| 199 | + require.NoError(p.t, err) |
| 200 | + |
| 201 | + transactors = append(transactors, transactor) |
| 202 | + } |
| 203 | + |
| 204 | + return transactors |
| 205 | +} |
0 commit comments