Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion devnet-sdk/book/src/shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export ETH_JWT_SECRET=...

```bash
# Enter devnet shell
go run devnet-sdk/shll/cmd/enter/main.go --descriptor devnet.json --chain ...
go run devnet-sdk/shell/cmd/enter/main.go --descriptor devnet.json --chain ...

# Now you can use tools directly
cast block latest
Expand All @@ -72,6 +72,7 @@ exit
## Implementation Details

The shell integration:

1. Reads the descriptor file
2. Sets up environment variables based on the descriptor content
3. Creates a new shell session with the configured environment
Expand Down
20 changes: 12 additions & 8 deletions devnet-sdk/descriptors/deployment.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package descriptors

import "github.com/ethereum-optimism/optimism/devnet-sdk/types"
import (
"github.com/ethereum-optimism/optimism/devnet-sdk/types"
"github.com/ethereum/go-ethereum/params"
)

type PortInfo struct {
Host string `json:"host"`
Expand Down Expand Up @@ -30,13 +33,14 @@ type AddressMap map[string]types.Address

// Chain represents a chain (L1 or L2) in a devnet.
type Chain struct {
Name string `json:"name"`
ID string `json:"id,omitempty"`
Services ServiceMap `json:"services,omitempty"`
Nodes []Node `json:"nodes"`
Addresses AddressMap `json:"addresses,omitempty"`
Wallets WalletMap `json:"wallets,omitempty"`
JWT string `json:"jwt,omitempty"`
Name string `json:"name"`
ID string `json:"id,omitempty"`
Services ServiceMap `json:"services,omitempty"`
Nodes []Node `json:"nodes"`
Addresses AddressMap `json:"addresses,omitempty"`
Wallets WalletMap `json:"wallets,omitempty"`
JWT string `json:"jwt,omitempty"`
ChainConfig *params.ChainConfig `json:"chain_config,omitempty"`
}

// Wallet represents a wallet with an address and optional private key.
Expand Down
54 changes: 48 additions & 6 deletions devnet-sdk/kt/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,30 @@ func NewEnclaveFSWithContext(ctx EnclaveContextIface) *EnclaveFS {
}

type Artifact struct {
reader *tar.Reader
reader *tar.Reader
archiveData []byte
gzipReader *gzip.Reader
}

func (fs *EnclaveFS) GetArtifact(ctx context.Context, name string) (*Artifact, error) {
artifact, err := fs.enclaveCtx.DownloadFilesArtifact(ctx, name)
archiveData, err := fs.enclaveCtx.DownloadFilesArtifact(ctx, name)
if err != nil {
return nil, err
}

buffer := bytes.NewBuffer(artifact)
zipReader, err := gzip.NewReader(buffer)
// Create a new reader for the archive data
buffer := bytes.NewBuffer(archiveData)
gzipReader, err := gzip.NewReader(buffer)
if err != nil {
return nil, err
}
tarReader := tar.NewReader(zipReader)
return &Artifact{reader: tarReader}, nil
tarReader := tar.NewReader(gzipReader)

return &Artifact{
reader: tarReader,
archiveData: archiveData,
gzipReader: gzipReader,
}, nil
}

type ArtifactFileWriter struct {
Expand All @@ -73,7 +81,33 @@ func NewArtifactFileWriter(path string, writer io.Writer) *ArtifactFileWriter {
}
}

// resetReader recreates the tar reader from the stored archive data
func (a *Artifact) resetReader() error {
// Close the existing gzip reader if it exists
if a.gzipReader != nil {
a.gzipReader.Close()
}

// Create a new reader from the stored archive data
buffer := bytes.NewBuffer(a.archiveData)
gzipReader, err := gzip.NewReader(buffer)
if err != nil {
return err
}

a.gzipReader = gzipReader
a.reader = tar.NewReader(gzipReader)
return nil
}

// ExtractFiles extracts specific files from the artifact to the provided writers.
// This function can be called multiple times on the same Artifact instance.
func (a *Artifact) ExtractFiles(writers ...*ArtifactFileWriter) error {
// Reset the reader to the beginning of the archive
if err := a.resetReader(); err != nil {
return err
}

paths := make(map[string]io.Writer)
for _, writer := range writers {
canonicalPath := filepath.Clean(writer.path)
Expand Down Expand Up @@ -149,3 +183,11 @@ func NewArtifactFileReader(path string, reader io.Reader) *ArtifactFileReader {
reader: reader,
}
}

// Close closes the gzip reader and releases resources.
func (a *Artifact) Close() error {
if a.gzipReader != nil {
return a.gzipReader.Close()
}
return nil
}
106 changes: 106 additions & 0 deletions devnet-sdk/kt/fs/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,112 @@ func TestArtifactExtraction(t *testing.T) {
}
}

func TestMultipleExtractCalls(t *testing.T) {
// Create a test artifact with multiple files
files := map[string]string{
"file1.txt": "content1",
"file2.txt": "content2",
"dir/file3.txt": "content3",
}

// Create mock context with artifact
mockCtx := &mockEnclaveContext{
artifacts: map[string][]byte{
"test-artifact": createTarGzArtifact(t, files),
},
}

fs := NewEnclaveFSWithContext(mockCtx)
artifact, err := fs.GetArtifact(context.Background(), "test-artifact")
require.NoError(t, err)
defer artifact.Close()

// First extraction - extract file1.txt
buf1 := &bytes.Buffer{}
writer1 := NewArtifactFileWriter("file1.txt", buf1)
err = artifact.ExtractFiles(writer1)
require.NoError(t, err)
require.Equal(t, "content1", buf1.String(), "content mismatch for file1.txt on first extraction")

// Second extraction - extract file2.txt
buf2 := &bytes.Buffer{}
writer2 := NewArtifactFileWriter("file2.txt", buf2)
err = artifact.ExtractFiles(writer2)
require.NoError(t, err)
require.Equal(t, "content2", buf2.String(), "content mismatch for file2.txt on second extraction")

// Third extraction - extract multiple files at once
buf3 := &bytes.Buffer{}
buf4 := &bytes.Buffer{}
writer3 := NewArtifactFileWriter("file1.txt", buf3)
writer4 := NewArtifactFileWriter("dir/file3.txt", buf4)
err = artifact.ExtractFiles(writer3, writer4)
require.NoError(t, err)
require.Equal(t, "content1", buf3.String(), "content mismatch for file1.txt on third extraction")
require.Equal(t, "content3", buf4.String(), "content mismatch for dir/file3.txt on third extraction")
}

func TestComplexExtractionScenarios(t *testing.T) {
// Create a test artifact with files containing longer content
longContent1 := "This is a longer content that will be extracted in parts\nIt has multiple lines\nAnd should be extractable multiple times"
longContent2 := "Another file with content\nThat spans multiple lines\nAnd should be extractable"

files := map[string]string{
"config.json": `{"key1":"value1","key2":"value2","nested":{"inner":"value"}}`,
"data.txt": longContent1,
"log.txt": longContent2,
}

// Create mock context with artifact
mockCtx := &mockEnclaveContext{
artifacts: map[string][]byte{
"test-artifact": createTarGzArtifact(t, files),
},
}

fs := NewEnclaveFSWithContext(mockCtx)
artifact, err := fs.GetArtifact(context.Background(), "test-artifact")
require.NoError(t, err)
defer artifact.Close()

// Test case 1: Extract all files first
bufAll := make(map[string]*bytes.Buffer)
var writersAll []*ArtifactFileWriter

for path := range files {
bufAll[path] = &bytes.Buffer{}
writersAll = append(writersAll, NewArtifactFileWriter(path, bufAll[path]))
}

err = artifact.ExtractFiles(writersAll...)
require.NoError(t, err)

// Verify all contents
for path, content := range files {
require.Equal(t, content, bufAll[path].String(), "content mismatch for %s", path)
}

// Test case 2: Now extract each file individually and verify
for path, content := range files {
buf := &bytes.Buffer{}
writer := NewArtifactFileWriter(path, buf)

err = artifact.ExtractFiles(writer)
require.NoError(t, err)
require.Equal(t, content, buf.String(), "individual extraction failed for %s", path)
}

// Test case 3: Extract the same file multiple times
for i := 0; i < 3; i++ {
buf := &bytes.Buffer{}
writer := NewArtifactFileWriter("data.txt", buf)

err = artifact.ExtractFiles(writer)
require.NoError(t, err)
require.Equal(t, longContent1, buf.String(), "repeated extraction %d failed for data.txt", i)
}
}

func TestPutArtifact(t *testing.T) {
tests := []struct {
name string
Expand Down
63 changes: 53 additions & 10 deletions devnet-sdk/system/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/ethereum/go-ethereum/common"
coreTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/params"
)

var (
Expand Down Expand Up @@ -61,22 +62,26 @@ type chain struct {
id string
rpcUrl string

users map[string]Wallet
clients *clientManager
registry interfaces.ContractsRegistry
mu sync.Mutex
users map[string]Wallet
clients *clientManager
registry interfaces.ContractsRegistry
mu sync.Mutex
chainConfig *params.ChainConfig
addresses map[string]types.Address
}

func (c *chain) Client() (*ethclient.Client, error) {
return c.clients.Client(c.rpcUrl)
}

func newChain(chainID string, rpcUrl string, users map[string]Wallet) *chain {
func newChain(chainID string, rpcUrl string, users map[string]Wallet, chainConfig *params.ChainConfig, addresses map[string]types.Address) *chain {
return &chain{
id: chainID,
rpcUrl: rpcUrl,
users: users,
clients: newClientManager(),
id: chainID,
rpcUrl: rpcUrl,
users: users,
clients: newClientManager(),
chainConfig: chainConfig,
addresses: addresses,
}
}

Expand Down Expand Up @@ -189,15 +194,53 @@ func (c *chain) SupportsEIP(ctx context.Context, eip uint64) bool {
return false
}

func (c *chain) ChainConfig() (*params.ChainConfig, error) {
if c.chainConfig == nil {
return nil, fmt.Errorf("chain config not configured on L1 chains yet")
}
return c.chainConfig, nil
}

func (c *chain) BlockByHash(ctx context.Context, hash common.Hash) (*coreTypes.Block, error) {
client, err := c.Client()
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
return client.BlockByHash(ctx, hash)
}

func (c *chain) BlockByNumber(ctx context.Context, number *big.Int) (*coreTypes.Block, error) {
client, err := c.Client()
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
return client.BlockByNumber(ctx, number)
}

func (c *chain) LatestBlock(ctx context.Context) (*coreTypes.Block, error) {
client, err := c.Client()
if err != nil {
return nil, fmt.Errorf("failed to get client: %w", err)
}
return client.BlockByNumber(ctx, nil) // nil means latest block in geth
}

func (c *chain) Addresses() map[string]common.Address {
return c.addresses
}

func chainFromDescriptor(d *descriptors.Chain) (Chain, error) {
// TODO: handle incorrect descriptors better. We could panic here.
firstNodeRPC := d.Nodes[0].Services["el"].Endpoints["rpc"]
rpcURL := fmt.Sprintf("http://%s:%d", firstNodeRPC.Host, firstNodeRPC.Port)

c := newChain(d.ID, rpcURL, nil) // Create chain first
c := newChain(d.ID, rpcURL, nil, d.ChainConfig, d.Addresses) // Create chain first

users := make(map[string]Wallet)
for key, w := range d.Wallets {
// TODO: The assumption that the wallet will necessarily be used on chain `d` may
// be problematic if the L2 admin wallets are to be used to sign L1 transactions.
// TBD on whether they belong somewhere other than `d.Wallets`.
k, err := newWallet(w.PrivateKey, w.Address, c)
if err != nil {
return nil, fmt.Errorf("failed to create wallet: %w", err)
Expand Down
10 changes: 5 additions & 5 deletions devnet-sdk/system/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/ethereum-optimism/optimism/devnet-sdk/descriptors"
"github.com/ethereum-optimism/optimism/devnet-sdk/types"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -90,8 +91,7 @@ func TestChainWallet(t *testing.T) {
assert.Nil(t, err)

chain := newChain("1", "http://localhost:8545", map[string]Wallet{
"user1": wallet,
})
"user1": wallet}, nil, map[string]common.Address{})

t.Run("finds wallet meeting constraints", func(t *testing.T) {
constraint := &addressConstraint{addr: testAddr}
Expand Down Expand Up @@ -156,7 +156,7 @@ func TestChainID(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
chain := newChain(tt.idString, "", nil)
chain := newChain(tt.idString, "", nil, nil, map[string]types.Address{})
got := chain.ID()
// Compare the underlying big.Int values
assert.Equal(t, 0, tt.want.Cmp(got))
Expand All @@ -166,7 +166,7 @@ func TestChainID(t *testing.T) {

func TestSupportsEIP(t *testing.T) {
ctx := context.Background()
chain := newChain("1", "http://localhost:8545", nil)
chain := newChain("1", "http://localhost:8545", nil, nil, map[string]types.Address{})

// Since we can't reliably test against a live node, we're just testing the error case
t.Run("returns false for connection error", func(t *testing.T) {
Expand All @@ -176,7 +176,7 @@ func TestSupportsEIP(t *testing.T) {
}

func TestContractsRegistry(t *testing.T) {
chain := newChain("1", "http://localhost:8545", nil)
chain := newChain("1", "http://localhost:8545", nil, nil, map[string]types.Address{})

t.Run("returns empty registry on error", func(t *testing.T) {
registry := chain.ContractsRegistry()
Expand Down
Loading