Skip to content

Commit f5a2ca9

Browse files
authored
feat: add a new provider for zkSync EVM CTF (#166)
This adds a new provider for zkSync EVM CTF, which allows users to interact with the zkSync CTF environment. This sets up an evm.Chain with the zkSync specific fields populated.
1 parent b76e365 commit f5a2ca9

File tree

10 files changed

+344
-8
lines changed

10 files changed

+344
-8
lines changed

.changeset/shaky-plums-say.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Adds a zkSync CTF provider under the EVM Chain

chain/aptos/provider/ctf_provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func (p *CTFChainProvider) ChainSelector() uint64 {
135135
// BlockChain returns the Aptos chain instance managed by this provider. You must call Initialize
136136
// before using this method to ensure the chain is properly set up.
137137
func (p *CTFChainProvider) BlockChain() chain.BlockChain {
138-
return p.chain
138+
return *p.chain
139139
}
140140

141141
// startContainer starts a CTF container for the Aptos chain with the given chain ID and deployer account.

chain/aptos/provider/ctf_provider_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,5 +153,5 @@ func Test_CTFChainProvider_BlockChain(t *testing.T) {
153153
chain: chain,
154154
}
155155

156-
assert.Equal(t, chain, p.BlockChain())
156+
assert.Equal(t, *chain, p.BlockChain())
157157
}

chain/aptos/provider/rpc_provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,5 @@ func (p *RPCChainProvider) ChainSelector() uint64 {
129129
// BlockChain returns the Aptos chain instance managed by this provider. You must call Initialize
130130
// before using this method to ensure the chain is properly set up.
131131
func (p *RPCChainProvider) BlockChain() chain.BlockChain {
132-
return p.chain
132+
return *p.chain
133133
}

chain/aptos/provider/rpc_provider_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,5 @@ func Test_RPCChainProvider_BlockChain(t *testing.T) {
151151
chain: chain,
152152
}
153153

154-
assert.Equal(t, chain, p.BlockChain())
154+
assert.Equal(t, *chain, p.BlockChain())
155155
}

chain/evm/evm_chain.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import (
1414
)
1515

1616
// OnchainClient is an EVM chain client.
17-
// For EVM specifically we can use existing geth interface
18-
// to abstract chain clients.
17+
// For EVM specifically we can use existing geth interface to abstract chain clients.
1918
type OnchainClient interface {
2019
bind.ContractBackend
2120
bind.DeployBackend
21+
2222
BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error)
2323
NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error)
2424
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package provider
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
chain_selectors "github.com/smartcontractkit/chain-selectors"
10+
11+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/testutils"
13+
)
14+
15+
func Test_ZkSyncCTFChainProviderConfig_validate(t *testing.T) {
16+
t.Parallel()
17+
18+
tests := []struct {
19+
name string
20+
config ZkSyncCTFChainProviderConfig
21+
wantErr string
22+
}{
23+
{
24+
name: "valid config",
25+
config: ZkSyncCTFChainProviderConfig{
26+
Once: testutils.DefaultNetworkOnce,
27+
},
28+
wantErr: "",
29+
},
30+
{
31+
name: "missing sync.Once instance",
32+
config: ZkSyncCTFChainProviderConfig{
33+
Once: nil,
34+
},
35+
wantErr: "sync.Once instance is required",
36+
},
37+
}
38+
39+
for _, tt := range tests {
40+
t.Run(tt.name, func(t *testing.T) {
41+
t.Parallel()
42+
43+
err := tt.config.validate()
44+
if tt.wantErr != "" {
45+
require.ErrorContains(t, err, tt.wantErr)
46+
} else {
47+
require.NoError(t, err)
48+
}
49+
})
50+
}
51+
}
52+
53+
func Test_CTFChainProvider_Initialize(t *testing.T) {
54+
t.Parallel()
55+
56+
var chainSelector = chain_selectors.TEST_1000.Selector
57+
58+
tests := []struct {
59+
name string
60+
giveSelector uint64
61+
giveConfig ZkSyncCTFChainProviderConfig
62+
wantErr string
63+
}{
64+
{
65+
name: "valid initialization",
66+
giveSelector: chainSelector,
67+
giveConfig: ZkSyncCTFChainProviderConfig{
68+
Once: testutils.DefaultNetworkOnce,
69+
},
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
t.Parallel()
76+
77+
p := NewZkSyncCTFChainProvider(t, tt.giveSelector, tt.giveConfig)
78+
79+
got, err := p.Initialize(t.Context())
80+
if tt.wantErr != "" {
81+
require.ErrorContains(t, err, tt.wantErr)
82+
} else {
83+
require.NoError(t, err)
84+
assert.NotNil(t, p.chain)
85+
86+
// Check that the chain is of type *aptos.Chain and has the expected fields
87+
gotChain, ok := got.(evm.Chain)
88+
require.True(t, ok, "expected got to be of type aptos.Chain")
89+
assert.Equal(t, tt.giveSelector, gotChain.Selector)
90+
assert.NotEmpty(t, gotChain.Client)
91+
assert.NotEmpty(t, gotChain.DeployerKey)
92+
assert.NotEmpty(t, gotChain.Users)
93+
assert.NotNil(t, gotChain.Confirm)
94+
assert.True(t, gotChain.IsZkSyncVM)
95+
assert.NotNil(t, gotChain.ClientZkSyncVM)
96+
assert.NotNil(t, gotChain.DeployerKeyZkSyncVM)
97+
}
98+
})
99+
}
100+
}
101+
102+
func Test_ZkSyncCTFChainProvider_Name(t *testing.T) {
103+
t.Parallel()
104+
105+
p := &ZkSyncCTFChainProvider{}
106+
assert.Equal(t, "ZkSync EVM CTF Chain Provider", p.Name())
107+
}
108+
109+
func Test_ZkSyncCTFChainProvider_ChainSelector(t *testing.T) {
110+
t.Parallel()
111+
112+
p := &ZkSyncCTFChainProvider{selector: chain_selectors.TEST_1000.Selector}
113+
assert.Equal(t, chain_selectors.TEST_1000.Selector, p.ChainSelector())
114+
}
115+
116+
func Test_ZkSyncCTFChainProvider_BlockChain(t *testing.T) {
117+
t.Parallel()
118+
119+
chain := &evm.Chain{}
120+
121+
p := &ZkSyncCTFChainProvider{
122+
chain: chain,
123+
}
124+
125+
assert.Equal(t, *chain, p.BlockChain())
126+
}

chain/solana/provider/ctf_provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ func (p *CTFChainProvider) ChainSelector() uint64 {
185185
// BlockChain returns the Solana chain instance managed by this provider. You must call Initialize
186186
// before using this method to ensure the chain is properly set up.
187187
func (p *CTFChainProvider) BlockChain() chain.BlockChain {
188-
return p.chain
188+
return *p.chain
189189
}
190190

191191
// startContainer starts a CTF container for the Solana chain.

chain/solana/provider/ctf_provider_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,5 @@ func Test_CTFChainProvider_BlockChain(t *testing.T) {
203203
chain: chain,
204204
}
205205

206-
assert.Equal(t, chain, p.BlockChain())
206+
assert.Equal(t, *chain, p.BlockChain())
207207
}

0 commit comments

Comments
 (0)