Commit 9d4cee7e authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into preset-block-heights

parents 6455a797 dc8a87b4
......@@ -56,8 +56,9 @@ def main():
deployment_dir = pjoin(contracts_bedrock_dir, 'deployments', 'devnetL1')
op_node_dir = pjoin(args.monorepo_dir, 'op-node')
ops_bedrock_dir = pjoin(monorepo_dir, 'ops-bedrock')
deploy_config_dir = pjoin(contracts_bedrock_dir, 'deploy-config'),
devnet_config_path = pjoin(contracts_bedrock_dir, 'deploy-config', 'devnetL1.json')
deploy_config_dir = pjoin(contracts_bedrock_dir, 'deploy-config')
devnet_config_path = pjoin(deploy_config_dir, 'devnetL1.json')
devnet_config_template_path = pjoin(deploy_config_dir, 'devnetL1-template.json')
ops_chain_ops = pjoin(monorepo_dir, 'op-chain-ops')
sdk_dir = pjoin(monorepo_dir, 'packages', 'sdk')
......@@ -69,6 +70,7 @@ def main():
l1_deployments_path=pjoin(deployment_dir, '.deploy'),
deploy_config_dir=deploy_config_dir,
devnet_config_path=devnet_config_path,
devnet_config_template_path=devnet_config_template_path,
op_node_dir=op_node_dir,
ops_bedrock_dir=ops_bedrock_dir,
ops_chain_ops=ops_chain_ops,
......@@ -124,10 +126,16 @@ def deploy_contracts(paths):
'--rpc-url', 'http://127.0.0.1:8545'
], env={}, cwd=paths.contracts_bedrock_dir)
def init_devnet_l1_deploy_config(paths, update_timestamp=False):
deploy_config = read_json(paths.devnet_config_template_path)
if update_timestamp:
deploy_config['l1GenesisBlockTimestamp'] = '{:#x}'.format(int(time.time()))
write_json(paths.devnet_config_path, deploy_config)
def devnet_l1_genesis(paths):
log.info('Generating L1 genesis state')
init_devnet_l1_deploy_config(paths)
geth = subprocess.Popen([
'geth', '--dev', '--http', '--http.api', 'eth,debug',
'--verbosity', '4', '--gcmode', 'archive', '--dev.gaslimit', '30000000'
......@@ -157,13 +165,13 @@ def devnet_deploy(paths):
if os.path.exists(paths.allocs_path) == False:
devnet_l1_genesis(paths)
devnet_config_backup = pjoin(paths.devnet_dir, 'devnetL1.json.bak')
shutil.copy(paths.devnet_config_path, devnet_config_backup)
deploy_config = read_json(paths.devnet_config_path)
deploy_config['l1GenesisBlockTimestamp'] = '{:#x}'.format(int(time.time()))
write_json(paths.devnet_config_path, deploy_config)
# It's odd that we want to regenerate the devnetL1.json file with
# an updated timestamp different than the one used in the devnet_l1_genesis
# function. But, without it, CI flakes on this test rather consistently.
# If someone reads this comment and understands why this is being done, please
# update this comment to explain.
init_devnet_l1_deploy_config(paths, update_timestamp=True)
outfile_l1 = pjoin(paths.devnet_dir, 'genesis-l1.json')
run_command([
'go', 'run', 'cmd/main.go', 'genesis', 'l1',
'--deploy-config', paths.devnet_config_path,
......
......@@ -14,3 +14,4 @@ finalized and may change without notice.
* [Manual Usage](./manual.md)
* [Creating Traces with Cannon](./cannon.md)
* [Automation with `op-challenger`](./run-challenger.md)
* [Challenging Invalid Output Proposals](./invalid-proposals.md)
## Challenging Invalid Output Proposals
The dispute game factory deployed to Goerli reads from the permissioned L2 Output Oracle contract. This restricts games
to challenging valid output proposals and an honest challenger should win every game. To test creating games that
challenge an invalid output proposal, a custom chain is required. The simplest way to do this is using the end-to-end
test utilities in [`op-e2e`](https://github.com/ethereum-optimism/optimism/tree/develop/op-e2e).
A simple starting point has been provided in the `TestCannonProposedOutputRootInvalid` test case
in [`faultproof_test.go`](https://github.com/ethereum-optimism/optimism/blob/6e174ae2b2587d9ac5e2930d7574f85d254ca8b4/op-e2e/faultproof_test.go#L334).
This is a table test that takes the output root to propose, plus functions for move and step to counter the honest
claims. The test asserts that the defender always wins and thus the output root is found to be invalid.
......@@ -13,43 +13,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestMultipleAlphabetGames(t *testing.T) {
InitParallel(t)
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
gameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
// Start a challenger with the correct alphabet trace
gameFactory.StartChallenger(ctx, sys.NodeEndpoint("l1"), "TowerDefense",
challenger.WithAlphabet("abcdefg"),
challenger.WithPrivKey(sys.cfg.Secrets.Alice),
challenger.WithAgreeProposedOutput(true),
)
game1 := gameFactory.StartAlphabetGame(ctx, "abcxyz")
// Wait for the challenger to respond to the first game
game1.WaitForClaimCount(ctx, 2)
game2 := gameFactory.StartAlphabetGame(ctx, "zyxabc")
// Wait for the challenger to respond to the second game
game2.WaitForClaimCount(ctx, 2)
// Challenger should respond to new claims
game2.Attack(ctx, 1, common.Hash{0xaa})
game2.WaitForClaimCount(ctx, 4)
game1.Defend(ctx, 1, common.Hash{0xaa})
game1.WaitForClaimCount(ctx, 4)
gameDuration := game1.GameDuration(ctx)
sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game1.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
game2.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
}
func TestMultipleCannonGames(t *testing.T) {
InitParallel(t)
......@@ -106,36 +69,6 @@ func TestMultipleCannonGames(t *testing.T) {
challenger.WaitForGameDataDeletion(ctx, game1, game2)
}
func TestResolveDisputeGame(t *testing.T) {
InitParallel(t)
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
game := disputeGameFactory.StartAlphabetGame(ctx, "zyxwvut")
require.NotNil(t, game)
gameDuration := game.GameDuration(ctx)
game.WaitForGameStatus(ctx, disputegame.StatusInProgress)
game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "HonestAlice",
challenger.WithAgreeProposedOutput(true),
challenger.WithAlphabet("abcdefg"),
challenger.WithPrivKey(sys.cfg.Secrets.Alice),
)
game.WaitForClaimCount(ctx, 2)
sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
// Challenger should resolve the game now that the clocks have expired.
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
}
func TestChallengerCompleteDisputeGame(t *testing.T) {
InitParallel(t)
......@@ -333,22 +266,32 @@ func TestCannonDefendStep(t *testing.T) {
func TestCannonProposedOutputRootInvalid(t *testing.T) {
InitParallel(t)
honestStepsFail := func(ctx context.Context, correctTrace *disputegame.HonestHelper, parentClaimIdx int64) {
// honestStepsFail attempts to perform both an attack and defend step using the correct trace.
honestStepsFail := func(ctx context.Context, game *disputegame.CannonGameHelper, correctTrace *disputegame.HonestHelper, parentClaimIdx int64) {
// Attack step should fail
correctTrace.StepFails(ctx, parentClaimIdx, true)
// Defending should fail too
correctTrace.StepFails(ctx, parentClaimIdx, false)
}
tests := []struct {
// name is the name of the test
name string
// outputRoot is the invalid output root to propose
outputRoot common.Hash
performMove func(ctx context.Context, correctTrace *disputegame.HonestHelper, parentClaimIdx int64)
performStep func(ctx context.Context, correctTrace *disputegame.HonestHelper, parentClaimIdx int64)
// performMove is called to respond to each claim posted by the honest op-challenger.
// It should either attack or defend the claim at parentClaimIdx
performMove func(ctx context.Context, game *disputegame.CannonGameHelper, correctTrace *disputegame.HonestHelper, parentClaimIdx int64)
// performStep is called once the maximum game depth is reached. It should perform a step to counter the
// claim at parentClaimIdx. Since the proposed output root is invalid, the step call should always revert.
performStep func(ctx context.Context, game *disputegame.CannonGameHelper, correctTrace *disputegame.HonestHelper, parentClaimIdx int64)
}{
{
name: "AttackWithCorrectTrace",
outputRoot: common.Hash{0xab},
performMove: func(ctx context.Context, correctTrace *disputegame.HonestHelper, parentClaimIdx int64) {
performMove: func(ctx context.Context, game *disputegame.CannonGameHelper, correctTrace *disputegame.HonestHelper, parentClaimIdx int64) {
// Attack everything but oddly using the correct hash.
correctTrace.Attack(ctx, parentClaimIdx)
},
......@@ -357,7 +300,7 @@ func TestCannonProposedOutputRootInvalid(t *testing.T) {
{
name: "DefendWithCorrectTrace",
outputRoot: common.Hash{0xab},
performMove: func(ctx context.Context, correctTrace *disputegame.HonestHelper, parentClaimIdx int64) {
performMove: func(ctx context.Context, game *disputegame.CannonGameHelper, correctTrace *disputegame.HonestHelper, parentClaimIdx int64) {
// Can only attack the root claim
if parentClaimIdx == 0 {
correctTrace.Attack(ctx, parentClaimIdx)
......@@ -376,16 +319,16 @@ func TestCannonProposedOutputRootInvalid(t *testing.T) {
InitParallel(t)
ctx := context.Background()
sys, l1Client, game, correctTrace := setupDisputeGameForInvalidOutputRoot(t, common.Hash{0xab})
sys, l1Client, game, correctTrace := setupDisputeGameForInvalidOutputRoot(t, test.outputRoot)
t.Cleanup(sys.Close)
// Now maliciously play the game and it should be impossible to win
game.ChallengeRootClaim(ctx,
func(parentClaimIdx int64) {
test.performMove(ctx, correctTrace, parentClaimIdx)
test.performMove(ctx, game, correctTrace, parentClaimIdx)
},
func(parentClaimIdx int64) {
test.performStep(ctx, correctTrace, parentClaimIdx)
test.performStep(ctx, game, correctTrace, parentClaimIdx)
})
// Time travel past when the game will be resolvable.
......
......@@ -18,9 +18,10 @@ COPY --from=builder /app/entrypoint.sh /bin/entrypoint.sh
COPY --from=builder /app/bin/ufm /bin/ufm
RUN apk update && \
apk add ca-certificates && \
chmod +x /bin/entrypoint.sh
RUN apk add ca-certificates jq curl bind-tools
VOLUME /etc/ufm
EXPOSE 8080
......
......@@ -39,12 +39,6 @@ address = "0x0000000000000000000000000000000000000000"
private_key = "0000000000000000000000000000000000000000000000000000000000000000"
# Transaction value in wei
tx_value = 100000000000000
# Gas limit
gas_limit = 21000
# Gas tip cap
gas_tip_cap = 2000000000
# Fee cap
gas_fee_cap = 20000000000
[providers.p1]
# URL to the RPC provider
......@@ -52,13 +46,15 @@ url = "http://localhost:8551"
# Read only providers are only used to check for transactions
read_only = true
# Interval to poll the provider for expected transactions
read_interval = "1s"
read_interval = "10s"
# Interval to submit new transactions to the provider
send_interval = "5s"
# Wallet to be used for sending transactions
wallet = "default"
# Network to pool transactions, i.e. providers in the same network will check transactions from each other
network = "op-goerli"
send_interval = "30s"
# Interval between send transaction when we get "already known" txpool err
send_transaction_retry_interval = "100ms"
# Max time to retry
send_transaction_retry_timeout = "5s"
# Interval between each send transaction to the same network
send_transaction_cool_down = "30s"
# Interval between receipt retrieval
receipt_retrieval_interval = "500ms"
# Max time to check for receipt
......@@ -72,13 +68,15 @@ url = "http://localhost:8552"
# Read only providers are only used to check for transactions
read_only = false
# Interval to poll the provider for expected transactions
read_interval = "2s"
read_interval = "10s"
# Interval to submit new transactions to the provider
send_interval = "3s"
# Wallet to be used for sending transactions
wallet = "default"
# Network to pool transactions, i.e. providers in the same network will check transactions from each other
network = "op-goerli"
send_interval = "30s"
# Interval between send transaction when we get "already known" txpool err
send_transaction_retry_interval = "100ms"
# Max time to retry
send_transaction_retry_timeout = "5s"
# Interval between each send transaction to the same network
send_transaction_cool_down = "30s"
# Interval between receipt retrieval
receipt_retrieval_interval = "500ms"
# Max time to check for receipt
......
......@@ -49,9 +49,6 @@ type WalletConfig struct {
// transaction parameters
TxValue big.Int `toml:"tx_value"`
GasLimit uint64 `toml:"gas_limit"`
GasTipCap big.Int `toml:"gas_tip_cap"`
GasFeeCap big.Int `toml:"gas_fee_cap"`
}
type ProviderConfig struct {
......@@ -64,6 +61,7 @@ type ProviderConfig struct {
SendInterval TOMLDuration `toml:"send_interval"`
SendTransactionRetryInterval TOMLDuration `toml:"send_transaction_retry_interval"`
SendTransactionRetryTimeout TOMLDuration `toml:"send_transaction_retry_timeout"`
SendTransactionCoolDown TOMLDuration `toml:"send_transaction_cool_down"`
ReceiptRetrievalInterval TOMLDuration `toml:"receipt_retrieval_interval"`
ReceiptRetrievalTimeout TOMLDuration `toml:"receipt_retrieval_timeout"`
......@@ -130,12 +128,6 @@ func (c *Config) Validate() error {
if wallet.TxValue.BitLen() == 0 {
return errors.Errorf("wallet [%s] tx_value is missing", name)
}
if wallet.GasLimit == 0 {
return errors.Errorf("wallet [%s] gas_limit is missing", name)
}
if wallet.GasFeeCap.BitLen() == 0 {
return errors.Errorf("wallet [%s] gas_fee_cap is missing", name)
}
}
for name, provider := range c.Providers {
......@@ -154,6 +146,9 @@ func (c *Config) Validate() error {
if provider.SendTransactionRetryTimeout == 0 {
return errors.Errorf("provider [%s] send_transaction_retry_timeout is missing", name)
}
if provider.SendTransactionCoolDown == 0 {
return errors.Errorf("provider [%s] send_transaction_cool_down is missing", name)
}
if provider.ReceiptRetrievalInterval == 0 {
return errors.Errorf("provider [%s] receipt_retrieval_interval is missing", name)
}
......
......@@ -2,6 +2,7 @@ package clients
import (
"context"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics"
......@@ -22,7 +23,7 @@ func Dial(providerName string, url string) (*InstrumentedEthClient, error) {
start := time.Now()
c, err := ethclient.Dial(url)
if err != nil {
metrics.RecordError(providerName, "ethclient.Dial")
metrics.RecordErrorDetails(providerName, "ethclient.Dial", err)
return nil, err
}
metrics.RecordRPCLatency(providerName, "ethclient", "Dial", time.Since(start))
......@@ -34,7 +35,7 @@ func (i *InstrumentedEthClient) TransactionByHash(ctx context.Context, hash comm
tx, isPending, err := i.c.TransactionByHash(ctx, hash)
if err != nil {
if !i.ignorableErrors(err) {
metrics.RecordError(i.providerName, "ethclient.TransactionByHash")
metrics.RecordErrorDetails(i.providerName, "ethclient.TransactionByHash", err)
}
return nil, false, err
}
......@@ -46,7 +47,7 @@ func (i *InstrumentedEthClient) PendingNonceAt(ctx context.Context, address stri
start := time.Now()
nonce, err := i.c.PendingNonceAt(ctx, common.HexToAddress(address))
if err != nil {
metrics.RecordError(i.providerName, "ethclient.PendingNonceAt")
metrics.RecordErrorDetails(i.providerName, "ethclient.PendingNonceAt", err)
return 0, err
}
metrics.RecordRPCLatency(i.providerName, "ethclient", "PendingNonceAt", time.Since(start))
......@@ -58,7 +59,7 @@ func (i *InstrumentedEthClient) TransactionReceipt(ctx context.Context, txHash c
receipt, err := i.c.TransactionReceipt(ctx, txHash)
if err != nil {
if !i.ignorableErrors(err) {
metrics.RecordError(i.providerName, "ethclient.TransactionReceipt")
metrics.RecordErrorDetails(i.providerName, "ethclient.TransactionReceipt", err)
}
return nil, err
}
......@@ -71,7 +72,7 @@ func (i *InstrumentedEthClient) SendTransaction(ctx context.Context, tx *types.T
err := i.c.SendTransaction(ctx, tx)
if err != nil {
if !i.ignorableErrors(err) {
metrics.RecordError(i.providerName, "ethclient.SendTransaction")
metrics.RecordErrorDetails(i.providerName, "ethclient.SendTransaction", err)
}
return err
}
......@@ -79,6 +80,39 @@ func (i *InstrumentedEthClient) SendTransaction(ctx context.Context, tx *types.T
return err
}
func (i *InstrumentedEthClient) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
start := time.Now()
gas, err := i.c.EstimateGas(ctx, msg)
if err != nil {
metrics.RecordErrorDetails(i.providerName, "ethclient.EstimateGas", err)
return 0, err
}
metrics.RecordRPCLatency(i.providerName, "ethclient", "EstimateGas", time.Since(start))
return gas, err
}
func (i *InstrumentedEthClient) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
start := time.Now()
gasTipCap, err := i.c.SuggestGasTipCap(ctx)
if err != nil {
metrics.RecordErrorDetails(i.providerName, "ethclient.SuggestGasTipCap", err)
return nil, err
}
metrics.RecordRPCLatency(i.providerName, "ethclient", "SuggestGasTipCap", time.Since(start))
return gasTipCap, err
}
func (i *InstrumentedEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
start := time.Now()
header, err := i.c.HeaderByNumber(ctx, number)
if err != nil {
metrics.RecordErrorDetails(i.providerName, "ethclient.HeaderByNumber", err)
return nil, err
}
metrics.RecordRPCLatency(i.providerName, "ethclient", "HeaderByNumber", time.Since(start))
return header, err
}
func (i *InstrumentedEthClient) ignorableErrors(err error) bool {
msg := err.Error()
// we dont use errors.Is because eth client actually uses errors.New,
......
......@@ -22,7 +22,7 @@ func NewSignerClient(providerName string, logger log.Logger, endpoint string, tl
start := time.Now()
c, err := signer.NewSignerClient(logger, endpoint, tlsConfig)
if err != nil {
metrics.RecordError(providerName, "signer.NewSignerClient")
metrics.RecordErrorDetails(providerName, "signer.NewSignerClient", err)
return nil, err
}
metrics.RecordRPCLatency(providerName, "signer", "NewSignerClient", time.Since(start))
......@@ -33,7 +33,7 @@ func (i *InstrumentedSignerClient) SignTransaction(ctx context.Context, chainId
start := time.Now()
tx, err := i.c.SignTransaction(ctx, chainId, tx)
if err != nil {
metrics.RecordError(i.providerName, "signer.SignTransaction")
metrics.RecordErrorDetails(i.providerName, "signer.SignTransaction", err)
return nil, err
}
metrics.RecordRPCLatency(i.providerName, "signer", "SignTransaction", time.Since(start))
......
......@@ -2,6 +2,7 @@ package provider
import (
"context"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics"
......@@ -21,7 +22,7 @@ import (
// RoundTrip send a new transaction to measure round trip latency
func (p *Provider) RoundTrip(ctx context.Context) {
log.Debug("roundTripLatency",
log.Debug("RoundTrip",
"provider", p.name)
client, err := iclients.Dial(p.name, p.config.URL)
......@@ -33,33 +34,38 @@ func (p *Provider) RoundTrip(ctx context.Context) {
return
}
var nonce uint64
p.txPool.M.Lock()
if p.txPool.Nonce == uint64(0) {
nonce, err = client.PendingNonceAt(ctx, p.walletConfig.Address)
if err != nil {
log.Error("cant get nounce",
"provider", p.name,
"err", err)
p.txPool.M.Unlock()
return
}
p.txPool.Nonce = nonce
} else {
p.txPool.Nonce++
nonce = p.txPool.Nonce
}
p.txPool.M.Unlock()
p.txPool.ExclusiveSend.Lock()
defer p.txPool.ExclusiveSend.Unlock()
txHash := common.Hash{}
attempt := 0
nonce := uint64(0)
// used for timeout
firstAttemptAt := time.Now()
// used for actual round trip time (disregard retry time)
roundTripStartedAt := time.Now()
var roundTripStartedAt time.Time
for {
// sleep until we get a clear to send
for {
tx := p.createTx(nonce)
txHash = tx.Hash()
coolDown := time.Duration(p.config.SendTransactionCoolDown) - time.Since(p.txPool.LastSend)
if coolDown > 0 {
time.Sleep(coolDown)
} else {
break
}
}
tx, err := p.createTx(ctx, client, nonce)
nonce = tx.Nonce()
if err != nil {
log.Error("cant create tx",
"provider", p.name,
"nonce", nonce,
"err", err)
return
}
signedTx, err := p.sign(ctx, tx)
if err != nil {
......@@ -69,7 +75,6 @@ func (p *Provider) RoundTrip(ctx context.Context) {
"err", err)
return
}
txHash = signedTx.Hash()
roundTripStartedAt = time.Now()
......@@ -78,25 +83,29 @@ func (p *Provider) RoundTrip(ctx context.Context) {
if err.Error() == txpool.ErrAlreadyKnown.Error() ||
err.Error() == txpool.ErrReplaceUnderpriced.Error() ||
err.Error() == core.ErrNonceTooLow.Error() {
log.Warn("cant send transaction (retryable)",
"provider", p.name,
"err", err,
"nonce", nonce)
if time.Since(firstAttemptAt) >= time.Duration(p.config.SendTransactionRetryTimeout) {
log.Error("send transaction timed out (known already)",
"provider", p.name,
"hash", txHash.Hex(),
"nonce", nonce,
"elapsed", time.Since(firstAttemptAt),
"attempt", attempt,
"nonce", nonce)
metrics.RecordError(p.name, "ethclient.SendTransaction.nonce")
"attempt", attempt)
metrics.RecordErrorDetails(p.name, "send.timeout", err)
return
}
log.Warn("tx already known, incrementing nonce and trying again",
"provider", p.name,
"nonce", nonce)
time.Sleep(time.Duration(p.config.SendTransactionRetryInterval))
p.txPool.M.Lock()
p.txPool.Nonce++
nonce = p.txPool.Nonce
p.txPool.M.Unlock()
nonce++
attempt++
if attempt%10 == 0 {
log.Debug("retrying send transaction...",
......@@ -108,6 +117,7 @@ func (p *Provider) RoundTrip(ctx context.Context) {
} else {
log.Error("cant send transaction",
"provider", p.name,
"nonce", nonce,
"err", err)
metrics.RecordErrorDetails(p.name, "ethclient.SendTransaction", err)
return
......@@ -131,6 +141,7 @@ func (p *Provider) RoundTrip(ctx context.Context) {
SentAt: sentAt,
SeenBy: make(map[string]time.Time),
}
p.txPool.LastSend = sentAt
p.txPool.M.Unlock()
var receipt *types.Receipt
......@@ -140,13 +151,17 @@ func (p *Provider) RoundTrip(ctx context.Context) {
log.Error("receipt retrieval timed out",
"provider", p.name,
"hash", txHash,
"nonce", nonce,
"elapsed", time.Since(sentAt))
metrics.RecordErrorDetails(p.name, "receipt.timeout", err)
return
}
time.Sleep(time.Duration(p.config.ReceiptRetrievalInterval))
if attempt%10 == 0 {
log.Debug("checking for receipt...",
"provider", p.name,
"hash", txHash,
"nonce", nonce,
"attempt", attempt,
"elapsed", time.Since(sentAt))
}
......@@ -155,6 +170,7 @@ func (p *Provider) RoundTrip(ctx context.Context) {
log.Error("cant get receipt for transaction",
"provider", p.name,
"hash", txHash.Hex(),
"nonce", nonce,
"err", err)
return
}
......@@ -168,6 +184,7 @@ func (p *Provider) RoundTrip(ctx context.Context) {
log.Info("got transaction receipt",
"hash", txHash.Hex(),
"nonce", nonce,
"roundTripLatency", roundTripLatency,
"provider", p.name,
"blockNumber", receipt.BlockNumber,
......@@ -175,20 +192,83 @@ func (p *Provider) RoundTrip(ctx context.Context) {
"gasUsed", receipt.GasUsed)
}
func (p *Provider) createTx(nonce uint64) *types.Transaction {
toAddress := common.HexToAddress(p.walletConfig.Address)
func (p *Provider) createTx(ctx context.Context, client *iclients.InstrumentedEthClient, nonce uint64) (*types.Transaction, error) {
var err error
if nonce == 0 {
nonce, err = client.PendingNonceAt(ctx, p.walletConfig.Address)
if err != nil {
log.Error("cant get nounce",
"provider", p.name,
"nonce", nonce,
"err", err)
return nil, err
}
}
gasTipCap, err := client.SuggestGasTipCap(ctx)
if err != nil {
log.Error("cant get gas tip cap",
"provider", p.name,
"err", err)
return nil, err
}
gasTipCap = new(big.Int).Mul(gasTipCap, big.NewInt(110))
gasTipCap = new(big.Int).Div(gasTipCap, big.NewInt(100))
head, err := client.HeaderByNumber(ctx, nil)
if err != nil {
log.Error("cant get base fee from head",
"provider", p.name,
"err", err)
return nil, err
}
baseFee := head.BaseFee
gasFeeCap := new(big.Int).Add(
gasTipCap,
new(big.Int).Mul(baseFee, big.NewInt(2)))
addr := common.HexToAddress(p.walletConfig.Address)
var data []byte
tx := types.NewTx(&types.DynamicFeeTx{
dynamicTx := &types.DynamicFeeTx{
ChainID: &p.walletConfig.ChainID,
Nonce: nonce,
GasFeeCap: &p.walletConfig.GasFeeCap,
GasTipCap: &p.walletConfig.GasTipCap,
Gas: p.walletConfig.GasLimit,
To: &toAddress,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
To: &addr,
Value: &p.walletConfig.TxValue,
Data: data,
}
gas, err := client.EstimateGas(ctx, ethereum.CallMsg{
From: addr,
To: &addr,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Data: dynamicTx.Data,
Value: dynamicTx.Value,
})
return tx
if err != nil {
log.Error("cant estimate gas",
"provider", p.name,
"err", err)
return nil, err
}
dynamicTx.Gas = gas
tx := types.NewTx(dynamicTx)
log.Info("tx created",
"provider", p.name,
"from", addr,
"to", dynamicTx.To,
"nonce", dynamicTx.Nonce,
"value", dynamicTx.Value,
"gas", dynamicTx.Gas,
"gasTipCap", dynamicTx.GasTipCap,
"gasFeeCap", dynamicTx.GasFeeCap,
)
return tx, nil
}
func (p *Provider) sign(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
......
......@@ -15,7 +15,11 @@ type NetworkTransactionPool struct {
M sync.Mutex
Transactions map[string]*TransactionState
Expected int
Nonce uint64
// Last time a transaction was sent
LastSend time.Time
// Prevents concurrent transaction send
ExclusiveSend sync.Mutex
}
type TransactionState struct {
......
......@@ -26,3 +26,6 @@ deployments/hardhat
deployments/getting-started
deployments/*/.deploy
deployments/1337
# Devnet config which changes with each 'make devnet-up'
deploy-config/devnetL1.json
......@@ -138,6 +138,9 @@ Test contracts should be named one of the following according to their use:
- `TargetContract_Function_Test` for contracts containing happy path tests for a given function.
- `TargetContract_Function_TestFail` for contracts containing sad path tests for a given function.
To minimize clutter, getter functions can be grouped together into a single test contract,
ie. `TargetContract_Getters_Test`.
## Withdrawaing From Fee Vaults
See the file `scripts/FeeVaultWithdrawal.s.sol` to withdraw from the L2 fee vaults. It includes
......
......@@ -23,7 +23,7 @@ importers:
version: 0.4.8
'@nrwl/nx-cloud':
specifier: latest
version: 16.3.0
version: 16.4.0
'@types/chai':
specifier: ^4.2.18
version: 4.2.21
......@@ -103,8 +103,8 @@ importers:
specifier: 0.4.0
version: 0.4.0
mkdirp:
specifier: ^1.0.4
version: 1.0.4
specifier: ^3.0.1
version: 3.0.1
mocha:
specifier: ^10.2.0
version: 10.2.0
......@@ -184,7 +184,7 @@ importers:
version: 2.9.6(chai@4.3.7)
ts-node:
specifier: ^10.9.1
version: 10.9.1(@types/node@20.5.3)(typescript@5.1.6)
version: 10.9.1(@types/node@20.5.3)(typescript@5.2.2)
tsx:
specifier: ^3.12.7
version: 3.12.7
......@@ -575,6 +575,12 @@ importers:
specifier: ^3.22.0
version: 3.22.0
ufm-test-services/metamask:
devDependencies:
typescript:
specifier: ^5.1.6
version: 5.1.6
packages:
/@aashutoshrathi/word-wrap@1.2.6:
......@@ -1690,6 +1696,32 @@ packages:
- typescript
dev: true
/@ethereum-waffle/compiler@4.0.3(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(solc@0.8.15)(typechain@8.3.1)(typescript@5.2.2):
resolution: {integrity: sha512-5x5U52tSvEVJS6dpCeXXKvRKyf8GICDwiTwUvGD3/WD+DpvgvaoHOL82XqpTSUHgV3bBq6ma5/8gKUJUIAnJCw==}
engines: {node: '>=10.0'}
peerDependencies:
ethers: '*'
solc: '*'
typechain: ^8.0.0
dependencies:
'@resolver-engine/imports': 0.3.3
'@resolver-engine/imports-fs': 0.3.3
'@typechain/ethers-v5': 10.2.1(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.1)(typescript@5.2.2)
'@types/mkdirp': 0.5.2
'@types/node-fetch': 2.6.4
ethers: 5.7.2
mkdirp: 0.5.6
node-fetch: 2.6.12
solc: 0.8.15
typechain: 8.3.1(typescript@5.2.2)
transitivePeerDependencies:
- '@ethersproject/abi'
- '@ethersproject/providers'
- encoding
- supports-color
- typescript
dev: true
/@ethereum-waffle/ens@4.0.3(@ensdomains/ens@0.4.5)(@ensdomains/resolver@0.2.4)(ethers@5.7.2):
resolution: {integrity: sha512-PVLcdnTbaTfCrfSOrvtlA9Fih73EeDvFS28JQnT5M5P4JMplqmchhcZB1yg/fCtx4cvgHlZXa0+rOCAk2Jk0Jw==}
engines: {node: '>=10.0'}
......@@ -2534,6 +2566,10 @@ packages:
resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==}
engines: {node: '>= 16'}
/@noble/hashes@1.3.2:
resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==}
engines: {node: '>= 16'}
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
......@@ -2599,7 +2635,7 @@ packages:
dependencies:
'@nomiclabs/hardhat-ethers': 2.0.6(ethers@5.7.2)(hardhat@2.9.6)
'@types/sinon-chai': 3.2.5
ethereum-waffle: 4.0.10(@ensdomains/ens@0.4.5)(@ensdomains/resolver@0.2.4)(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typescript@5.1.6)
ethereum-waffle: 4.0.10(@ensdomains/ens@0.4.5)(@ensdomains/resolver@0.2.4)(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typescript@5.2.2)
ethers: 5.7.2
hardhat: 2.9.6(chai@4.3.7)
dev: true
......@@ -2669,10 +2705,10 @@ packages:
- nx
dev: true
/@nrwl/nx-cloud@16.3.0:
resolution: {integrity: sha512-nJrGsVufhY74KcP7kM7BqFOGAoO5OEF6+wfiM295DgmEG9c1yW+x5QiQaC42K9SWYn/eKQa1X7466ZA5lynXoQ==}
/@nrwl/nx-cloud@16.4.0:
resolution: {integrity: sha512-QitrYK6z9ceagetBlgLMZnC0T85k2JTk+oK0MxZ5p/woclqeYN7SiGNZgMzDq8TjJwt8Fm/MDnsSo3xtufmLBg==}
dependencies:
nx-cloud: 16.3.0
nx-cloud: 16.4.0
transitivePeerDependencies:
- debug
dev: true
......@@ -3071,7 +3107,7 @@ packages:
/@scure/bip39@1.2.1:
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
dependencies:
'@noble/hashes': 1.3.1
'@noble/hashes': 1.3.2
'@scure/base': 1.1.1
/@sentry-internal/tracing@7.64.0:
......@@ -3702,6 +3738,24 @@ packages:
typescript: 5.1.6
dev: true
/@typechain/ethers-v5@10.2.1(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typechain@8.3.1)(typescript@5.2.2):
resolution: {integrity: sha512-n3tQmCZjRE6IU4h6lqUGiQ1j866n5MTCBJreNEHHVWXa2u9GJTaeYyU1/k+1qLutkyw+sS6VAN+AbeiTqsxd/A==}
peerDependencies:
'@ethersproject/abi': ^5.0.0
'@ethersproject/providers': ^5.0.0
ethers: ^5.1.3
typechain: ^8.1.1
typescript: '>=4.3.0'
dependencies:
'@ethersproject/abi': 5.7.0
'@ethersproject/providers': 5.7.2
ethers: 5.7.2
lodash: 4.17.21
ts-essentials: 7.0.3(typescript@5.2.2)
typechain: 8.3.1(typescript@5.2.2)
typescript: 5.2.2
dev: true
/@types/abstract-leveldown@5.0.2:
resolution: {integrity: sha512-+jA1XXF3jsz+Z7FcuiNqgK53hTa/luglT2TyTpKPqoYbxVY+mCPF22Rm+q3KPBrMHJwNXFrTViHszBOfU4vftQ==}
dev: true
......@@ -3713,20 +3767,20 @@ packages:
/@types/bn.js@4.11.6:
resolution: {integrity: sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/bn.js@5.1.0:
resolution: {integrity: sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/body-parser@1.19.1:
resolution: {integrity: sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==}
dependencies:
'@types/connect': 3.4.35
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/chai-as-promised@7.1.5:
......@@ -3752,7 +3806,7 @@ packages:
/@types/connect@3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
/@types/dateformat@5.0.0:
resolution: {integrity: sha512-SZg4JdHIWHQGEokbYGZSDvo5wA4TLYPXaqhigs/wH+REDOejcJzgH+qyY+HtEUtWOZxEUkbhbdYPqQDiEgrXeA==}
......@@ -3766,7 +3820,7 @@ packages:
/@types/express-serve-static-core@4.17.35:
resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
'@types/qs': 6.9.7
'@types/range-parser': 1.2.4
'@types/send': 0.17.1
......@@ -3812,7 +3866,7 @@ packages:
dependencies:
'@types/abstract-leveldown': 5.0.2
'@types/level-errors': 3.0.0
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/lru-cache@5.1.1:
......@@ -3843,7 +3897,7 @@ packages:
/@types/mkdirp@0.5.2:
resolution: {integrity: sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/mocha@10.0.1:
......@@ -3862,7 +3916,7 @@ packages:
/@types/node-fetch@2.6.4:
resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
form-data: 3.0.1
dev: true
......@@ -3875,10 +3929,10 @@ packages:
/@types/node@20.5.0:
resolution: {integrity: sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==}
dev: true
/@types/node@20.5.3:
resolution: {integrity: sha512-ITI7rbWczR8a/S6qjAW7DMqxqFMjjTo61qZVWJ1ubPvbIQsL5D/TvwjYEalM8Kthpe3hTzOGrF2TGbAu2uyqeA==}
dev: true
/@types/normalize-package-data@2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
......@@ -3890,7 +3944,7 @@ packages:
/@types/pbkdf2@3.1.0:
resolution: {integrity: sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/pino-multi-stream@5.1.3:
......@@ -3908,13 +3962,13 @@ packages:
/@types/pino-std-serializers@2.4.1:
resolution: {integrity: sha512-17XcksO47M24IVTVKPeAByWUd3Oez7EbIjXpSbzMPhXVzgjGtrOa49gKBwxH9hb8dKv58OelsWQ+A1G1l9S3wQ==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/pino@6.3.11:
resolution: {integrity: sha512-S7+fLONqSpHeW9d7TApUqO6VN47KYgOXhCNKwGBVLHObq8HhaAYlVqUNdfnvoXjCMiwE5xcPm/5R2ZUh8bgaXQ==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
'@types/pino-pretty': 4.7.1
'@types/pino-std-serializers': 2.4.1
sonic-boom: 2.8.0
......@@ -3964,7 +4018,7 @@ packages:
/@types/secp256k1@4.0.3:
resolution: {integrity: sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/seedrandom@3.0.1:
......@@ -3983,14 +4037,14 @@ packages:
resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
dependencies:
'@types/mime': 1.3.2
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/serve-static@1.13.10:
resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==}
dependencies:
'@types/mime': 1.3.2
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@types/sinon-chai@3.2.5:
......@@ -4027,18 +4081,18 @@ packages:
/@types/ws@7.4.7:
resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
/@types/ws@8.5.3:
resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: false
/@types/ws@8.5.5:
resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==}
dependencies:
'@types/node': 20.5.0
'@types/node': 20.5.3
dev: true
/@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.60.1)(eslint@8.47.0)(typescript@5.1.6):
......@@ -8273,6 +8327,31 @@ packages:
- typescript
dev: true
/ethereum-waffle@4.0.10(@ensdomains/ens@0.4.5)(@ensdomains/resolver@0.2.4)(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(typescript@5.2.2):
resolution: {integrity: sha512-iw9z1otq7qNkGDNcMoeNeLIATF9yKl1M8AIeu42ElfNBplq0e+5PeasQmm8ybY/elkZ1XyRO0JBQxQdVRb8bqQ==}
engines: {node: '>=10.0'}
hasBin: true
peerDependencies:
ethers: '*'
dependencies:
'@ethereum-waffle/chai': 4.0.10(@ensdomains/ens@0.4.5)(@ensdomains/resolver@0.2.4)(ethers@5.7.2)
'@ethereum-waffle/compiler': 4.0.3(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2)(ethers@5.7.2)(solc@0.8.15)(typechain@8.3.1)(typescript@5.2.2)
'@ethereum-waffle/mock-contract': 4.0.4(ethers@5.7.2)
'@ethereum-waffle/provider': 4.0.5(@ensdomains/ens@0.4.5)(@ensdomains/resolver@0.2.4)(ethers@5.7.2)
ethers: 5.7.2
solc: 0.8.15
typechain: 8.3.1(typescript@5.2.2)
transitivePeerDependencies:
- '@ensdomains/ens'
- '@ensdomains/resolver'
- '@ethersproject/abi'
- '@ethersproject/providers'
- debug
- encoding
- supports-color
- typescript
dev: true
/ethereumjs-abi@0.6.8:
resolution: {integrity: sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA==}
dependencies:
......@@ -10701,7 +10780,7 @@ packages:
strong-log-transformer: 2.1.0
tar: 6.1.11
temp-dir: 1.0.0
typescript: 5.1.6
typescript: 5.2.2
upath: 2.0.1
uuid: 9.0.0
validate-npm-package-license: 3.0.4
......@@ -11808,6 +11887,12 @@ packages:
hasBin: true
dev: true
/mkdirp@3.0.1:
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
hasBin: true
dev: true
/mlly@1.4.0:
resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==}
dependencies:
......@@ -12293,11 +12378,11 @@ packages:
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
dev: true
/nx-cloud@16.3.0:
resolution: {integrity: sha512-hmNgpeLO4v4WDSWa8YhwX+q+9ohIyY8iqxlWyIKixWzQH2XfRgYFjOLH4IDLGOlKa3hg7MB6+4+75cK9CfSmKw==}
/nx-cloud@16.4.0:
resolution: {integrity: sha512-jbq4hWvDwRlJVpxgMgbmNSkue+6XZSn53R6Vo6qmCAWODJ9KY1BZdZ/9VRL8IX/BRKebVFiXp3SapFB1qPhH8A==}
hasBin: true
dependencies:
'@nrwl/nx-cloud': 16.3.0
'@nrwl/nx-cloud': 16.4.0
axios: 1.1.3
chalk: 4.1.2
dotenv: 10.0.0
......@@ -15083,6 +15168,14 @@ packages:
typescript: 5.1.6
dev: true
/ts-essentials@7.0.3(typescript@5.2.2):
resolution: {integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==}
peerDependencies:
typescript: '>=3.7.0'
dependencies:
typescript: 5.2.2
dev: true
/ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
dev: true
......@@ -15131,7 +15224,7 @@ packages:
yn: 3.1.1
dev: true
/ts-node@10.9.1(@types/node@20.5.3)(typescript@5.1.6):
/ts-node@10.9.1(@types/node@20.5.3)(typescript@5.2.2):
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
......@@ -15157,7 +15250,7 @@ packages:
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.1.6
typescript: 5.2.2
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
......@@ -15288,7 +15381,7 @@ packages:
joycon: 3.1.1
postcss-load-config: 4.0.1
resolve-from: 5.0.0
rollup: 3.26.3
rollup: 3.28.0
source-map: 0.8.0-beta.0
sucrase: 3.34.0
tree-kill: 1.2.2
......@@ -15443,6 +15536,27 @@ packages:
- supports-color
dev: true
/typechain@8.3.1(typescript@5.2.2):
resolution: {integrity: sha512-fA7clol2IP/56yq6vkMTR+4URF1nGjV82Wx6Rf09EsqD4tkzMAvEaqYxVFCavJm/1xaRga/oD55K+4FtuXwQOQ==}
hasBin: true
peerDependencies:
typescript: '>=4.3.0'
dependencies:
'@types/prettier': 2.3.2
debug: 4.3.4(supports-color@8.1.1)
fs-extra: 7.0.1
glob: 7.1.7
js-sha3: 0.8.0
lodash: 4.17.21
mkdirp: 1.0.4
prettier: 2.8.8
ts-command-line-args: 2.5.1
ts-essentials: 7.0.3(typescript@5.2.2)
typescript: 5.2.2
transitivePeerDependencies:
- supports-color
dev: true
/typed-array-buffer@1.0.0:
resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
engines: {node: '>= 0.4'}
......@@ -15511,6 +15625,12 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
/typescript@5.2.2:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
/typical@4.0.0:
resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==}
engines: {node: '>=8'}
......
......@@ -2,3 +2,4 @@ packages:
- 'packages/*'
- 'endpoint-monitor'
- 'op-exporter'
- 'ufm-test-services/metamask'
......@@ -30,6 +30,13 @@ single dispute game will not result in the bug becoming consensus.
For added context, we define a few types that are used in the following snippets.
```solidity
/// @notice A custom type for a generic hash.
type Hash is bytes32;
/// @notice A dedicated timestamp type.
type Timestamp is uint64;
/// @notice The type of proof system being used.
enum GameType {
/// @dev The game will use a `IDisputeGame` implementation that utilizes fault proofs.
......@@ -90,38 +97,74 @@ interface IDisputeGameFactory {
/// @param rootClaim The root claim of the dispute game
event DisputeGameCreated(address indexed disputeProxy, GameType indexed gameType, Claim indexed rootClaim);
/// @notice `games` queries an internal a mapping that maps the hash of `gameType ++ rootClaim ++ extraData`
/// to the deployed `DisputeGame` clone.
/// @notice Emitted when a new game implementation added to the factory
/// @param impl The implementation contract for the given `GameType`.
/// @param gameType The type of the DisputeGame.
event ImplementationSet(address indexed impl, GameType indexed gameType);
/// @notice The total number of dispute games created by this factory.
/// @return gameCount_ The total number of dispute games created by this factory.
function gameCount() external view returns (uint256 gameCount_);
/// @notice `games` queries an internal mapping that maps the hash of
/// `gameType ++ rootClaim ++ extraData` to the deployed `DisputeGame` clone.
/// @dev `++` equates to concatenation.
/// @param gameType The type of the DisputeGame - used to decide the proxy implementation
/// @param rootClaim The root claim of the DisputeGame.
/// @param extraData Any extra data that should be provided to the created dispute game.
/// @return _proxy The clone of the `DisputeGame` created with the given parameters. Returns `address(0)` if nonexistent.
function games(GameType gameType, Claim rootClaim, bytes calldata extraData) external view returns (IDisputeGame _proxy);
/// @notice `gameImpls` is a mapping that maps `GameType`s to their respective `IDisputeGame` implementations.
/// @param gameType The type of the dispute game.
/// @return _impl The address of the implementation of the game type. Will be cloned on creation of a new dispute game
/// with the given `gameType`.
function gameImpls(GameType gameType) public view returns (IDisputeGame _impl);
/// @notice The owner of the contract.
/// @dev Owner Permissions:
/// - Update the implementation contracts for a given game type.
/// @return _owner The owner of the contract.
function owner() public view returns (address _owner);
/// @param _gameType The type of the DisputeGame - used to decide the proxy implementation
/// @param _rootClaim The root claim of the DisputeGame.
/// @param _extraData Any extra data that should be provided to the created dispute game.
/// @return proxy_ The clone of the `DisputeGame` created with the given parameters.
/// Returns `address(0)` if nonexistent.
/// @return timestamp_ The timestamp of the creation of the dispute game.
function games(GameType _gameType, Claim _rootClaim, bytes calldata _extraData)
external
view
returns (IDisputeGame proxy_, Timestamp timestamp_);
/// @notice `gameAtIndex` returns the dispute game contract address and its creation timestamp
/// at the given index. Each created dispute game increments the underlying index.
/// @param _index The index of the dispute game.
/// @return gameType_ The type of the DisputeGame - used to decide the proxy implementation.
/// @return timestamp_ The timestamp of the creation of the dispute game.
/// @return proxy_ The clone of the `DisputeGame` created with the given parameters.
/// Returns `address(0)` if nonexistent.
function gameAtIndex(uint256 _index)
external
view
returns (GameType gameType_, Timestamp timestamp_, IDisputeGame proxy_);
/// @notice `gameImpls` is a mapping that maps `GameType`s to their respective
/// `IDisputeGame` implementations.
/// @param _gameType The type of the dispute game.
/// @return impl_ The address of the implementation of the game type.
/// Will be cloned on creation of a new dispute game with the given `gameType`.
function gameImpls(GameType _gameType) external view returns (IDisputeGame impl_);
/// @notice Creates a new DisputeGame proxy contract.
/// @param gameType The type of the DisputeGame - used to decide the proxy implementation
/// @param rootClaim The root claim of the DisputeGame.
/// @param extraData Any extra data that should be provided to the created dispute game.
function create(GameType gameType, Claim rootClaim, bytes calldata extraData) external returns (IDisputeGame proxy);
/// @notice Sets the implementation contract for a specific `GameType`
/// @param _gameType The type of the DisputeGame - used to decide the proxy implementation.
/// @param _rootClaim The root claim of the DisputeGame.
/// @param _extraData Any extra data that should be provided to the created dispute game.
/// @return proxy_ The address of the created DisputeGame proxy.
function create(GameType _gameType, Claim _rootClaim, bytes calldata _extraData)
external
returns (IDisputeGame proxy_);
/// @notice Sets the implementation contract for a specific `GameType`.
/// @dev May only be called by the `owner`.
/// @param gameType The type of the DisputeGame
/// @param impl The implementation contract for the given `GameType`
function setImplementation(GameType gameType, IDisputeGame impl) external;
/// @param _gameType The type of the DisputeGame.
/// @param _impl The implementation contract for the given `GameType`.
function setImplementation(GameType _gameType, IDisputeGame _impl) external;
/// @notice Returns a unique identifier for the given dispute game parameters.
/// @dev Hashes the concatenation of `gameType . rootClaim . extraData`
/// without expanding memory.
/// @param _gameType The type of the DisputeGame.
/// @param _rootClaim The root claim of the DisputeGame.
/// @param _extraData Any extra data that should be provided to the created dispute game.
/// @return uuid_ The unique identifier for the given dispute game parameters.
function getGameUUID(GameType _gameType, Claim _rootClaim, bytes memory _extraData)
external
pure
returns (Hash uuid_);
}
```
......@@ -146,82 +189,51 @@ interface IDisputeGame {
/// @custom:invariant The `initialize` function may only be called once.
function initialize() external;
/// @notice Returns the semantic version of the DisputeGame contract
function version() external pure returns (string memory _version);
/// @notice Emitted when the game is resolved.
/// @param status The status of the game after resolution.
event Resolved(GameStatus indexed status);
/// @notice Returns the timestamp that the DisputeGame contract was created at.
function createdAt() external pure returns (Timestamp _createdAt);
function createdAt() external pure returns (Timestamp createdAt_);
/// @notice Returns the current status of the game.
function status() external view returns (GameStatus _status);
function status() external view returns (GameStatus status_);
/// @notice Getter for the game type.
/// @dev `clones-with-immutable-args` argument #1
/// @dev The reference impl should be entirely different depending on the type (fault, validity)
/// i.e. The game type should indicate the security model.
/// @return _gameType The type of proof system being used.
function gameType() external view returns (GameType _gameType);
/// @return gameType_ The type of proof system being used.
function gameType() external view returns (GameType gameType_);
/// @notice Getter for the root claim.
/// @return _rootClaim The root claim of the DisputeGame.
/// @return rootClaim_ The root claim of the DisputeGame.
/// @dev `clones-with-immutable-args` argument #2
function rootClaim() external view returns (Claim _rootClaim);
function rootClaim() external view returns (Claim rootClaim_);
/// @notice Getter for the extra data.
/// @dev `clones-with-immutable-args` argument #3
/// @return _extraData Any extra data supplied to the dispute game contract by the creator.
function extraData() external view returns (bytes memory _extraData);
/// @return extraData_ Any extra data supplied to the dispute game contract by the creator.
function extraData() external view returns (bytes memory extraData_);
/// @notice Returns the address of the `BondManager` used
function bondManager() public view returns (IBondManager _bondManager);
function bondManager() public view returns (IBondManager bondManager_);
/// @notice If all necessary information has been gathered, this function should mark the game
/// status as either `CHALLENGER_WINS` or `DEFENDER_WINS` and return the status of
/// the resolved game. It is at this stage that the bonds should be awarded to the
/// necessary parties.
/// @dev May only be called if the `status` is `IN_PROGRESS`.
function resolve() public returns (GameStatus _status);
}
////////////////////////////////////////////////////////////////
// OUTPUT ATTESTATION DISPUTE GAME //
////////////////////////////////////////////////////////////////
/// @title IDisputeGame_OutputAttestation
/// @notice The interface for an attestation-based DisputeGame meant to contest output
/// proposals in Optimism's `L2OutputOracle` contract.
interface IDisputeGame_OutputAttestation is IDisputeGame {
/// @notice A mapping of addresses from the `signerSet` to booleans signifying whether
/// or not they have authorized the `rootClaim` to be invalidated.
function challenges(address challenger) external view returns (bool _challenged);
/// @notice The signer set consists of authorized public keys that may challenge the `rootClaim`.
/// This set of signers is snapshotted from the `SystemConfig` upon creation of the game.
/// @return An array of authorized signers from the `SystemConfig` contract.
function frozenSignerSet() external view returns (address[] memory _signers);
/// @notice The signer set consists of authorized public keys that may challenge the `rootClaim`.
/// @return _isAuthorized Whether or not the `addr` is part of the frozen signer set.
function signerSet(address addr) external view override returns (bool _isAuthorized);
/// @notice The amount of signatures required to successfully challenge the `rootClaim`
/// output proposal. Once this threshold is met by members of the `signerSet`
/// calling `challenge`, the game will be resolved to `CHALLENGER_WINS`.
/// @custom:invariant The `signatureThreshold` may never be greater than the length of the `signerSet`.
function frozenSignatureThreshold() external view returns (uint256 _signatureThreshold);
/// @notice Returns the L2 Block Number that the `rootClaim` commits to. Exists within the `extraData`.
function l2BlockNumber() public view returns (uint256 _l2BlockNumber);
/// @notice Challenge the `rootClaim`.
/// @dev - If the `ecrecover`ed address that created the signature is not a part of the
/// signer set returned by `signerSet`, this function should revert.
/// - If the signature provided is the signature that breaches the signature threshold,
/// the function should call the `resolve` function to resolve the game as `CHALLENGER_WINS`.
/// - When the game resolves, the bond attached to the root claim should be distributed among
/// the signers who participated in challenging the invalid claim.
/// @param signature An EIP-712 signature committing to the `rootClaim` and `l2BlockNumber` (within the `extraData`)
/// from a key that exists within the `signerSet`.
function challenge(bytes calldata signature) external;
/// @return status_ The status of the game after resolution.
function resolve() public returns (GameStatus status_);
/// @notice A compliant implementation of this interface should return the components of the
/// game UUID's preimage provided in the cwia payload. The preimage of the UUID is
/// constructed as `keccak256(gameType . rootClaim . extraData)` where `.` denotes
/// concatenation.
/// @return gameType_ The type of proof system being used.
/// @return rootClaim_ The root claim of the DisputeGame.
/// @return extraData_ Any extra data supplied to the dispute game contract by the creator.
function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_);
}
```
# Fault Dispute Game
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Overview](#overview)
- [Definitions](#definitions)
- [Virtual Machine (VM)](#virtual-machine-vm)
- [Execution Trace](#execution-trace)
- [Claims](#claims)
- [DAG](#dag)
- [Game Tree](#game-tree)
- [Position](#position)
- [GAME_DURATION](#game_duration)
- [Game Mechanics](#game-mechanics)
- [Actors](#actors)
- [Moves](#moves)
- [Attack](#attack)
- [Defend](#defend)
- [Step](#step)
- [Step Types](#step-types)
- [Team Dynamics](#team-dynamics)
- [Game Clock](#game-clock)
- [Resolution](#resolution)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Overview
The Fault Dispute Game (FDG) is a specific type of
[dispute game](./dispute-game-interface.md) that verifies the validity of a
root claim by iteratively bisecting an execution trace down to a single instruction step.
It relies on a Virtual Machine (VM) to falsify invalid claims made
at a single instruction step.
Actors, i.e. Players, interact with the game by making claims that dispute other claims in the FDG.
Each claim made narrows the execution trace until the source of dispute is a single state transition.
Once a time limit is reached, the dispute game is _resolved_, based on
claims made that are disputed and which aren't, to determine the winners of the game.
## Definitions
### Virtual Machine (VM)
This is a state transition function (STF) that takes a _pre-state_ and computes the post-state.
The VM may reference external data during the STF and as such, it also accepts a _proof_ of this data.
Typically, the pre-state contains a commitment to the _proof_ to verify the integrity of the data referenced.
Mathemtically, we define the STF as $VM(S_i,P_i)$ where
- $S_i$ is the pre-state
- $P_i$ is an optional proof needed for the transition from $S_i$ to $S_{i+1}$.
### Execution Trace
An execution trace $T$ is a sequence $(S_0,S_1,S_2,...,S_n)$ where each $S_i$ is a VM state and
for each $i$, $0 \le i \lt n$, $S_{n+1} = VM(S_i, P_i)$.
Every execution trace has a unique starting state, $S_0$, that's preset to a FDG implementation.
We refer to this state as the **ABSOLUTE\_PRESTATE**.
### Claims
Claims assert an execution trace. This is represented as `ClaimHash`, a `bytes32` commitment to
the last VM state in the trace. A FDG is initialized with a root claim, which commits to the entire
execution trace. As we'll see later, there can be multiple claims, committing to different states in the FDG.
### DAG
A Directed Acyclic Graph $G = (V,E)$ representing the relationship between claims, where:
- $V$ is the set of nodes, each representing a claim. Formally, $V = \{C_1,C_2,...,C_n\}$,
where $C_i$ is a claim.
- $E$ is the set of _directed_ edges. An edge $(C_i,C_j)$ exists if $C_j$ is a direct dispute
against $C_i$ through either an "Attack" or "Defend" [move](#moves).
### Game Tree
The Game Tree is a binary tree of positions. Every claim in the DAG references a position in the Game Tree.
The Game Tree has a maximum depth, `MAX_GAME_DEPTH`, that's preset to an FDG implementation.
Thus, the Game Tree contains $2^{d-1}$ positions, where $d$ is the `MAX_GAME_DEPTH`
(unless $d=0$, in which case there's only 1 position).
### Position
A position represents the location of a claim in the Game Tree. This is represented by a
"generalized index" (or **gindex**) where the high-order bit is the level in the tree and the remaining
bits is a unique bit pattern, allowing a unique identifier for each node in the tree.
The **gindex** of a position $n$ can be calculated as $2^{d(n)} + idx(n)$, where:
- $d(n)$ is a function returning the depth of the position in the Game Tree
- $idx(n)$ is a function returning the index of the position at its depth (starting from the left).
Positions at the deepest level of the game tree correspond to indices in the execution trace.
Positions higher up the game tree also cover the deepest, right-most positions relative to the current position.
We refer to this coverage as the **trace index** of a Position.
> This means claims commit to an execution trace that terminates at the same index as their Position's trace index.
Note that there can be multiple positions covering the same _trace index_.
### GAME_DURATION
This is an immutable, preset to a FDG implementation, representing the duration of the game.
## Game Mechanics
### Actors
The game involves two types of participants (or Players): **Challengers** and **Defenders**.
These players are grouped into separate teams, each employing distinct strategies to interact with the game.
Team members share a common goal regarding the game's outcome. Players interact with the game primarily through _moves_.
### Moves
A Move is a challenge against a claim's execution trace and must include an alternate claim asserting a different trace.
Moves can either be attacks or defenses and serve to update to DAG by adding nodes and edges targeting the disputed
claim.
Initially, claims added to the DAG are _uncontesteed_ (i.e. not **countered**). Once a move targets a claim, that claim
is considered countered.
The status of a claim &mdash; whether it's countered or not &mdash; helps determine its validity and, ultimately, the
game's winner.
#### Attack
A logical move made when a claim is disagreed with.
A claim at the relative attack position to a node, `n`, in the Game Tree commits to half
of the trace of the `n`’s claim.
The attack position relative to a node can be calculated by multiplying its gindex by 2.
To illustrate this, here's a Game Tree highlighting an attack on a Claim positioned at 6.
![Attacking node 6](assets/attack.png)
Attacking the node at 6 moves creates a new claim positioned at 12.
#### Defend
The logical move against a claim when you agree with both it and its parent.
A defense at the relative position to a node, `n``, in the Game Tree commits to the first half of n + 1’s trace range.
![Defend at 4](assets/defend.png)
Note that because of this, some nodes may never exist within the Game Tree.
However, they're not necessary as these nodes have complimentary, valid positions
with the same trace index within the tree. For example, a Position with gindex 5 has the same
trace index as another Position with gindex 2. We can verify that all trace indices have valid moves within the game:
![Game Tree Showing All Valid Move Positions](assets/valid-moves.png)
There may be multiple claims at the same position, so long as their `ClaimHash` are unique.
Each move adds new claims to the Game Tree at strictly increasing depth.
Once a claim is at `MAX_GAME_DEPTH`, the only way to dispute such claims is to **step**.
### Step
At `MAX_GAME_DEPTH`, the position of claims correspond to indices of an execution trace.
It's at this point that the FDG is able to query the VM to determine the validity of claims,
by checking the states they're committing to.
This is done by applying the VM's STF to the state a claim commits to.
If the STF post-state does not match the claimed state, the challenge succeeds.
```solidity
/// @notice Perform an instruction step via an on-chain fault proof processor.
/// @dev This function should point to a fault proof processor in order to execute
/// a step in the fault proof program on-chain. The interface of the fault proof
/// processor contract should adhere to the `IBigStepper` interface.
/// @param _claimIndex The index of the challenged claim within `claimData`.
/// @param _isAttack Whether or not the step is an attack or a defense.
/// @param _stateData The stateData of the step is the preimage of the claim at the given
/// prestate, which is at `_stateIndex` if the move is an attack and `_claimIndex` if
/// the move is a defense. If the step is an attack on the first instruction, it is
/// the absolute prestate of the fault proof VM.
/// @param _proof Proof to access memory nodes in the VM's merkle state tree.
function step(uint256 _claimIndex, bool isAttack, bytes calldata _stateData, bytes calldata _proof) external;
```
### Step Types
Similar to moves, there are two ways to step on a claim; attack or defend.
These determine the pre-state input to the VM STF and the expected output.
- **Attack Step** - Challenges a claim by providing a pre-state, proving an invalid state transition.
It uses the previous state in the execution trace as input and expects the disputed claim's state as output.
There must exist a claim in the DAG that commits to the input.
- **Defense Step** - Challenges a claim by proving it was an invalid attack,
thereby defending the disputed ancestor's claim. It uses the disputed claim's state as input and expects
the next state in the execution trace as output. There must exist a claim in the DAG that commits to the
expected output.
The FDG step handles the inputs to the VM and asserts the expected output.
A step that successfully proves an invalid post-state (when attacking) or pre-state (when defending) is a
successful counter against the disputed claim.
Players interface with `step` by providing an indicator of attack and state data (including any proofs)
that corresponds to the expected pre/post state (depending on whether it's an attack or defend).
The FDG will assert that an existing claim commits to the state data provided by players.
### Team Dynamics
Challengers seek to dispute the root claim, while Defenders aim to support it.
Both types of actors will move accordingly to support their team. For Challengers, this means
attacking the root claim and disputing claims positioned at even depths in the Game Tree.
Defenders do the opposite by disputing claims positioned at odd depths.
Players on either team are motivated to support the actions of their teammates.
This involves countering disputes against claims made by their team (assuming these claims are honest).
Uncontested claims are likely to result in a loss, as explained later under [Resolution](#resolution).
### Game Clock
Every claim in the game has a Clock. A claim's inherits the clock of its grandparent claim in the
DAG (and so on). Akin to a chess clock, it keeps track of the total time each team takes to make
moves, preventing delays.
Making a move resumes the clock for the disputed claim and puases it for the newly added one.
A move against a particular claim is no longer possible once the parent of the disputed claim's Clock
has exceeded half of the `GAME_DURATION`. By which point, the claim's clock has _expired_.
### Resolution
Resolving the FDG determines which team won.
This is done by examining the left-most, uncontested (or undisputed) claim.
If the depth of this claim is odd then Challengers win, otherwise Defenders win.
Resolution is only possible once the Clock of the left-most, uncontested claim has expired.
Given these rules, players are motivated to move quickly to challenge dishonest claims.
Each move bisects the execution trace and eventually, `MAX_GAME_DEPTH` is reached where disputes
can be settled conclusively. Dishonest players are disincentivized to participate, via backwards induction,
as an invalid claim won't remain uncontested. Further incentives can be added to the game by requiring
claims to be bonded, while rewarding game winners using the bonds of dishonest claims.
# Needs to point to docker, otherwise you'll get the error: exec: "docker": executable file not found in $PATH
PATH=/
# Runs every minute
* * * * * /usr/local/bin/docker-compose -f /path/to/docker-compose.yml --profile 1minute up -d
# Runs every 5 minutes
*/5 * * * * /usr/local/bin/docker-compose -f /path/to/docker-compose.yml --profile 5minutes up -d
\ No newline at end of file
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
\ No newline at end of file
version: "3"
services:
pushgateway:
image: prom/pushgateway
ports:
- "9091:9091"
restart: unless-stopped
read_only: true
security_opt:
- "no-new-privileges:true"
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
depends_on:
- pushgateway
command:
- '--config.file=/etc/prometheus/prometheus.yml'
read_only: true
security_opt:
- "no-new-privileges:true"
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
depends_on:
- prometheus
environment:
# TODO - Replace with ENV variables to allow for better security
- GF_SECURITY_ADMIN_PASSWORD=adminpassword
volumes:
- ./datesources.yml:/etc/grafana/provisioning/datasources/datasources.yaml
security_opt:
- "no-new-privileges:true"
# Example Test Service Config
# my_test_service:
# build: ./my_test_service_dir
# container_name: my_test_service
# profiles: ["1minute"]
# depends_on:
# - prometheus
# restart: no
FROM node:18
WORKDIR /app
COPY ./ ./
RUN npm i -g pnpm
RUN pnpm install
CMD /bin/sh /app/start.sh
MIT License
Copyright (c) 2023 Optimism
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "@eth-optimism/ufm-test-service-metamask",
"version": "0.1.0",
"description": "A User facing monitoring Test Service for MetaMask",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git",
"directory": "ufm-test-services/metamask"
},
"homepage": "https://optimism.io",
"type": "module",
"scripts": {
"lint": "prettier --check .",
"lint:fix": "prettier --write ."
},
"devDependencies": {
"typescript": "^5.1.6"
}
}
#!/bin/bash
\ No newline at end of file
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./src",
"noEmit": true,
"target": "ESNext",
"lib": ["esnext"],
"module": "esnext",
"moduleResolution": "Node",
"isolatedModules": true,
"allowUnreachableCode": false,
"skipLibCheck": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noImplicitThis": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true
},
"include": ["./src"]
}
global:
scrape_interval: 5s
scrape_configs:
- job_name: 'pushgateway'
static_configs:
- targets: ['pushgateway:9091']
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment