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
148 changes: 148 additions & 0 deletions specs/contract-deployment-registry/contracts/registry-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Contract: the registry package's exported surface

Phase 1 output. These are the signatures the generator and the harness call. They
come before implementation, per the constitution's second principle.

## Loading

```go
// Load returns the registry the binary carries, with any supplied files layered
// over it. A supplied file naming a chain the binary carries replaces that
// chain's entry, and Load records which source won (CDR-018, CDR-019, CDR-020).
func Load(paths ...string) (*Registry, error)

// Sources reports which file supplied each chain, for the run to log.
func (r *Registry) Sources() map[string]string
```

`Load()` with no paths returns the embedded registry alone. That is the long-lived
chain case, and it touches no disk and no network.

## Lookup

```go
// Chain returns the entry matching both the chain id and the genesis hash
// (CDR-015). It reports false when no entry matches either.
func (r *Registry) Chain(chainID int64, genesisHash string) (Chain, bool)

// Contract returns one named contract. It reports false when the chain carries
// no entry for that name, which is the deploy case (CDR-003).
func (c Chain) Contract(name string) (Contract, bool)
```

Two returns rather than an error. A missing chain and a missing contract are both
ordinary, and both lead to a deployment rather than a failure.

## Verification

```go
// CodeReader is the one chain call this package makes. *ethclient.Client and
// bind.ContractBackend both satisfy it, and a test supplies a fake.
type CodeReader interface {
CodeAt(ctx context.Context, account common.Address, block *big.Int) ([]byte, error)
}

// Verify checks that the code at the recorded address hashes to the recorded
// hash (CDR-006, CDR-007). It returns a *MismatchError naming the chain, the
// contract, the address, and both hashes (CDR-008). It never deploys (CDR-009).
func Verify(ctx context.Context, code CodeReader, chain Chain, name string) error

// MismatchError is the failure CDR-008 describes. It is a distinct type so a
// caller can tell a stale registry from a dial failure.
type MismatchError struct {
ChainName string
ChainID int64
ContractName string
Address common.Address
Want common.Hash // recorded at deployment
Got common.Hash // observed now; the zero hash means no code
}

func (e *MismatchError) Error() string
```

`Got` holds the zero hash for the absent-code case. A caller therefore needs one
error type, not two, and the message tells the two apart.

## Recording

```go
// Record reads the code at a freshly deployed address and returns the entry to
// write (CDR-010). It does not write anything.
func Record(ctx context.Context, code CodeReader, name string,
addr common.Address) (Contract, error)

// WriteChain writes a chain file for an operator to review and commit. It fails
// if path names a file inside the embedded chains directory (CDR-011).
func WriteChain(path string, chain Chain) error
```

`WriteChain` refusing to write into `registry/chains/` is what makes CDR-011
enforceable rather than a convention. Pointing a run at the committed registry
still does not let it edit that registry.

## What the generator calls

`Resolve` answers one question: does this contract already exist on this chain?

```go
// Resolve returns the address for a named contract, and reports whether the
// caller needs to deploy. It verifies first (CDR-006, CDR-007).
func Resolve(ctx context.Context, code CodeReader, r *Registry,
chainID int64, genesisHash, name string) (common.Address, bool, error)
```

It returns `(addr, false, nil)` to bind, `(zero, true, nil)` to deploy, and an
error when a recorded address failed verification. A profile forcing a deployment
(CDR-016) does not call it.

`Resolve` is the registry's whole contribution. Binding is the caller's, and the
caller does it once for every contract the profile needs, before load generation
starts.

## The seam load generation sees

A scenario receives a bound contract and never an address (CDR-021). The
hand-off already exists on `ContractDeployer[T]`:

```go
GetBindFunc() ContractBindFunc[T] // the caller uses this to bind
SetContract(contract *T) // the caller hands the instance over
```

The preparation step lives outside `registry` and outside the scenario. It:

1. reads the profile and collects the contracts its scenarios need (CDR-022),
2. calls `Resolve` for each, and deploys where the registry has nothing,
3. binds each result with one client (CDR-023), which is not the client that
sends load,
4. calls `SetContract` on each scenario.

`AttachScenario` goes away. It takes an address, dials its own client, binds, and
panics on failure. This step now owns those three concerns, and turns that
panic into an error.

A scenario is then a struct holding its bound contracts and its config. It shapes
transactions and knows nothing about where they came from.

The binding client is not on the load path. `CreateTransactionOpts` sets
`auth.NoSend`, so a bound contract hands the transaction back rather than sending
it, and the sender's own per-endpoint clients do the sending. One client for
binding and code reads is therefore not a throughput concern.

## The boundary

```go
// registry imports, in full:
// context, encoding/json, embed, fmt, math/big, os, path/filepath
// github.com/ethereum/go-ethereum/common
// github.com/ethereum/go-ethereum/crypto
// github.com/sei-protocol/sei-load/generator/bindings
```

CDR-017 asserts this with `go list -deps`. `generator/bindings` is the one
sei-load import, and it is itself a leaf.

Note what is absent: no `config`, no `types`, no `generator`, no `sender`, no
`stats`, and no `ethclient`. `Resolve` takes a `CodeReader`, so the caller owns
the client.
123 changes: 123 additions & 0 deletions specs/contract-deployment-registry/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Data model: the chain file and the registry types

Phase 1 output. The file format is the one-way door in this feature: once a
committed file exists, changing its shape is a migration. It comes first, and it
needs sign-off before code depends on it.

## The chain file

One file per chain. Files the binary knows live in `registry/chains/` and compile
in. A deployment supplies an extra file by path.

```json
{
"chainId": 713715,
"chainName": "arctic-1",
"genesisHash": "3f1a…64 hex chars…9c",
"genesisS3URI": "s3://prod-sei-k8s-genesis-artifacts/arctic-1/genesis.json",
"contracts": [
{
"name": "storagerw",
"address": "0x1234567890123456789012345678901234567890",
"codeHash": "0xabcd…"
}
]
}
```

### Field by field

| Field | Type | Required | Why it is here |
| -- | -- | -- | -- |
| `chainId` | number | yes | The EVM chain id. Half the identity, and what a transaction signs against. |
| `chainName` | string | yes | For a human and for an error message. Never matched on. |
| `genesisHash` | string | yes | The other half of the identity. Bare hex, no algorithm prefix, matching `SeiNetwork.Status.GenesisHash`. |
| `genesisS3URI` | string | no | Provenance. Where the genesis came from. Never matched on. |
| `contracts` | array | yes | The contracts on this chain, each named. An empty array means the chain carries none yet. |
| `contracts[].name` | string | yes | The contract's name, which a scenario asks for. |
| `contracts[].address` | string | yes | The deployed address. |
| `contracts[].codeHash` | string | yes | Keccak-256 of the runtime code, observed at deployment. |

### Three decisions inside the format

**`chainName` and `genesisS3URI` are never matched on.** CDR-015 matches on
`chainId` and `genesisHash`. Both other fields exist for a human reading a
failure. Recording that here stops a later reader from treating either as a key.

**`codeHash` is Keccak-256, not SHA-256.** The EVM already defines an account's
code hash as Keccak-256 of its runtime code. A reader can therefore check this
value against chain state, not only against `eth_getCode`. `genesisHash` stays
SHA-256: the controller defines it, and this file does not redefine it. Two
hashes in one file use two algorithms, which is worth stating rather than
discovering.

**`contracts` is a list, not a map keyed by scenario.** CDR-005. A scenario is a
list of contracts, and most files hold a list of one today. The shape does not
change when TokenOps needs three.

## Go types

In package `registry`.

```go
// Contract is one named contract on one chain. A scenario is a list of these,
// and a list of one is ordinary.
type Contract struct {
Name string `json:"name"`
Address common.Address `json:"address"`
CodeHash common.Hash `json:"codeHash"`
}

// Chain is one chain and the contracts deployed on it.
type Chain struct {
ChainID int64 `json:"chainId"`
ChainName string `json:"chainName"`
GenesisHash string `json:"genesisHash"`
GenesisS3URI string `json:"genesisS3URI,omitempty"`
Contracts []Contract `json:"contracts"`
}

// Registry holds every chain the binary carries, plus any the deployment
// supplied.
type Registry struct {
chains map[chainKey]Chain
}

// chainKey is what CDR-015 matches on, and nothing else.
type chainKey struct {
chainID int64
genesisHash string
}
```

`common.Address` and `common.Hash` already marshal to and from hex strings, so
the JSON shape above needs no custom marshaller.

## What the types deliberately do not hold

**No ABI, and no bytecode.** Those live in `generator/bindings`, which the
registry imports. Duplicating them in a chain file would let the file and the
binding disagree, and CDR-007 exists to detect exactly that disagreement.

**No deployer key, and no endpoint.** A chain file describes a chain, not how to
reach it. The run already has endpoints from its profile.

**No timestamps, and no provenance beyond the S3 URI.** Git holds when an entry
landed and who committed it.

## Where deployment stays

The registry looks up, verifies, and records. It does not deploy.

`ContractScenarioBase.Deploy` already deploys, and moving it would mean moving
`ContractDeployer[T]` and its four implementors into the registry package. That
is a refactor this feature does not need: CDR-003 deploys when no entry exists,
and the scenario already knows how.

The flow: a preparation step asks the registry for each contract the profile needs.
It deploys where the registry has nothing. It binds every result with one client,
and hands each scenario its bound contracts. The scenario never sees an address
(CDR-021).

Un-defer when an operator needs to deploy without starting a run. The spec's
Deferred section already names that as the deploy subcommand.
Loading
Loading